mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-26 21:17:23 +00:00
Svc REST: De-layer Update
This is the last layered method. All allocator logic is moved to the beginUpdate() path. Removing the now-useless layer will happen in a subsequent commit.
This commit is contained in:
parent
89a9ca52bc
commit
ccf3376570
@ -45,6 +45,7 @@ import (
|
|||||||
schedulinghelper "k8s.io/component-helpers/scheduling/corev1"
|
schedulinghelper "k8s.io/component-helpers/scheduling/corev1"
|
||||||
apiservice "k8s.io/kubernetes/pkg/api/service"
|
apiservice "k8s.io/kubernetes/pkg/api/service"
|
||||||
"k8s.io/kubernetes/pkg/apis/core"
|
"k8s.io/kubernetes/pkg/apis/core"
|
||||||
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
"k8s.io/kubernetes/pkg/apis/core/helper"
|
"k8s.io/kubernetes/pkg/apis/core/helper"
|
||||||
podshelper "k8s.io/kubernetes/pkg/apis/core/pods"
|
podshelper "k8s.io/kubernetes/pkg/apis/core/pods"
|
||||||
corev1 "k8s.io/kubernetes/pkg/apis/core/v1"
|
corev1 "k8s.io/kubernetes/pkg/apis/core/v1"
|
||||||
@ -4528,6 +4529,18 @@ func validateServiceExternalTrafficFieldsValue(service *core.Service) field.Erro
|
|||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateServiceExternalTrafficFieldsUpdate(before, after *api.Service) field.ErrorList {
|
||||||
|
allErrs := field.ErrorList{}
|
||||||
|
|
||||||
|
if apiservice.NeedsHealthCheck(before) && apiservice.NeedsHealthCheck(after) {
|
||||||
|
if after.Spec.HealthCheckNodePort != before.Spec.HealthCheckNodePort {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "healthCheckNodePort"), "field is immutable"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
// validateServiceInternalTrafficFieldsValue validates InternalTraffic related
|
// validateServiceInternalTrafficFieldsValue validates InternalTraffic related
|
||||||
// spec have legal value.
|
// spec have legal value.
|
||||||
func validateServiceInternalTrafficFieldsValue(service *core.Service) field.ErrorList {
|
func validateServiceInternalTrafficFieldsValue(service *core.Service) field.ErrorList {
|
||||||
@ -4592,6 +4605,8 @@ func ValidateServiceUpdate(service, oldService *core.Service) field.ErrorList {
|
|||||||
upgradeDowngradeLoadBalancerClassErrs := validateLoadBalancerClassField(oldService, service)
|
upgradeDowngradeLoadBalancerClassErrs := validateLoadBalancerClassField(oldService, service)
|
||||||
allErrs = append(allErrs, upgradeDowngradeLoadBalancerClassErrs...)
|
allErrs = append(allErrs, upgradeDowngradeLoadBalancerClassErrs...)
|
||||||
|
|
||||||
|
allErrs = append(allErrs, validateServiceExternalTrafficFieldsUpdate(oldService, service)...)
|
||||||
|
|
||||||
return append(allErrs, ValidateService(service)...)
|
return append(allErrs, ValidateService(service)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,6 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"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"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
apiservice "k8s.io/kubernetes/pkg/api/service"
|
apiservice "k8s.io/kubernetes/pkg/api/service"
|
||||||
@ -253,7 +252,6 @@ func (al *RESTAllocStuff) healthCheckNodePortUpdate(oldService, service *api.Ser
|
|||||||
oldHealthCheckNodePort := oldService.Spec.HealthCheckNodePort
|
oldHealthCheckNodePort := oldService.Spec.HealthCheckNodePort
|
||||||
|
|
||||||
needsHealthCheckNodePort := apiservice.NeedsHealthCheck(service)
|
needsHealthCheckNodePort := apiservice.NeedsHealthCheck(service)
|
||||||
newHealthCheckNodePort := service.Spec.HealthCheckNodePort
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
// Case 1: Transition from don't need HealthCheckNodePort to needs HealthCheckNodePort.
|
// Case 1: Transition from don't need HealthCheckNodePort to needs HealthCheckNodePort.
|
||||||
@ -271,130 +269,88 @@ func (al *RESTAllocStuff) healthCheckNodePortUpdate(oldService, service *api.Ser
|
|||||||
klog.Infof("Transition to non LoadBalancer type service or LoadBalancer type service with ExternalTrafficPolicy=Global")
|
klog.Infof("Transition to non LoadBalancer type service or LoadBalancer type service with ExternalTrafficPolicy=Global")
|
||||||
klog.V(4).Infof("Releasing healthCheckNodePort: %d", oldHealthCheckNodePort)
|
klog.V(4).Infof("Releasing healthCheckNodePort: %d", oldHealthCheckNodePort)
|
||||||
nodePortOp.ReleaseDeferred(int(oldHealthCheckNodePort))
|
nodePortOp.ReleaseDeferred(int(oldHealthCheckNodePort))
|
||||||
|
|
||||||
// Case 3: Remain in needs HealthCheckNodePort.
|
|
||||||
// Reject changing the value of the HealthCheckNodePort field.
|
|
||||||
case neededHealthCheckNodePort && needsHealthCheckNodePort:
|
|
||||||
if oldHealthCheckNodePort != newHealthCheckNodePort {
|
|
||||||
//FIXME: Let validation do this.
|
|
||||||
klog.Warningf("Attempt to change value of health check node port DENIED")
|
|
||||||
fldPath := field.NewPath("spec", "healthCheckNodePort")
|
|
||||||
el := field.ErrorList{field.Invalid(fldPath, newHealthCheckNodePort,
|
|
||||||
"cannot change healthCheckNodePort on loadBalancer service with externalTraffic=Local during update")}
|
|
||||||
return false, errors.NewInvalid(api.Kind("Service"), service.Name, el)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
func (rs *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||||
oldObj, err := rs.services.Get(ctx, name, &metav1.GetOptions{})
|
return rs.services.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||||
if err != nil {
|
}
|
||||||
// Support create on update, if forced to.
|
|
||||||
if forceAllowCreate {
|
func (al *RESTAllocStuff) allocateUpdate(service, oldService *api.Service, dryRun bool) (transaction, error) {
|
||||||
obj, err := objInfo.UpdatedObject(ctx, nil)
|
result := metaTransaction{}
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
// Ensure IP family fields are correctly initialized. We do it here, since
|
||||||
}
|
// we want this to be visible even when dryRun == true.
|
||||||
createdObj, err := rs.Create(ctx, obj, createValidation, &metav1.CreateOptions{DryRun: options.DryRun})
|
if err := al.initIPFamilyFields(oldService, service); err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
return createdObj, true, nil
|
|
||||||
}
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
oldService := oldObj.(*api.Service)
|
|
||||||
obj, err := objInfo.UpdatedObject(ctx, oldService)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
service := obj.(*api.Service)
|
// Allocate ClusterIPs
|
||||||
|
//FIXME: we need to put values in, even if dry run - else validation should
|
||||||
if !rest.ValidNamespace(ctx, &service.ObjectMeta) {
|
//not pass. It does but that should be fixed. Plumb dryRun thru update
|
||||||
return nil, false, errors.NewConflict(api.Resource("services"), service.Namespace, fmt.Errorf("Service.Namespace does not match the provided context"))
|
//logic.
|
||||||
}
|
// xref: https://groups.google.com/g/kubernetes-sig-api-machinery/c/_-5TKHXHcXE/m/RfKj7CtzAQAJ
|
||||||
|
if !dryRun {
|
||||||
// Copy over non-user fields
|
if txn, err := al.allocUpdateServiceClusterIPsNew(service, oldService); err != nil {
|
||||||
if err := rest.BeforeUpdate(rs.strategy, ctx, service, oldService); err != nil {
|
result.Revert()
|
||||||
return nil, false, err
|
return nil, err
|
||||||
}
|
} else {
|
||||||
|
result = append(result, txn)
|
||||||
var allocated map[api.IPFamily]string
|
|
||||||
var toReleaseIPs map[api.IPFamily]string
|
|
||||||
|
|
||||||
performRelease := false // when set, any clusterIP that should be released will be released
|
|
||||||
// cleanup
|
|
||||||
// on failure: Any allocated ip must be released back
|
|
||||||
// on failure: any ip that should be released, will *not* be released
|
|
||||||
// on success: any ip that should be released, will be released
|
|
||||||
defer func() {
|
|
||||||
//FIXME: plumb dryRun down here
|
|
||||||
// release the allocated, this is expected to be cleared if the entire function ran to success
|
|
||||||
if allocated_released, err := rs.alloc.releaseClusterIPs(allocated); err != nil {
|
|
||||||
klog.V(4).Infof("service %v/%v failed to clean up after failed service update error:%v. Allocated/Released:%v/%v", service.Namespace, service.Name, err, allocated, allocated_released)
|
|
||||||
|
|
||||||
}
|
|
||||||
// performRelease is set when the enture function ran to success
|
|
||||||
if performRelease {
|
|
||||||
//FIXME: plumb dryRun down here
|
|
||||||
if toReleaseIPs_released, err := rs.alloc.releaseClusterIPs(toReleaseIPs); err != nil {
|
|
||||||
klog.V(4).Infof("service %v/%v failed to clean up after failed service update error:%v. ShouldRelease/Released:%v/%v", service.Namespace, service.Name, err, toReleaseIPs, toReleaseIPs_released)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
nodePortOp := portallocator.StartOperation(rs.alloc.serviceNodePorts, dryrun.IsDryRun(options.DryRun))
|
// Allocate ports
|
||||||
defer nodePortOp.Finish()
|
if txn, err := al.allocUpdateServiceNodePortsNew(service, oldService, dryRun); err != nil {
|
||||||
|
result.Revert()
|
||||||
// try set ip families (for missing ip families)
|
return nil, err
|
||||||
if err := rs.alloc.initIPFamilyFields(oldService, service); err != nil {
|
} else {
|
||||||
return nil, false, err
|
result = append(result, txn)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !dryrun.IsDryRun(options.DryRun) {
|
return result, nil
|
||||||
allocated, toReleaseIPs, err = rs.alloc.handleClusterIPsForUpdatedService(oldService, service)
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
//FIXME: rename and merge with updateNodePorts?
|
||||||
}
|
func (al *RESTAllocStuff) allocUpdateServiceNodePortsNew(service, oldService *api.Service, dryRun bool) (transaction, error) {
|
||||||
|
// The allocator tracks dry-run-ness internally.
|
||||||
|
nodePortOp := portallocator.StartOperation(al.serviceNodePorts, dryRun)
|
||||||
|
|
||||||
|
txn := callbackTransaction{
|
||||||
|
commit: func() {
|
||||||
|
nodePortOp.Commit()
|
||||||
|
// We don't NEED to call Finish() here, but for that package says
|
||||||
|
// to, so for future-safety, we will.
|
||||||
|
nodePortOp.Finish()
|
||||||
|
},
|
||||||
|
revert: func() {
|
||||||
|
// Weirdly named but this will revert if commit wasn't called
|
||||||
|
nodePortOp.Finish()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update service from NodePort or LoadBalancer to ExternalName or ClusterIP, should release NodePort if exists.
|
// Update service from NodePort or LoadBalancer to ExternalName or ClusterIP, should release NodePort if exists.
|
||||||
if (oldService.Spec.Type == api.ServiceTypeNodePort || oldService.Spec.Type == api.ServiceTypeLoadBalancer) &&
|
if (oldService.Spec.Type == api.ServiceTypeNodePort || oldService.Spec.Type == api.ServiceTypeLoadBalancer) &&
|
||||||
(service.Spec.Type == api.ServiceTypeExternalName || service.Spec.Type == api.ServiceTypeClusterIP) {
|
(service.Spec.Type == api.ServiceTypeExternalName || service.Spec.Type == api.ServiceTypeClusterIP) {
|
||||||
releaseNodePorts(oldService, nodePortOp)
|
releaseNodePorts(oldService, nodePortOp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update service from any type to NodePort or LoadBalancer, should update NodePort.
|
// Update service from any type to NodePort or LoadBalancer, should update NodePort.
|
||||||
if service.Spec.Type == api.ServiceTypeNodePort || service.Spec.Type == api.ServiceTypeLoadBalancer {
|
if service.Spec.Type == api.ServiceTypeNodePort || service.Spec.Type == api.ServiceTypeLoadBalancer {
|
||||||
if err := updateNodePorts(oldService, service, nodePortOp); err != nil {
|
if err := updateNodePorts(oldService, service, nodePortOp); err != nil {
|
||||||
return nil, false, err
|
txn.Revert()
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update service from LoadBalancer to non-LoadBalancer, should remove any LoadBalancerStatus.
|
|
||||||
if service.Spec.Type != api.ServiceTypeLoadBalancer {
|
|
||||||
// Although loadbalancer delete is actually asynchronous, we don't need to expose the user to that complexity.
|
|
||||||
service.Status.LoadBalancer = api.LoadBalancerStatus{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle ExternalTraffic related updates.
|
// Handle ExternalTraffic related updates.
|
||||||
success, err := rs.alloc.healthCheckNodePortUpdate(oldService, service, nodePortOp)
|
success, err := al.healthCheckNodePortUpdate(oldService, service, nodePortOp)
|
||||||
if !success || err != nil {
|
if !success || err != nil {
|
||||||
return nil, false, err
|
txn.Revert()
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
out, created, err := rs.services.Update(ctx, service.Name, rest.DefaultUpdatedObjectInfo(service), createValidation, updateValidation, forceAllowCreate, options)
|
return txn, nil
|
||||||
if err == nil {
|
|
||||||
el := nodePortOp.Commit()
|
|
||||||
if el != nil {
|
|
||||||
// problems should be fixed by an eventual reconciliation / restart
|
|
||||||
utilruntime.HandleError(fmt.Errorf("error(s) committing NodePorts changes: %v", el))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// all good
|
|
||||||
allocated = nil // if something was allocated, keep it allocated
|
|
||||||
performRelease = true // if something that should be released then go ahead and release it
|
|
||||||
|
|
||||||
return out, created, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetResetFields implements rest.ResetFieldsStrategy
|
// GetResetFields implements rest.ResetFieldsStrategy
|
||||||
@ -635,6 +591,35 @@ func (al *RESTAllocStuff) allocServiceClusterIPs(service *api.Service, dryRun bo
|
|||||||
return allocated, err
|
return allocated, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//FIXME: rename and merge with handleClusterIPsForUpdatedService
|
||||||
|
func (al *RESTAllocStuff) allocUpdateServiceClusterIPsNew(service *api.Service, oldService *api.Service) (transaction, error) {
|
||||||
|
allocated, released, err := al.handleClusterIPsForUpdatedService(oldService, service)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// on failure: Any newly allocated IP must be released back
|
||||||
|
// on failure: Any previously allocated IP that would have been released,
|
||||||
|
// must *not* be released
|
||||||
|
// on success: Any previously allocated IP that should be released, will be
|
||||||
|
// released
|
||||||
|
txn := callbackTransaction{
|
||||||
|
commit: func() {
|
||||||
|
if actuallyReleased, err := al.releaseClusterIPs(released); err != nil {
|
||||||
|
klog.V(4).Infof("service %v/%v failed to clean up after failed service update error:%v. ShouldRelease/Released:%v/%v",
|
||||||
|
service.Namespace, service.Name, err, released, actuallyReleased)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
revert: func() {
|
||||||
|
if actuallyReleased, err := al.releaseClusterIPs(allocated); err != nil {
|
||||||
|
klog.V(4).Infof("service %v/%v failed to clean up after failed service update error:%v. Allocated/Released:%v/%v",
|
||||||
|
service.Namespace, service.Name, err, allocated, actuallyReleased)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return txn, nil
|
||||||
|
}
|
||||||
|
|
||||||
// handles type change/upgrade/downgrade change type for an update service
|
// handles type change/upgrade/downgrade change type for an update service
|
||||||
// this func does not perform actual release of clusterIPs. it returns
|
// this func does not perform actual release of clusterIPs. it returns
|
||||||
// a map[family]ip for the caller to release when everything else has
|
// a map[family]ip for the caller to release when everything else has
|
||||||
|
@ -133,14 +133,17 @@ func (s *serviceStorage) Create(ctx context.Context, obj runtime.Object, createV
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
func (s *serviceStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||||
obj, err := objInfo.UpdatedObject(ctx, nil)
|
ret, created, err := s.inner.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return ret, created, err
|
||||||
}
|
}
|
||||||
if !dryrun.IsDryRun(options.DryRun) {
|
if dryrun.IsDryRun(options.DryRun) {
|
||||||
s.saveService(obj.(*api.Service))
|
return ret.DeepCopyObject(), created, err
|
||||||
}
|
}
|
||||||
return obj, false, nil
|
svc := ret.(*api.Service)
|
||||||
|
s.saveService(svc)
|
||||||
|
|
||||||
|
return s.Services[name].DeepCopy(), created, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
func (s *serviceStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||||
@ -716,12 +719,13 @@ func TestServiceRegistryUpdateLoadBalancerService(t *testing.T) {
|
|||||||
svc2 := obj.(*api.Service).DeepCopy()
|
svc2 := obj.(*api.Service).DeepCopy()
|
||||||
svc2.Spec.Type = api.ServiceTypeLoadBalancer
|
svc2.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
svc2.Spec.AllocateLoadBalancerNodePorts = utilpointer.BoolPtr(true)
|
svc2.Spec.AllocateLoadBalancerNodePorts = utilpointer.BoolPtr(true)
|
||||||
if _, _, err := storage.Update(ctx, svc2.Name, rest.DefaultUpdatedObjectInfo(svc2), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
|
obj, _, err = storage.Update(ctx, svc2.Name, rest.DefaultUpdatedObjectInfo(svc2), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change port.
|
// Change port.
|
||||||
svc3 := svc2.DeepCopy()
|
svc3 := obj.(*api.Service).DeepCopy()
|
||||||
svc3.Spec.Ports[0].Port = 6504
|
svc3.Spec.Ports[0].Port = 6504
|
||||||
if _, _, err := storage.Update(ctx, svc3.Name, rest.DefaultUpdatedObjectInfo(svc3), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
|
if _, _, err := storage.Update(ctx, svc3.Name, rest.DefaultUpdatedObjectInfo(svc3), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
@ -971,7 +975,7 @@ func TestServiceRegistryIPUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update = createdService.DeepCopy()
|
update = updatedService.DeepCopy()
|
||||||
update.Spec.Ports[0].Port = 6503
|
update.Spec.Ports[0].Port = 6503
|
||||||
update.Spec.ClusterIP = testIP
|
update.Spec.ClusterIP = testIP
|
||||||
update.Spec.ClusterIPs[0] = testIP
|
update.Spec.ClusterIPs[0] = testIP
|
||||||
|
@ -326,15 +326,27 @@ 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)
|
||||||
|
|
||||||
|
// Fix up allocated values that the client may have not specified (for
|
||||||
|
// idempotence).
|
||||||
|
svcreg.PatchAllocatedValues(newSvc, oldSvc)
|
||||||
|
|
||||||
// Make sure ClusterIP and ClusterIPs are in sync. This has to happen
|
// Make sure ClusterIP and ClusterIPs are in sync. This has to happen
|
||||||
// early, before anyone looks at them.
|
// early, before anyone looks at them.
|
||||||
// NOTE: the args are (old, new)
|
// NOTE: the args are (old, new)
|
||||||
svcreg.NormalizeClusterIPs(oldSvc, newSvc)
|
svcreg.NormalizeClusterIPs(oldSvc, newSvc)
|
||||||
|
|
||||||
|
// Allocate and initialize fields.
|
||||||
|
txn, err := r.alloc.allocateUpdate(newSvc, oldSvc, 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5837,3 +5837,5 @@ func TestDeleteDryRun(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//FIXME: Tests for update()
|
||||||
|
@ -120,7 +120,6 @@ func (strategy svcStrategy) PrepareForUpdate(ctx context.Context, obj, old runti
|
|||||||
oldService := old.(*api.Service)
|
oldService := old.(*api.Service)
|
||||||
newService.Status = oldService.Status
|
newService.Status = oldService.Status
|
||||||
|
|
||||||
patchAllocatedValues(newService, oldService)
|
|
||||||
//FIXME: Normalize is now called from BeginUpdate in pkg/registry/core/service/storage
|
//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)
|
||||||
@ -305,11 +304,12 @@ func (serviceStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runt
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// patchAllocatedValues allows clients to avoid a read-modify-write cycle while
|
// PatchAllocatedValues allows clients to avoid a read-modify-write cycle while
|
||||||
// preserving values that we allocated on their behalf. For example, they
|
// preserving values that we allocated on their behalf. For example, they
|
||||||
// might create a Service without specifying the ClusterIP, in which case we
|
// might create a Service without specifying the ClusterIP, in which case we
|
||||||
// allocate one. If they resubmit that same YAML, we want it to succeed.
|
// allocate one. If they resubmit that same YAML, we want it to succeed.
|
||||||
func patchAllocatedValues(newSvc, oldSvc *api.Service) {
|
//FIXME: move this to pkg/registry/core/service/storage
|
||||||
|
func PatchAllocatedValues(newSvc, oldSvc *api.Service) {
|
||||||
if needsClusterIP(oldSvc) && needsClusterIP(newSvc) {
|
if needsClusterIP(oldSvc) && needsClusterIP(newSvc) {
|
||||||
if newSvc.Spec.ClusterIP == "" {
|
if newSvc.Spec.ClusterIP == "" {
|
||||||
newSvc.Spec.ClusterIP = oldSvc.Spec.ClusterIP
|
newSvc.Spec.ClusterIP = oldSvc.Spec.ClusterIP
|
||||||
@ -521,6 +521,13 @@ func dropTypeDependentFields(newSvc *api.Service, oldSvc *api.Service) {
|
|||||||
// NOTE: there are other fields like `selector` which we could wipe.
|
// NOTE: there are other fields like `selector` which we could wipe.
|
||||||
// Historically we did not wipe them and they are not allocated from
|
// Historically we did not wipe them and they are not allocated from
|
||||||
// finite pools, so we are (currently) choosing to leave them alone.
|
// finite pools, so we are (currently) choosing to leave them alone.
|
||||||
|
|
||||||
|
// Clear the load-balancer status if it is no longer appropriate. Although
|
||||||
|
// LB de-provisioning is actually asynchronous, we don't need to expose the
|
||||||
|
// user to that complexity.
|
||||||
|
if newSvc.Spec.Type != api.ServiceTypeLoadBalancer {
|
||||||
|
newSvc.Status.LoadBalancer = api.LoadBalancerStatus{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func needsClusterIP(svc *api.Service) bool {
|
func needsClusterIP(svc *api.Service) bool {
|
||||||
|
@ -22,7 +22,6 @@ import (
|
|||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
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/util/diff"
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
@ -124,75 +123,6 @@ func makeServiceWithClusterIp(clusterIP string, clusterIPs []string) *api.Servic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This should be done on types that are not part of our API
|
|
||||||
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: "no change",
|
|
||||||
tweakSvc: func(oldSvc, newSvc *api.Service) {
|
|
||||||
// nothing
|
|
||||||
},
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "change port",
|
|
||||||
tweakSvc: func(oldSvc, newSvc *api.Service) {
|
|
||||||
newSvc.Spec.Ports[0].Port++
|
|
||||||
},
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bad namespace",
|
|
||||||
tweakSvc: func(oldSvc, newSvc *api.Service) {
|
|
||||||
newSvc.Namespace = "#$%%invalid"
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "change name",
|
|
||||||
tweakSvc: func(oldSvc, newSvc *api.Service) {
|
|
||||||
newSvc.Name += "2"
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "change ClusterIP",
|
|
||||||
tweakSvc: func(oldSvc, newSvc *api.Service) {
|
|
||||||
oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"}
|
|
||||||
newSvc.Spec.ClusterIPs = []string{"4.3.2.1"}
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "change selector",
|
|
||||||
tweakSvc: func(oldSvc, newSvc *api.Service) {
|
|
||||||
newSvc.Spec.Selector = map[string]string{"newkey": "newvalue"}
|
|
||||||
},
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
strategy, _ := newStrategy("172.30.0.0/16", false)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServiceStatusStrategy(t *testing.T) {
|
func TestServiceStatusStrategy(t *testing.T) {
|
||||||
_, testStatusStrategy := newStrategy("10.0.0.0/16", false)
|
_, testStatusStrategy := newStrategy("10.0.0.0/16", false)
|
||||||
ctx := genericapirequest.NewDefaultContext()
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
Loading…
Reference in New Issue
Block a user