func ReaperFor(kind unversioned.GroupKind, c client.Interface) (Reaper, error) { switch kind { case api.Kind("ReplicationController"): return &ReplicationControllerReaper{c, Interval, Timeout}, nil case extensions.Kind("ReplicaSet"): return &ReplicaSetReaper{c, Interval, Timeout}, nil case extensions.Kind("DaemonSet"): return &DaemonSetReaper{c, Interval, Timeout}, nil case api.Kind("Pod"): return &PodReaper{c}, nil case api.Kind("Service"): return &ServiceReaper{c}, nil case extensions.Kind("Job"), batch.Kind("Job"): return &JobReaper{c, Interval, Timeout}, nil case extensions.Kind("Deployment"): return &DeploymentReaper{c, Interval, Timeout}, nil } return nil, &NoSuchReaperError{kind} }
func TestServiceReplenishmentUpdateFunc(t *testing.T) { mockReplenish := &testReplenishment{} options := ReplenishmentControllerOptions{ GroupKind: api.Kind("Service"), ReplenishmentFunc: mockReplenish.Replenish, ResyncPeriod: controller.NoResyncPeriodFunc, } oldService := &api.Service{ ObjectMeta: api.ObjectMeta{Namespace: "test", Name: "mysvc"}, Spec: api.ServiceSpec{ Type: api.ServiceTypeNodePort, Ports: []api.ServicePort{{ Port: 80, TargetPort: intstr.FromInt(80), }}, }, } newService := &api.Service{ ObjectMeta: api.ObjectMeta{Namespace: "test", Name: "mysvc"}, Spec: api.ServiceSpec{ Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 80, TargetPort: intstr.FromInt(80), }}}, } updateFunc := ServiceReplenishmentUpdateFunc(&options) updateFunc(oldService, newService) if mockReplenish.groupKind != api.Kind("Service") { t.Errorf("Unexpected group kind %v", mockReplenish.groupKind) } if mockReplenish.namespace != oldService.Namespace { t.Errorf("Unexpected namespace %v", mockReplenish.namespace) } }
func TestCheckInvalidErr(t *testing.T) { tests := []struct { err error expected string }{ { errors.NewInvalid(api.Kind("Invalid1"), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field"), "single", "details")}), `Error from server: Invalid1 "invalidation" is invalid: field: Invalid value: "single": details`, }, { errors.NewInvalid(api.Kind("Invalid2"), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field1"), "multi1", "details"), field.Invalid(field.NewPath("field2"), "multi2", "details")}), `Error from server: Invalid2 "invalidation" is invalid: [field1: Invalid value: "multi1": details, field2: Invalid value: "multi2": details]`, }, { errors.NewInvalid(api.Kind("Invalid3"), "invalidation", field.ErrorList{}), `Error from server: Invalid3 "invalidation" is invalid: <nil>`, }, } var errReturned string errHandle := func(err string) { errReturned = err } for _, test := range tests { checkErr("", test.err, errHandle) if errReturned != test.expected { t.Fatalf("Got: %s, expected: %s", errReturned, test.expected) } } }
func TestSyncResourceQuotaNoChange(t *testing.T) { resourceQuota := api.ResourceQuota{ ObjectMeta: api.ObjectMeta{ Namespace: "default", Name: "rq", }, Spec: api.ResourceQuotaSpec{ Hard: api.ResourceList{ api.ResourceCPU: resource.MustParse("4"), }, }, Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourceCPU: resource.MustParse("4"), }, Used: api.ResourceList{ api.ResourceCPU: resource.MustParse("0"), }, }, } kubeClient := fake.NewSimpleClientset(&api.PodList{}, &resourceQuota) resourceQuotaControllerOptions := &ResourceQuotaControllerOptions{ KubeClient: kubeClient, ResyncPeriod: controller.NoResyncPeriodFunc, Registry: install.NewRegistry(kubeClient), GroupKindsToReplenish: []unversioned.GroupKind{ api.Kind("Pod"), api.Kind("Service"), api.Kind("ReplicationController"), api.Kind("PersistentVolumeClaim"), }, ControllerFactory: NewReplenishmentControllerFactoryFromClient(kubeClient), ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc, } quotaController := NewResourceQuotaController(resourceQuotaControllerOptions) err := quotaController.syncResourceQuota(resourceQuota) if err != nil { t.Fatalf("Unexpected error %v", err) } expectedActionSet := sets.NewString( strings.Join([]string{"list", "replicationcontrollers", ""}, "-"), strings.Join([]string{"list", "services", ""}, "-"), strings.Join([]string{"list", "pods", ""}, "-"), strings.Join([]string{"list", "resourcequotas", ""}, "-"), strings.Join([]string{"list", "secrets", ""}, "-"), strings.Join([]string{"list", "persistentvolumeclaims", ""}, "-"), ) actionSet := sets.NewString() for _, action := range kubeClient.Actions() { actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-")) } if !actionSet.HasAll(expectedActionSet.List()...) { t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet)) } }
// NewPodEvaluator returns an evaluator that can evaluate pods func NewPodEvaluator(kubeClient clientset.Interface) quota.Evaluator { computeResources := []api.ResourceName{ api.ResourceCPU, api.ResourceMemory, api.ResourceRequestsCPU, api.ResourceRequestsMemory, api.ResourceLimitsCPU, api.ResourceLimitsMemory, } allResources := append(computeResources, api.ResourcePods) return &generic.GenericEvaluator{ Name: "Evaluator.Pod", InternalGroupKind: api.Kind("Pod"), InternalOperationResources: map[admission.Operation][]api.ResourceName{ admission.Create: allResources, // TODO: the quota system can only charge for deltas on compute resources when pods support updates. // admission.Update: computeResources, }, GetFuncByNamespace: func(namespace, name string) (runtime.Object, error) { return kubeClient.Core().Pods(namespace).Get(name) }, ConstraintsFunc: PodConstraintsFunc, MatchedResourceNames: allResources, MatchesScopeFunc: PodMatchesScopeFunc, UsageFunc: PodUsageFunc, ListFuncByNamespace: func(namespace string, options api.ListOptions) (runtime.Object, error) { return kubeClient.Core().Pods(namespace).List(options) }, } }
func TestErrorNew(t *testing.T) { err := NewAlreadyExists(api.Resource("tests"), "1") if !IsAlreadyExists(err) { t.Errorf("expected to be %s", unversioned.StatusReasonAlreadyExists) } if IsConflict(err) { t.Errorf("expected to not be %s", unversioned.StatusReasonConflict) } if IsNotFound(err) { t.Errorf(fmt.Sprintf("expected to not be %s", unversioned.StatusReasonNotFound)) } if IsInvalid(err) { t.Errorf("expected to not be %s", unversioned.StatusReasonInvalid) } if IsBadRequest(err) { t.Errorf("expected to not be %s", unversioned.StatusReasonBadRequest) } if IsForbidden(err) { t.Errorf("expected to not be %s", unversioned.StatusReasonForbidden) } if IsServerTimeout(err) { t.Errorf("expected to not be %s", unversioned.StatusReasonServerTimeout) } if IsMethodNotSupported(err) { t.Errorf("expected to not be %s", unversioned.StatusReasonMethodNotAllowed) } if !IsConflict(NewConflict(api.Resource("tests"), "2", errors.New("message"))) { t.Errorf("expected to be conflict") } if !IsNotFound(NewNotFound(api.Resource("tests"), "3")) { t.Errorf("expected to be %s", unversioned.StatusReasonNotFound) } if !IsInvalid(NewInvalid(api.Kind("Test"), "2", nil)) { t.Errorf("expected to be %s", unversioned.StatusReasonInvalid) } if !IsBadRequest(NewBadRequest("reason")) { t.Errorf("expected to be %s", unversioned.StatusReasonBadRequest) } if !IsForbidden(NewForbidden(api.Resource("tests"), "2", errors.New("reason"))) { t.Errorf("expected to be %s", unversioned.StatusReasonForbidden) } if !IsUnauthorized(NewUnauthorized("reason")) { t.Errorf("expected to be %s", unversioned.StatusReasonUnauthorized) } if !IsServerTimeout(NewServerTimeout(api.Resource("tests"), "reason", 0)) { t.Errorf("expected to be %s", unversioned.StatusReasonServerTimeout) } if time, ok := SuggestsClientDelay(NewServerTimeout(api.Resource("tests"), "doing something", 10)); time != 10 || !ok { t.Errorf("expected to be %s", unversioned.StatusReasonServerTimeout) } if time, ok := SuggestsClientDelay(NewTimeoutError("test reason", 10)); time != 10 || !ok { t.Errorf("expected to be %s", unversioned.StatusReasonTimeout) } if !IsMethodNotSupported(NewMethodNotSupported(api.Resource("foos"), "delete")) { t.Errorf("expected to be %s", unversioned.StatusReasonMethodNotAllowed) } }
// scalerScaleAndWait scales a controller using a Scaler and a real client. func (r *RollingUpdater) scaleAndWaitWithScaler(rc *api.ReplicationController, retry *RetryParams, wait *RetryParams) (*api.ReplicationController, error) { scaler, err := ScalerFor(api.Kind("ReplicationController"), r.c) if err != nil { return nil, fmt.Errorf("Couldn't make scaler: %s", err) } if err := scaler.Scale(rc.Namespace, rc.Name, uint(rc.Spec.Replicas), &ScalePrecondition{-1, ""}, retry, wait); err != nil { return nil, err } return r.c.ReplicationControllers(rc.Namespace).Get(rc.Name) }
func TestObjectReplenishmentDeleteFunc(t *testing.T) { mockReplenish := &testReplenishment{} options := ReplenishmentControllerOptions{ GroupKind: api.Kind("Pod"), ReplenishmentFunc: mockReplenish.Replenish, ResyncPeriod: controller.NoResyncPeriodFunc, } oldPod := &api.Pod{ ObjectMeta: api.ObjectMeta{Namespace: "test", Name: "pod"}, Status: api.PodStatus{Phase: api.PodRunning}, } deleteFunc := ObjectReplenishmentDeleteFunc(&options) deleteFunc(oldPod) if mockReplenish.groupKind != api.Kind("Pod") { t.Errorf("Unexpected group kind %v", mockReplenish.groupKind) } if mockReplenish.namespace != oldPod.Namespace { t.Errorf("Unexpected namespace %v", mockReplenish.namespace) } }
// NewReplicationControllerEvaluator returns an evaluator that can evaluate replication controllers func NewReplicationControllerEvaluator(kubeClient clientset.Interface) quota.Evaluator { allResources := []api.ResourceName{api.ResourceReplicationControllers} return &generic.GenericEvaluator{ Name: "Evaluator.ReplicationController", InternalGroupKind: api.Kind("ReplicationController"), InternalOperationResources: map[admission.Operation][]api.ResourceName{ admission.Create: allResources, }, MatchedResourceNames: allResources, MatchesScopeFunc: generic.MatchesNoScopeFunc, ConstraintsFunc: generic.ObjectCountConstraintsFunc(api.ResourceReplicationControllers), UsageFunc: generic.ObjectCountUsageFunc(api.ResourceReplicationControllers), ListFuncByNamespace: func(namespace string, options api.ListOptions) (runtime.Object, error) { return kubeClient.Core().ReplicationControllers(namespace).List(options) }, } }
func (s strategy) Export(obj runtime.Object, exact bool) error { t, ok := obj.(*api.Secret) if !ok { // unexpected programmer error return fmt.Errorf("unexpected object: %v", obj) } s.PrepareForCreate(obj) if exact { return nil } // secrets that are tied to the UID of a service account cannot be exported anyway if t.Type == api.SecretTypeServiceAccountToken || len(t.Annotations[api.ServiceAccountUIDKey]) > 0 { errs := []*field.Error{ field.Invalid(field.NewPath("type"), t, "can not export service account secrets"), } return errors.NewInvalid(api.Kind("Secret"), t.Name, errs) } return nil }
// NewServiceEvaluator returns an evaluator that can evaluate service quotas func NewServiceEvaluator(kubeClient clientset.Interface) quota.Evaluator { allResources := []api.ResourceName{ api.ResourceServices, api.ResourceServicesNodePorts, api.ResourceServicesLoadBalancers, } return &generic.GenericEvaluator{ Name: "Evaluator.Service", InternalGroupKind: api.Kind("Service"), InternalOperationResources: map[admission.Operation][]api.ResourceName{ admission.Create: allResources, admission.Update: allResources, }, MatchedResourceNames: allResources, MatchesScopeFunc: generic.MatchesNoScopeFunc, ConstraintsFunc: generic.ObjectCountConstraintsFunc(api.ResourceServices), UsageFunc: ServiceUsageFunc, ListFuncByNamespace: func(namespace string, options api.ListOptions) (runtime.Object, error) { return kubeClient.Core().Services(namespace).List(options) }, } }
func TestSyncResourceQuota(t *testing.T) { podList := api.PodList{ Items: []api.Pod{ { ObjectMeta: api.ObjectMeta{Name: "pod-running", Namespace: "testing"}, Status: api.PodStatus{Phase: api.PodRunning}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}}, }, }, { ObjectMeta: api.ObjectMeta{Name: "pod-running-2", Namespace: "testing"}, Status: api.PodStatus{Phase: api.PodRunning}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}}, }, }, { ObjectMeta: api.ObjectMeta{Name: "pod-failed", Namespace: "testing"}, Status: api.PodStatus{Phase: api.PodFailed}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}}, }, }, }, } resourceQuota := api.ResourceQuota{ ObjectMeta: api.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: api.ResourceQuotaSpec{ Hard: api.ResourceList{ api.ResourceCPU: resource.MustParse("3"), api.ResourceMemory: resource.MustParse("100Gi"), api.ResourcePods: resource.MustParse("5"), }, }, } expectedUsage := api.ResourceQuota{ Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourceCPU: resource.MustParse("3"), api.ResourceMemory: resource.MustParse("100Gi"), api.ResourcePods: resource.MustParse("5"), }, Used: api.ResourceList{ api.ResourceCPU: resource.MustParse("200m"), api.ResourceMemory: resource.MustParse("2Gi"), api.ResourcePods: resource.MustParse("2"), }, }, } kubeClient := fake.NewSimpleClientset(&podList, &resourceQuota) resourceQuotaControllerOptions := &ResourceQuotaControllerOptions{ KubeClient: kubeClient, ResyncPeriod: controller.NoResyncPeriodFunc, Registry: install.NewRegistry(kubeClient), GroupKindsToReplenish: []unversioned.GroupKind{ api.Kind("Pod"), api.Kind("Service"), api.Kind("ReplicationController"), api.Kind("PersistentVolumeClaim"), }, ControllerFactory: NewReplenishmentControllerFactoryFromClient(kubeClient), ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc, } quotaController := NewResourceQuotaController(resourceQuotaControllerOptions) err := quotaController.syncResourceQuota(resourceQuota) if err != nil { t.Fatalf("Unexpected error %v", err) } expectedActionSet := sets.NewString( strings.Join([]string{"list", "replicationcontrollers", ""}, "-"), strings.Join([]string{"list", "services", ""}, "-"), strings.Join([]string{"list", "pods", ""}, "-"), strings.Join([]string{"list", "resourcequotas", ""}, "-"), strings.Join([]string{"list", "secrets", ""}, "-"), strings.Join([]string{"list", "persistentvolumeclaims", ""}, "-"), strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ) actionSet := sets.NewString() for _, action := range kubeClient.Actions() { actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-")) } if !actionSet.HasAll(expectedActionSet.List()...) { t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet)) } lastActionIndex := len(kubeClient.Actions()) - 1 usage := kubeClient.Actions()[lastActionIndex].(core.UpdateAction).GetObject().(*api.ResourceQuota) // ensure hard and used limits are what we expected for k, v := range expectedUsage.Status.Hard { actual := usage.Status.Hard[k] actualValue := actual.String() expectedValue := v.String() if expectedValue != actualValue { t.Errorf("Usage Hard: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue) } } for k, v := range expectedUsage.Status.Used { actual := usage.Status.Used[k] actualValue := actual.String() expectedValue := v.String() if expectedValue != actualValue { t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue) } } }
func (r *replenishmentControllerFactory) NewController(options *ReplenishmentControllerOptions) (framework.ControllerInterface, error) { var result framework.ControllerInterface if r.kubeClient != nil && r.kubeClient.Core().GetRESTClient().GetRateLimiter() != nil { metrics.RegisterMetricAndTrackRateLimiterUsage("replenishment_controller", r.kubeClient.Core().GetRESTClient().GetRateLimiter()) } switch options.GroupKind { case api.Kind("Pod"): if r.podInformer != nil { r.podInformer.AddEventHandler(framework.ResourceEventHandlerFuncs{ UpdateFunc: PodReplenishmentUpdateFunc(options), DeleteFunc: ObjectReplenishmentDeleteFunc(options), }) result = r.podInformer.GetController() break } r.podInformer = informers.CreateSharedPodInformer(r.kubeClient, options.ResyncPeriod()) result = r.podInformer case api.Kind("Service"): _, result = framework.NewInformer( &cache.ListWatch{ ListFunc: func(options api.ListOptions) (runtime.Object, error) { return r.kubeClient.Core().Services(api.NamespaceAll).List(options) }, WatchFunc: func(options api.ListOptions) (watch.Interface, error) { return r.kubeClient.Core().Services(api.NamespaceAll).Watch(options) }, }, &api.Service{}, options.ResyncPeriod(), framework.ResourceEventHandlerFuncs{ UpdateFunc: ServiceReplenishmentUpdateFunc(options), DeleteFunc: ObjectReplenishmentDeleteFunc(options), }, ) case api.Kind("ReplicationController"): _, result = framework.NewInformer( &cache.ListWatch{ ListFunc: func(options api.ListOptions) (runtime.Object, error) { return r.kubeClient.Core().ReplicationControllers(api.NamespaceAll).List(options) }, WatchFunc: func(options api.ListOptions) (watch.Interface, error) { return r.kubeClient.Core().ReplicationControllers(api.NamespaceAll).Watch(options) }, }, &api.ReplicationController{}, options.ResyncPeriod(), framework.ResourceEventHandlerFuncs{ DeleteFunc: ObjectReplenishmentDeleteFunc(options), }, ) case api.Kind("PersistentVolumeClaim"): _, result = framework.NewInformer( &cache.ListWatch{ ListFunc: func(options api.ListOptions) (runtime.Object, error) { return r.kubeClient.Core().PersistentVolumeClaims(api.NamespaceAll).List(options) }, WatchFunc: func(options api.ListOptions) (watch.Interface, error) { return r.kubeClient.Core().PersistentVolumeClaims(api.NamespaceAll).Watch(options) }, }, &api.PersistentVolumeClaim{}, options.ResyncPeriod(), framework.ResourceEventHandlerFuncs{ DeleteFunc: ObjectReplenishmentDeleteFunc(options), }, ) case api.Kind("Secret"): _, result = framework.NewInformer( &cache.ListWatch{ ListFunc: func(options api.ListOptions) (runtime.Object, error) { return r.kubeClient.Core().Secrets(api.NamespaceAll).List(options) }, WatchFunc: func(options api.ListOptions) (watch.Interface, error) { return r.kubeClient.Core().Secrets(api.NamespaceAll).Watch(options) }, }, &api.Secret{}, options.ResyncPeriod(), framework.ResourceEventHandlerFuncs{ DeleteFunc: ObjectReplenishmentDeleteFunc(options), }, ) case api.Kind("ConfigMap"): _, result = framework.NewInformer( &cache.ListWatch{ ListFunc: func(options api.ListOptions) (runtime.Object, error) { return r.kubeClient.Core().ConfigMaps(api.NamespaceAll).List(options) }, WatchFunc: func(options api.ListOptions) (watch.Interface, error) { return r.kubeClient.Core().ConfigMaps(api.NamespaceAll).Watch(options) }, }, &api.ConfigMap{}, options.ResyncPeriod(), framework.ResourceEventHandlerFuncs{ DeleteFunc: ObjectReplenishmentDeleteFunc(options), }, ) default: return nil, fmt.Errorf("no replenishment controller available for %s", options.GroupKind) } return result, nil }
func (reaper *ReplicationControllerReaper) Stop(namespace, name string, timeout time.Duration, gracePeriod *api.DeleteOptions) error { rc := reaper.ReplicationControllers(namespace) scaler, err := ScalerFor(api.Kind("ReplicationController"), *reaper) if err != nil { return err } ctrl, err := rc.Get(name) if err != nil { return err } if timeout == 0 { timeout = Timeout + time.Duration(10*ctrl.Spec.Replicas)*time.Second } // The rc manager will try and detect all matching rcs for a pod's labels, // and only sync the oldest one. This means if we have a pod with labels // [(k1: v1), (k2: v2)] and two rcs: rc1 with selector [(k1=v1)], and rc2 with selector [(k1=v1),(k2=v2)], // the rc manager will sync the older of the two rcs. // // If there are rcs with a superset of labels, eg: // deleting: (k1=v1), superset: (k2=v2, k1=v1) // - It isn't safe to delete the rc because there could be a pod with labels // (k1=v1) that isn't managed by the superset rc. We can't scale it down // either, because there could be a pod (k2=v2, k1=v1) that it deletes // causing a fight with the superset rc. // If there are rcs with a subset of labels, eg: // deleting: (k2=v2, k1=v1), subset: (k1=v1), superset: (k2=v2, k1=v1, k3=v3) // - Even if it's safe to delete this rc without a scale down because all it's pods // are being controlled by the subset rc the code returns an error. // In theory, creating overlapping controllers is user error, so the loop below // tries to account for this logic only in the common case, where we end up // with multiple rcs that have an exact match on selectors. overlappingCtrls, err := getOverlappingControllers(rc, ctrl) if err != nil { return fmt.Errorf("error getting replication controllers: %v", err) } exactMatchRCs := []api.ReplicationController{} overlapRCs := []string{} for _, overlappingRC := range overlappingCtrls { if len(overlappingRC.Spec.Selector) == len(ctrl.Spec.Selector) { exactMatchRCs = append(exactMatchRCs, overlappingRC) } else { overlapRCs = append(overlapRCs, overlappingRC.Name) } } if len(overlapRCs) > 0 { return fmt.Errorf( "Detected overlapping controllers for rc %v: %v, please manage deletion individually with --cascade=false.", ctrl.Name, strings.Join(overlapRCs, ",")) } if len(exactMatchRCs) == 1 { // No overlapping controllers. retry := NewRetryParams(reaper.pollInterval, reaper.timeout) waitForReplicas := NewRetryParams(reaper.pollInterval, timeout) if err = scaler.Scale(namespace, name, 0, nil, retry, waitForReplicas); err != nil { return err } } return rc.Delete(name) }
func (rs *REST) Update(ctx api.Context, name string, objInfo rest.UpdatedObjectInfo) (runtime.Object, bool, error) { oldService, err := rs.registry.GetService(ctx, name) if err != nil { return nil, false, err } obj, err := objInfo.UpdatedObject(ctx, oldService) if err != nil { return nil, false, err } service := obj.(*api.Service) if !api.ValidNamespace(ctx, &service.ObjectMeta) { return nil, false, errors.NewConflict(api.Resource("services"), service.Namespace, fmt.Errorf("Service.Namespace does not match the provided context")) } // Copy over non-user fields // TODO: make this a merge function if errs := validation.ValidateServiceUpdate(service, oldService); len(errs) > 0 { return nil, false, errors.NewInvalid(api.Kind("Service"), service.Name, errs) } nodePortOp := portallocator.StartOperation(rs.serviceNodePorts) defer nodePortOp.Finish() assignNodePorts := shouldAssignNodePorts(service) oldNodePorts := CollectServiceNodePorts(oldService) newNodePorts := []int{} if assignNodePorts { for i := range service.Spec.Ports { servicePort := &service.Spec.Ports[i] nodePort := int(servicePort.NodePort) if nodePort != 0 { if !contains(oldNodePorts, nodePort) { err := nodePortOp.Allocate(nodePort) if err != nil { el := field.ErrorList{field.Invalid(field.NewPath("spec", "ports").Index(i).Child("nodePort"), nodePort, err.Error())} return nil, false, errors.NewInvalid(api.Kind("Service"), service.Name, el) } } } else { nodePort, err = nodePortOp.AllocateNext() if err != nil { // TODO: what error should be returned here? It's not a // field-level validation failure (the field is valid), and it's // not really an internal error. return nil, false, errors.NewInternalError(fmt.Errorf("failed to allocate a nodePort: %v", err)) } servicePort.NodePort = int32(nodePort) } // Detect duplicate node ports; this should have been caught by validation, so we panic if contains(newNodePorts, nodePort) { panic("duplicate node port") } newNodePorts = append(newNodePorts, nodePort) } } else { // Validate should have validated that nodePort == 0 } // The comparison loops are O(N^2), but we don't expect N to be huge // (there's a hard-limit at 2^16, because they're ports; and even 4 ports would be a lot) for _, oldNodePort := range oldNodePorts { if !contains(newNodePorts, oldNodePort) { continue } nodePortOp.ReleaseDeferred(oldNodePort) } // Remove any LoadBalancerStatus now if Type != LoadBalancer; // although loadbalancer delete is actually asynchronous, we don't need to expose the user to that complexity. if service.Spec.Type != api.ServiceTypeLoadBalancer { service.Status.LoadBalancer = api.LoadBalancerStatus{} } out, err := rs.registry.UpdateService(ctx, service) if err == nil { el := nodePortOp.Commit() if el != nil { // problems should be fixed by an eventual reconciliation / restart glog.Errorf("error(s) committing NodePorts changes: %v", el) } } return out, false, err }
func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) { service := obj.(*api.Service) if err := rest.BeforeCreate(Strategy, ctx, obj); err != nil { return nil, err } // TODO: this should probably move to strategy.PrepareForCreate() releaseServiceIP := false defer func() { if releaseServiceIP { if api.IsServiceIPSet(service) { rs.serviceIPs.Release(net.ParseIP(service.Spec.ClusterIP)) } } }() nodePortOp := portallocator.StartOperation(rs.serviceNodePorts) defer nodePortOp.Finish() if api.IsServiceIPRequested(service) { // Allocate next available. ip, err := rs.serviceIPs.AllocateNext() if err != nil { // TODO: what error should be returned here? It's not a // field-level validation failure (the field is valid), and it's // not really an internal error. return nil, errors.NewInternalError(fmt.Errorf("failed to allocate a serviceIP: %v", err)) } service.Spec.ClusterIP = ip.String() releaseServiceIP = true } else if api.IsServiceIPSet(service) { // Try to respect the requested IP. if err := rs.serviceIPs.Allocate(net.ParseIP(service.Spec.ClusterIP)); 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 nil, errors.NewInvalid(api.Kind("Service"), service.Name, el) } releaseServiceIP = true } assignNodePorts := shouldAssignNodePorts(service) for i := range service.Spec.Ports { servicePort := &service.Spec.Ports[i] if servicePort.NodePort != 0 { err := nodePortOp.Allocate(int(servicePort.NodePort)) if err != nil { // TODO: when validation becomes versioned, this gets more complicated. el := field.ErrorList{field.Invalid(field.NewPath("spec", "ports").Index(i).Child("nodePort"), servicePort.NodePort, err.Error())} return nil, errors.NewInvalid(api.Kind("Service"), service.Name, el) } } else if assignNodePorts { nodePort, err := nodePortOp.AllocateNext() if err != nil { // TODO: what error should be returned here? It's not a // field-level validation failure (the field is valid), and it's // not really an internal error. return nil, errors.NewInternalError(fmt.Errorf("failed to allocate a nodePort: %v", err)) } servicePort.NodePort = int32(nodePort) } } out, err := rs.registry.CreateService(ctx, service) if err != nil { err = rest.CheckGeneratedNameError(Strategy, err, service) } if err == nil { el := nodePortOp.Commit() if el != nil { // these should be caught by an eventual reconciliation / restart glog.Errorf("error(s) committing service node-ports changes: %v", el) } releaseServiceIP = false } return out, err }
func TestNewInvalid(t *testing.T) { testCases := []struct { Err *field.Error Details *unversioned.StatusDetails }{ { field.Duplicate(field.NewPath("field[0].name"), "bar"), &unversioned.StatusDetails{ Kind: "Kind", Name: "name", Causes: []unversioned.StatusCause{{ Type: unversioned.CauseTypeFieldValueDuplicate, Field: "field[0].name", }}, }, }, { field.Invalid(field.NewPath("field[0].name"), "bar", "detail"), &unversioned.StatusDetails{ Kind: "Kind", Name: "name", Causes: []unversioned.StatusCause{{ Type: unversioned.CauseTypeFieldValueInvalid, Field: "field[0].name", }}, }, }, { field.NotFound(field.NewPath("field[0].name"), "bar"), &unversioned.StatusDetails{ Kind: "Kind", Name: "name", Causes: []unversioned.StatusCause{{ Type: unversioned.CauseTypeFieldValueNotFound, Field: "field[0].name", }}, }, }, { field.NotSupported(field.NewPath("field[0].name"), "bar", nil), &unversioned.StatusDetails{ Kind: "Kind", Name: "name", Causes: []unversioned.StatusCause{{ Type: unversioned.CauseTypeFieldValueNotSupported, Field: "field[0].name", }}, }, }, { field.Required(field.NewPath("field[0].name"), ""), &unversioned.StatusDetails{ Kind: "Kind", Name: "name", Causes: []unversioned.StatusCause{{ Type: unversioned.CauseTypeFieldValueRequired, Field: "field[0].name", }}, }, }, } for i, testCase := range testCases { vErr, expected := testCase.Err, testCase.Details expected.Causes[0].Message = vErr.ErrorBody() err := NewInvalid(api.Kind("Kind"), "name", field.ErrorList{vErr}) status := err.ErrStatus if status.Code != 422 || status.Reason != unversioned.StatusReasonInvalid { t.Errorf("%d: unexpected status: %#v", i, status) } if !reflect.DeepEqual(expected, status.Details) { t.Errorf("%d: expected %#v, got %#v", i, expected, status.Details) } } }