// Update performs an atomic update and set of the object. Returns the result of the update // or an error. If the registry allows create-on-update, the create flow will be executed. // A bool is returned along with the object and any errors, to indicate object creation. func (e *Store) Update(ctx genericapirequest.Context, name string, objInfo rest.UpdatedObjectInfo) (runtime.Object, bool, error) { key, err := e.KeyFunc(ctx, name) if err != nil { return nil, false, err } var ( creatingObj runtime.Object creating = false ) storagePreconditions := &storage.Preconditions{} if preconditions := objInfo.Preconditions(); preconditions != nil { storagePreconditions.UID = preconditions.UID } out := e.NewFunc() // deleteObj is only used in case a deletion is carried out var deleteObj runtime.Object err = e.Storage.GuaranteedUpdate(ctx, key, out, true, storagePreconditions, func(existing runtime.Object, res storage.ResponseMeta) (runtime.Object, *uint64, error) { // Given the existing object, get the new object obj, err := objInfo.UpdatedObject(ctx, existing) if err != nil { return nil, nil, err } // If AllowUnconditionalUpdate() is true and the object specified by the user does not have a resource version, // then we populate it with the latest version. // Else, we check that the version specified by the user matches the version of latest storage object. resourceVersion, err := e.Storage.Versioner().ObjectResourceVersion(obj) if err != nil { return nil, nil, err } doUnconditionalUpdate := resourceVersion == 0 && e.UpdateStrategy.AllowUnconditionalUpdate() version, err := e.Storage.Versioner().ObjectResourceVersion(existing) if err != nil { return nil, nil, err } if version == 0 { if !e.UpdateStrategy.AllowCreateOnUpdate() { return nil, nil, kubeerr.NewNotFound(e.QualifiedResource, name) } creating = true creatingObj = obj if err := rest.BeforeCreate(e.CreateStrategy, ctx, obj); err != nil { return nil, nil, err } ttl, err := e.calculateTTL(obj, 0, false) if err != nil { return nil, nil, err } return obj, &ttl, nil } creating = false creatingObj = nil if doUnconditionalUpdate { // Update the object's resource version to match the latest storage object's resource version. err = e.Storage.Versioner().UpdateObject(obj, res.ResourceVersion) if err != nil { return nil, nil, err } } else { // Check if the object's resource version matches the latest resource version. newVersion, err := e.Storage.Versioner().ObjectResourceVersion(obj) if err != nil { return nil, nil, err } if newVersion == 0 { // TODO: The Invalid error should has a field for Resource. // After that field is added, we should fill the Resource and // leave the Kind field empty. See the discussion in #18526. qualifiedKind := schema.GroupKind{Group: e.QualifiedResource.Group, Kind: e.QualifiedResource.Resource} fieldErrList := field.ErrorList{field.Invalid(field.NewPath("metadata").Child("resourceVersion"), newVersion, "must be specified for an update")} return nil, nil, kubeerr.NewInvalid(qualifiedKind, name, fieldErrList) } if newVersion != version { return nil, nil, kubeerr.NewConflict(e.QualifiedResource, name, fmt.Errorf(OptimisticLockErrorMsg)) } } if err := rest.BeforeUpdate(e.UpdateStrategy, ctx, obj, existing); err != nil { return nil, nil, err } delete := e.shouldDelete(ctx, key, obj, existing) if delete { deleteObj = obj return nil, nil, errEmptiedFinalizers } ttl, err := e.calculateTTL(obj, res.TTL, true) if err != nil { return nil, nil, err } if int64(ttl) != res.TTL { return obj, &ttl, nil } return obj, nil, nil }) if err != nil { // delete the object if err == errEmptiedFinalizers { return e.deleteForEmptyFinalizers(ctx, name, key, deleteObj, storagePreconditions) } if creating { err = storeerr.InterpretCreateError(err, e.QualifiedResource, name) err = rest.CheckGeneratedNameError(e.CreateStrategy, err, creatingObj) } else { err = storeerr.InterpretUpdateError(err, e.QualifiedResource, name) } return nil, false, err } if creating { if e.AfterCreate != nil { if err := e.AfterCreate(out); err != nil { return nil, false, err } } } else { if e.AfterUpdate != nil { if err := e.AfterUpdate(out); err != nil { return nil, false, err } } } if e.Decorator != nil { if err := e.Decorator(out); err != nil { return nil, false, err } } return out, creating, nil }
// 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.ClusterIP = "1.2.3.4" newSvc.Spec.ClusterIP = "4.3.2.1" }, expectErr: true, }, { name: "change selectpor", tweakSvc: func(oldSvc, newSvc *api.Service) { newSvc.Spec.Selector = map[string]string{"newkey": "newvalue"} }, expectErr: false, }, } 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) } } }