dry-run: Use dry-runnable structure

Kubernetes-commit: 68937c4934013739a1efc1b051820667d6d6349d
This commit is contained in:
Antoine Pelisse 2018-06-21 08:49:13 -07:00 committed by Kubernetes Publisher
parent 3e8b2477c1
commit dbac430f68
6 changed files with 485 additions and 15 deletions

View File

@ -0,0 +1,134 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/storage"
)
type DryRunnableStorage struct {
Storage storage.Interface
Codec runtime.Codec
}
func (s *DryRunnableStorage) Versioner() storage.Versioner {
return s.Storage.Versioner()
}
func (s *DryRunnableStorage) Create(ctx context.Context, key string, obj, out runtime.Object, ttl uint64, dryRun bool) error {
if dryRun {
if err := s.Storage.Get(ctx, key, "", out, false); err == nil {
return storage.NewKeyExistsError(key, 0)
}
s.copyInto(obj, out)
return nil
}
return s.Storage.Create(ctx, key, obj, out, ttl)
}
func (s *DryRunnableStorage) Delete(ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions, dryRun bool) error {
if dryRun {
if err := s.Storage.Get(ctx, key, "", out, false); err != nil {
return err
}
return checkPreconditions(key, preconditions, out)
}
return s.Storage.Delete(ctx, key, out, preconditions)
}
func (s *DryRunnableStorage) Watch(ctx context.Context, key string, resourceVersion string, p storage.SelectionPredicate) (watch.Interface, error) {
return s.Storage.Watch(ctx, key, resourceVersion, p)
}
func (s *DryRunnableStorage) WatchList(ctx context.Context, key string, resourceVersion string, p storage.SelectionPredicate) (watch.Interface, error) {
return s.Storage.WatchList(ctx, key, resourceVersion, p)
}
func (s *DryRunnableStorage) Get(ctx context.Context, key string, resourceVersion string, objPtr runtime.Object, ignoreNotFound bool) error {
return s.Storage.Get(ctx, key, resourceVersion, objPtr, ignoreNotFound)
}
func (s *DryRunnableStorage) GetToList(ctx context.Context, key string, resourceVersion string, p storage.SelectionPredicate, listObj runtime.Object) error {
return s.Storage.GetToList(ctx, key, resourceVersion, p, listObj)
}
func (s *DryRunnableStorage) List(ctx context.Context, key string, resourceVersion string, p storage.SelectionPredicate, listObj runtime.Object) error {
return s.Storage.List(ctx, key, resourceVersion, p, listObj)
}
func (s *DryRunnableStorage) GuaranteedUpdate(
ctx context.Context, key string, ptrToType runtime.Object, ignoreNotFound bool,
preconditions *storage.Preconditions, tryUpdate storage.UpdateFunc, dryRun bool, suggestion ...runtime.Object) error {
if dryRun {
err := s.Storage.Get(ctx, key, "", ptrToType, ignoreNotFound)
if err != nil {
return err
}
err = checkPreconditions(key, preconditions, ptrToType)
if err != nil {
return err
}
rev, err := s.Versioner().ObjectResourceVersion(ptrToType)
out, _, err := tryUpdate(ptrToType, storage.ResponseMeta{ResourceVersion: rev})
if err != nil {
return err
}
s.copyInto(out, ptrToType)
return nil
}
return s.Storage.GuaranteedUpdate(ctx, key, ptrToType, ignoreNotFound, preconditions, tryUpdate, suggestion...)
}
func (s *DryRunnableStorage) Count(key string) (int64, error) {
return s.Storage.Count(key)
}
func checkPreconditions(key string, preconditions *storage.Preconditions, obj runtime.Object) error {
if preconditions == nil {
return nil
}
objMeta, err := meta.Accessor(obj)
if err != nil {
return storage.NewInternalErrorf("can't enforce preconditions %v on un-introspectable object %v, got error: %v", *preconditions, obj, err)
}
if preconditions.UID != nil && *preconditions.UID != objMeta.GetUID() {
errMsg := fmt.Sprintf("Precondition failed: UID in precondition: %v, UID in object meta: %v", *preconditions.UID, objMeta.GetUID())
return storage.NewInvalidObjError(key, errMsg)
}
return nil
}
func (s *DryRunnableStorage) copyInto(in, out runtime.Object) error {
var data []byte
data, err := runtime.Encode(s.Codec, in)
if err != nil {
return err
}
_, _, err = s.Codec.Decode(data, nil, out)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,304 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/api/apitesting"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
"k8s.io/apiserver/pkg/storage"
etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing"
"k8s.io/apiserver/pkg/storage/storagebackend/factory"
)
func NewDryRunnableTestStorage(t *testing.T) (DryRunnableStorage, func()) {
server, sc := etcdtesting.NewUnsecuredEtcd3TestClientServer(t)
sc.Codec = apitesting.TestStorageCodec(codecs, examplev1.SchemeGroupVersion)
s, destroy, err := factory.Create(*sc)
if err != nil {
t.Fatalf("Error creating storage: %v", err)
}
return DryRunnableStorage{Storage: s, Codec: sc.Codec}, func() {
destroy()
server.Terminate(t)
}
}
func UnstructuredOrDie(j string) *unstructured.Unstructured {
m := map[string]interface{}{}
err := json.Unmarshal([]byte(j), &m)
if err != nil {
panic(fmt.Errorf("Failed to unmarshal into Unstructured: %v", err))
}
return &unstructured.Unstructured{Object: m}
}
func TestDryRunCreateDoesntCreate(t *testing.T) {
s, destroy := NewDryRunnableTestStorage(t)
defer destroy()
obj := UnstructuredOrDie(`{"kind": "Pod"}`)
out := UnstructuredOrDie(`{}`)
err := s.Create(context.Background(), "key", obj, out, 0, true)
if err != nil {
t.Fatalf("Failed to create new dry-run object: %v", err)
}
err = s.Get(context.Background(), "key", "", out, false)
if e, ok := err.(*storage.StorageError); !ok || e.Code != storage.ErrCodeKeyNotFound {
t.Errorf("Expected key to be not found, error: %v", err)
}
}
func TestDryRunCreateReturnsObject(t *testing.T) {
s, destroy := NewDryRunnableTestStorage(t)
defer destroy()
obj := UnstructuredOrDie(`{"kind": "Pod"}`)
out := UnstructuredOrDie(`{}`)
err := s.Create(context.Background(), "key", obj, out, 0, true)
if err != nil {
t.Fatalf("Failed to create new dry-run object: %v", err)
}
if !reflect.DeepEqual(obj, out) {
t.Errorf("Returned object different from input object:\nExpected: %v\nGot: %v", obj, out)
}
}
func TestDryRunCreateExistingObjectFails(t *testing.T) {
s, destroy := NewDryRunnableTestStorage(t)
defer destroy()
obj := UnstructuredOrDie(`{"kind": "Pod"}`)
out := UnstructuredOrDie(`{}`)
err := s.Create(context.Background(), "key", obj, out, 0, false)
if err != nil {
t.Fatalf("Failed to create new object: %v", err)
}
err = s.Create(context.Background(), "key", obj, out, 0, true)
if e, ok := err.(*storage.StorageError); !ok || e.Code != storage.ErrCodeKeyExists {
t.Errorf("Expected KeyExists error: %v", err)
}
}
func TestDryRunUpdateMissingObjectFails(t *testing.T) {
s, destroy := NewDryRunnableTestStorage(t)
defer destroy()
obj := UnstructuredOrDie(`{"kind": "Pod"}`)
updateFunc := func(input runtime.Object, res storage.ResponseMeta) (output runtime.Object, ttl *uint64, err error) {
return input, nil, errors.New("UpdateFunction shouldn't be called")
}
err := s.GuaranteedUpdate(context.Background(), "key", obj, false, nil, updateFunc, true)
if e, ok := err.(*storage.StorageError); !ok || e.Code != storage.ErrCodeKeyNotFound {
t.Errorf("Expected key to be not found, error: %v", err)
}
}
func TestDryRunUpdatePreconditions(t *testing.T) {
s, destroy := NewDryRunnableTestStorage(t)
defer destroy()
obj := UnstructuredOrDie(`{"kind": "Pod", "metadata": {"uid": "my-uid"}}`)
out := UnstructuredOrDie(`{}`)
err := s.Create(context.Background(), "key", obj, out, 0, false)
if err != nil {
t.Fatalf("Failed to create new object: %v", err)
}
updateFunc := func(input runtime.Object, res storage.ResponseMeta) (output runtime.Object, ttl *uint64, err error) {
u, ok := input.(*unstructured.Unstructured)
if !ok {
return input, nil, errors.New("Input object is not unstructured")
}
unstructured.SetNestedField(u.Object, "value", "field")
return u, nil, nil
}
wrongID := types.UID("wrong-uid")
myID := types.UID("my-uid")
err = s.GuaranteedUpdate(context.Background(), "key", obj, false, &storage.Preconditions{UID: &wrongID}, updateFunc, true)
if e, ok := err.(*storage.StorageError); !ok || e.Code != storage.ErrCodeInvalidObj {
t.Errorf("Expected invalid object, error: %v", err)
}
err = s.GuaranteedUpdate(context.Background(), "key", obj, false, &storage.Preconditions{UID: &myID}, updateFunc, true)
if err != nil {
t.Fatalf("Failed to update with valid precondition: %v", err)
}
}
func TestDryRunUpdateDoesntUpdate(t *testing.T) {
s, destroy := NewDryRunnableTestStorage(t)
defer destroy()
obj := UnstructuredOrDie(`{"kind": "Pod"}`)
created := UnstructuredOrDie(`{}`)
err := s.Create(context.Background(), "key", obj, created, 0, false)
if err != nil {
t.Fatalf("Failed to create new object: %v", err)
}
updateFunc := func(input runtime.Object, res storage.ResponseMeta) (output runtime.Object, ttl *uint64, err error) {
u, ok := input.(*unstructured.Unstructured)
if !ok {
return input, nil, errors.New("Input object is not unstructured")
}
unstructured.SetNestedField(u.Object, "value", "field")
return u, nil, nil
}
err = s.GuaranteedUpdate(context.Background(), "key", obj, false, nil, updateFunc, true)
if err != nil {
t.Fatalf("Failed to dry-run update: %v", err)
}
out := UnstructuredOrDie(`{}`)
err = s.Get(context.Background(), "key", "", out, false)
if !reflect.DeepEqual(created, out) {
t.Fatalf("Returned object %q different from expected %q", created, out)
}
}
func TestDryRunUpdateReturnsObject(t *testing.T) {
s, destroy := NewDryRunnableTestStorage(t)
defer destroy()
obj := UnstructuredOrDie(`{"kind": "Pod"}`)
out := UnstructuredOrDie(`{}`)
err := s.Create(context.Background(), "key", obj, out, 0, false)
if err != nil {
t.Fatalf("Failed to create new object: %v", err)
}
updateFunc := func(input runtime.Object, res storage.ResponseMeta) (output runtime.Object, ttl *uint64, err error) {
u, ok := input.(*unstructured.Unstructured)
if !ok {
return input, nil, errors.New("Input object is not unstructured")
}
unstructured.SetNestedField(u.Object, "value", "field")
return u, nil, nil
}
err = s.GuaranteedUpdate(context.Background(), "key", obj, false, nil, updateFunc, true)
if err != nil {
t.Fatalf("Failed to dry-run update: %v", err)
}
out = UnstructuredOrDie(`{"field": "value", "kind": "Pod", "metadata": {"resourceVersion": "2", "selfLink": ""}}`)
if !reflect.DeepEqual(obj, out) {
t.Fatalf("Returned object %#v different from expected %#v", obj, out)
}
}
func TestDryRunDeleteDoesntDelete(t *testing.T) {
s, destroy := NewDryRunnableTestStorage(t)
defer destroy()
obj := UnstructuredOrDie(`{"kind": "Pod"}`)
out := UnstructuredOrDie(`{}`)
err := s.Create(context.Background(), "key", obj, out, 0, false)
if err != nil {
t.Fatalf("Failed to create new object: %v", err)
}
err = s.Delete(context.Background(), "key", out, nil, true)
if err != nil {
t.Fatalf("Failed to dry-run delete the object: %v", err)
}
err = s.Get(context.Background(), "key", "", out, false)
if err != nil {
t.Fatalf("Failed to retrieve dry-run deleted object: %v", err)
}
}
func TestDryRunDeleteMissingObjectFails(t *testing.T) {
s, destroy := NewDryRunnableTestStorage(t)
defer destroy()
out := UnstructuredOrDie(`{}`)
err := s.Delete(context.Background(), "key", out, nil, true)
if e, ok := err.(*storage.StorageError); !ok || e.Code != storage.ErrCodeKeyNotFound {
t.Errorf("Expected key to be not found, error: %v", err)
}
}
func TestDryRunDeleteReturnsObject(t *testing.T) {
s, destroy := NewDryRunnableTestStorage(t)
defer destroy()
obj := UnstructuredOrDie(`{"kind": "Pod"}`)
out := UnstructuredOrDie(`{}`)
err := s.Create(context.Background(), "key", obj, out, 0, false)
if err != nil {
t.Fatalf("Failed to create new object: %v", err)
}
out = UnstructuredOrDie(`{}`)
expected := UnstructuredOrDie(`{"kind": "Pod", "metadata": {"resourceVersion": "2", "selfLink": ""}}`)
err = s.Delete(context.Background(), "key", out, nil, true)
if err != nil {
t.Fatalf("Failed to delete with valid precondition: %v", err)
}
if !reflect.DeepEqual(expected, out) {
t.Fatalf("Returned object %q doesn't match expected: %q", out, expected)
}
}
func TestDryRunDeletePreconditions(t *testing.T) {
s, destroy := NewDryRunnableTestStorage(t)
defer destroy()
obj := UnstructuredOrDie(`{"kind": "Pod", "metadata": {"uid": "my-uid"}}`)
out := UnstructuredOrDie(`{}`)
err := s.Create(context.Background(), "key", obj, out, 0, false)
if err != nil {
t.Fatalf("Failed to create new object: %v", err)
}
wrongID := types.UID("wrong-uid")
myID := types.UID("my-uid")
err = s.Delete(context.Background(), "key", out, &storage.Preconditions{UID: &wrongID}, true)
if e, ok := err.(*storage.StorageError); !ok || e.Code != storage.ErrCodeInvalidObj {
t.Errorf("Expected invalid object, error: %v", err)
}
err = s.Delete(context.Background(), "key", out, &storage.Preconditions{UID: &myID}, true)
if err != nil {
t.Fatalf("Failed to delete with valid precondition: %v", err)
}
}

View File

@ -45,6 +45,7 @@ import (
"k8s.io/apiserver/pkg/storage"
storeerr "k8s.io/apiserver/pkg/storage/errors"
"k8s.io/apiserver/pkg/storage/etcd/metrics"
"k8s.io/apiserver/pkg/util/dryrun"
"github.com/golang/glog"
)
@ -172,8 +173,10 @@ type Store struct {
// of items into tabular output. If unset, the default will be used.
TableConvertor rest.TableConvertor
// Storage is the interface for the underlying storage for the resource.
Storage storage.Interface
// Storage is the interface for the underlying storage for the
// resource. It is wrapped into a "DryRunnableStorage" that will
// either pass-through or simply dry-run.
Storage DryRunnableStorage
// Called to cleanup clients used by the underlying Storage; optional.
DestroyFunc func()
}
@ -348,7 +351,7 @@ func (e *Store) Create(ctx context.Context, obj runtime.Object, createValidation
return nil, err
}
out := e.NewFunc()
if err := e.Storage.Create(ctx, key, obj, out, ttl); err != nil {
if err := e.Storage.Create(ctx, key, obj, out, ttl, dryrun.IsDryRun(options.DryRun)); err != nil {
err = storeerr.InterpretCreateError(err, qualifiedResource, name)
err = rest.CheckGeneratedNameError(e.CreateStrategy, err, obj)
if !kubeerr.IsAlreadyExists(err) {
@ -496,10 +499,10 @@ func (e *Store) shouldDeleteForFailedInitialization(ctx context.Context, obj run
// deleteWithoutFinalizers handles deleting an object ignoring its finalizer list.
// Used for objects that are either been finalized or have never initialized.
func (e *Store) deleteWithoutFinalizers(ctx context.Context, name, key string, obj runtime.Object, preconditions *storage.Preconditions) (runtime.Object, bool, error) {
func (e *Store) deleteWithoutFinalizers(ctx context.Context, name, key string, obj runtime.Object, preconditions *storage.Preconditions, dryRun bool) (runtime.Object, bool, error) {
out := e.NewFunc()
glog.V(6).Infof("going to delete %s from registry, triggered by update", name)
if err := e.Storage.Delete(ctx, key, out, preconditions); err != nil {
if err := e.Storage.Delete(ctx, key, out, preconditions, dryRun); err != nil {
// Deletion is racy, i.e., there could be multiple update
// requests to remove all finalizers from the object, so we
// ignore the NotFound error.
@ -633,12 +636,12 @@ func (e *Store) Update(ctx context.Context, name string, objInfo rest.UpdatedObj
return obj, &ttl, nil
}
return obj, nil, nil
})
}, dryrun.IsDryRun(options.DryRun))
if err != nil {
// delete the object
if err == errEmptiedFinalizers {
return e.deleteWithoutFinalizers(ctx, name, key, deleteObj, storagePreconditions)
return e.deleteWithoutFinalizers(ctx, name, key, deleteObj, storagePreconditions, dryrun.IsDryRun(options.DryRun))
}
if creating {
err = storeerr.InterpretCreateError(err, qualifiedResource, name)
@ -650,7 +653,7 @@ func (e *Store) Update(ctx context.Context, name string, objInfo rest.UpdatedObj
}
if e.shouldDeleteForFailedInitialization(ctx, out) {
return e.deleteWithoutFinalizers(ctx, name, key, out, storagePreconditions)
return e.deleteWithoutFinalizers(ctx, name, key, out, storagePreconditions, dryrun.IsDryRun(options.DryRun))
}
if creating {
@ -919,6 +922,7 @@ func (e *Store) updateForGracefulDeletionAndFinalizers(ctx context.Context, name
lastExisting = existing
return existing, nil
}),
dryrun.IsDryRun(options.DryRun),
)
switch err {
case nil:
@ -1000,10 +1004,15 @@ func (e *Store) Delete(ctx context.Context, name string, options *metav1.DeleteO
return out, false, err
}
// If dry-run, then just return the object as just saved.
if dryrun.IsDryRun(options.DryRun) {
return out, true, nil
}
// delete immediately, or no graceful deletion supported
glog.V(6).Infof("going to delete %s from registry: ", name)
out = e.NewFunc()
if err := e.Storage.Delete(ctx, key, out, &preconditions); err != nil {
if err := e.Storage.Delete(ctx, key, out, &preconditions, dryrun.IsDryRun(options.DryRun)); err != nil {
// Please refer to the place where we set ignoreNotFound for the reason
// why we ignore the NotFound error .
if storage.IsNotFound(err) && ignoreNotFound && lastExisting != nil {
@ -1364,8 +1373,9 @@ func (e *Store) CompleteWithOptions(options *generic.StoreOptions) error {
}
}
if e.Storage == nil {
e.Storage, e.DestroyFunc = opts.Decorator(
if e.Storage.Storage == nil {
e.Storage.Codec = opts.StorageConfig.Codec
e.Storage.Storage, e.DestroyFunc = opts.Decorator(
opts.StorageConfig,
e.NewFunc(),
prefix,

View File

@ -213,7 +213,7 @@ func TestStoreList(t *testing.T) {
destroyFunc, registry := NewTestGenericStoreRegistry(t)
if item.in != nil {
if err := storagetesting.CreateList("/pods", registry.Storage, item.in); err != nil {
if err := storagetesting.CreateList("/pods", registry.Storage.Storage, item.in); err != nil {
t.Errorf("Unexpected error %v", err)
}
}
@ -1901,7 +1901,7 @@ func newTestGenericStoreRegistry(t *testing.T, scheme *runtime.Scheme, hasCacheE
},
}
},
Storage: s,
Storage: DryRunnableStorage{Storage: s},
}
}

View File

@ -164,7 +164,7 @@ func (t *Tester) createObject(ctx context.Context, obj runtime.Object) error {
if err != nil {
return err
}
return t.storage.Storage.Create(ctx, key, obj, nil, 0)
return t.storage.Storage.Create(ctx, key, obj, nil, 0, false)
}
func (t *Tester) setObjectsForList(objects []runtime.Object) []runtime.Object {
@ -173,7 +173,7 @@ func (t *Tester) setObjectsForList(objects []runtime.Object) []runtime.Object {
t.tester.Errorf("unable to clear collection: %v", err)
return nil
}
if err := storagetesting.CreateObjList(key, t.storage.Storage, objects); err != nil {
if err := storagetesting.CreateObjList(key, t.storage.Storage.Storage, objects); err != nil {
t.tester.Errorf("unexpected error: %v", err)
return nil
}

22
pkg/util/dryrun/dryrun.go Normal file
View File

@ -0,0 +1,22 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dryrun
// IsDryRun returns true if the DryRun flag is an actual dry-run.
func IsDryRun(flag []string) bool {
return len(flag) > 0
}