Merge pull request #78713 from liggitt/crd-status-conversion-error

Set expected in-memory version when decoding unstructured objects from etcd
This commit is contained in:
Kubernetes Prow Robot 2019-06-06 10:48:13 -07:00 committed by GitHub
commit f3f2b4066b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 299 additions and 41 deletions

View File

@ -235,6 +235,8 @@ func testPrinter(t *testing.T, printer printers.ResourcePrinter, unmarshalFunc f
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{Name: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"},
} }
// our decoder defaults, so we should default our expected object as well
legacyscheme.Scheme.Default(obj)
buf.Reset() buf.Reset()
printer.PrintObj(obj, buf) printer.PrintObj(obj, buf)
var objOut v1.Pod var objOut v1.Pod

View File

@ -177,12 +177,19 @@ type StatusREST struct {
var _ = rest.Patcher(&StatusREST{}) var _ = rest.Patcher(&StatusREST{})
func (r *StatusREST) New() runtime.Object { func (r *StatusREST) New() runtime.Object {
return &unstructured.Unstructured{} return r.store.New()
} }
// Get retrieves the object from the storage. It is required to support Patch. // Get retrieves the object from the storage. It is required to support Patch.
func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return r.store.Get(ctx, name, options) o, err := r.store.Get(ctx, name, options)
if err != nil {
return nil, err
}
if u, ok := o.(*unstructured.Unstructured); ok {
shallowCopyObjectMeta(u)
}
return o, nil
} }
// Update alters the status subset of an object. // Update alters the status subset of an object.

View File

@ -58,15 +58,18 @@ func checks(checkers ...Checker) []Checker {
return checkers return checkers
} }
func TestWebhookConverter(t *testing.T) { func TestWebhookConverterWithWatchCache(t *testing.T) {
testWebhookConverter(t, false) testWebhookConverter(t, false, true)
}
func TestWebhookConverterWithoutWatchCache(t *testing.T) {
testWebhookConverter(t, false, false)
} }
func TestWebhookConverterWithDefaulting(t *testing.T) { func TestWebhookConverterWithDefaulting(t *testing.T) {
testWebhookConverter(t, true) testWebhookConverter(t, true, true)
} }
func testWebhookConverter(t *testing.T, defaulting bool) { func testWebhookConverter(t *testing.T, defaulting, watchCache bool) {
tests := []struct { tests := []struct {
group string group string
handler http.Handler handler http.Handler
@ -115,7 +118,7 @@ func testWebhookConverter(t *testing.T, defaulting bool) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceDefaulting, true)() defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceDefaulting, true)()
} }
tearDown, config, options, err := fixtures.StartDefaultServer(t) tearDown, config, options, err := fixtures.StartDefaultServer(t, fmt.Sprintf("--watch-cache=%v", watchCache))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -320,6 +323,28 @@ func validateNonTrivialConverted(t *testing.T, ctc *conversionTestContext) {
} }
verifyMultiVersionObject(t, getVersion.Name, obj) verifyMultiVersionObject(t, getVersion.Name, obj)
} }
// send a non-trivial patch to the main resource to verify the oldObject is in the right version
if _, err := client.Patch(name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"main":"true"}}}`), metav1.PatchOptions{}); err != nil {
t.Fatal(err)
}
// verify that the right, pruned version is in storage
obj, err = ctc.etcdObjectReader.GetStoredCustomResource(ns, name)
if err != nil {
t.Fatal(err)
}
verifyMultiVersionObject(t, "v1beta1", obj)
// send a non-trivial patch to the status subresource to verify the oldObject is in the right version
if _, err := client.Patch(name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"status":"true"}}}`), metav1.PatchOptions{}, "status"); err != nil {
t.Fatal(err)
}
// verify that the right, pruned version is in storage
obj, err = ctc.etcdObjectReader.GetStoredCustomResource(ns, name)
if err != nil {
t.Fatal(err)
}
verifyMultiVersionObject(t, "v1beta1", obj)
}) })
} }
} }

View File

@ -17,6 +17,7 @@ limitations under the License.
package integration package integration
import ( import (
"fmt"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -29,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
utilfeaturetesting "k8s.io/component-base/featuregate/testing" utilfeaturetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/utils/pointer" "k8s.io/utils/pointer"
@ -43,6 +45,18 @@ var defaultingFixture = &apiextensionsv1beta1.CustomResourceDefinition{
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Group: "tests.apiextensions.k8s.io", Group: "tests.apiextensions.k8s.io",
Version: "v1beta1", Version: "v1beta1",
Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
{
Name: "v1beta1",
Storage: false,
Served: true,
},
{
Name: "v1beta2",
Storage: true,
Served: false,
},
},
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "foos", Plural: "foos",
Singular: "foo", Singular: "foo",
@ -57,7 +71,7 @@ var defaultingFixture = &apiextensionsv1beta1.CustomResourceDefinition{
}, },
} }
const defaultingFooSchema = ` const defaultingFooV1beta1Schema = `
type: object type: object
properties: properties:
spec: spec:
@ -69,6 +83,13 @@ properties:
b: b:
type: string type: string
default: "B" default: "B"
c:
type: string
v1beta1:
type: string
default: "v1beta1"
v1beta2:
type: string
status: status:
type: object type: object
properties: properties:
@ -78,20 +99,76 @@ properties:
b: b:
type: string type: string
default: "B" default: "B"
c:
type: string
v1beta1:
type: string
default: "v1beta1"
v1beta2:
type: string
` `
func TestCustomResourceDefaulting(t *testing.T) { const defaultingFooV1beta2Schema = `
type: object
properties:
spec:
type: object
properties:
a:
type: string
default: "A"
b:
type: string
default: "B"
c:
type: string
v1beta1:
type: string
v1beta2:
type: string
default: "v1beta2"
status:
type: object
properties:
a:
type: string
default: "A"
b:
type: string
default: "B"
c:
type: string
v1beta1:
type: string
v1beta2:
type: string
default: "v1beta2"
`
func TestCustomResourceDefaultingWithWatchCache(t *testing.T) {
testDefaulting(t, true)
}
func TestCustomResourceDefaultingWithoutWatchCache(t *testing.T) {
testDefaulting(t, false)
}
func testDefaulting(t *testing.T, watchCache bool) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceDefaulting, true)() defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceDefaulting, true)()
tearDownFn, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) tearDownFn, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t, fmt.Sprintf("--watch-cache=%v", watchCache))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer tearDownFn() defer tearDownFn()
crd := defaultingFixture.DeepCopy() crd := defaultingFixture.DeepCopy()
crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{} crd.Spec.Versions[0].Schema = &apiextensionsv1beta1.CustomResourceValidation{}
if err := yaml.Unmarshal([]byte(defaultingFooSchema), &crd.Spec.Validation.OpenAPIV3Schema); err != nil { if err := yaml.Unmarshal([]byte(defaultingFooV1beta1Schema), &crd.Spec.Versions[0].Schema.OpenAPIV3Schema); err != nil {
t.Fatal(err)
}
crd.Spec.Versions[1].Schema = &apiextensionsv1beta1.CustomResourceValidation{}
if err := yaml.Unmarshal([]byte(defaultingFooV1beta2Schema), &crd.Spec.Versions[1].Schema.OpenAPIV3Schema); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -101,13 +178,15 @@ func TestCustomResourceDefaulting(t *testing.T) {
} }
mustExist := func(obj map[string]interface{}, pths [][]string) { mustExist := func(obj map[string]interface{}, pths [][]string) {
t.Helper()
for _, pth := range pths { for _, pth := range pths {
if _, found, _ := unstructured.NestedFieldNoCopy(obj, pth...); !found { if _, found, _ := unstructured.NestedFieldNoCopy(obj, pth...); !found {
t.Errorf("Expected '%s' field exist", strings.Join(pth, ".")) t.Errorf("Expected '%s' field was missing", strings.Join(pth, "."))
} }
} }
} }
mustNotExist := func(obj map[string]interface{}, pths [][]string) { mustNotExist := func(obj map[string]interface{}, pths [][]string) {
t.Helper()
for _, pth := range pths { for _, pth := range pths {
if fld, found, _ := unstructured.NestedFieldNoCopy(obj, pth...); found { if fld, found, _ := unstructured.NestedFieldNoCopy(obj, pth...); found {
t.Errorf("Expected '%s' field to not exist, but it does: %v", strings.Join(pth, "."), fld) t.Errorf("Expected '%s' field to not exist, but it does: %v", strings.Join(pth, "."), fld)
@ -115,6 +194,7 @@ func TestCustomResourceDefaulting(t *testing.T) {
} }
} }
updateCRD := func(update func(*apiextensionsv1beta1.CustomResourceDefinition)) { updateCRD := func(update func(*apiextensionsv1beta1.CustomResourceDefinition)) {
t.Helper()
var err error var err error
for retry := 0; retry < 10; retry++ { for retry := 0; retry < 10; retry++ {
var obj *apiextensionsv1beta1.CustomResourceDefinition var obj *apiextensionsv1beta1.CustomResourceDefinition
@ -136,22 +216,34 @@ func TestCustomResourceDefaulting(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
addDefault := func(key string, value interface{}) { addDefault := func(version string, key string, value interface{}) {
t.Helper()
updateCRD(func(obj *apiextensionsv1beta1.CustomResourceDefinition) { updateCRD(func(obj *apiextensionsv1beta1.CustomResourceDefinition) {
for _, root := range []string{"spec", "status"} { for _, root := range []string{"spec", "status"} {
obj.Spec.Validation.OpenAPIV3Schema.Properties[root].Properties[key] = apiextensionsv1beta1.JSONSchemaProps{ for i := range obj.Spec.Versions {
Type: "string", if obj.Spec.Versions[i].Name != version {
Default: jsonPtr(value), continue
}
obj.Spec.Versions[i].Schema.OpenAPIV3Schema.Properties[root].Properties[key] = apiextensionsv1beta1.JSONSchemaProps{
Type: "string",
Default: jsonPtr(value),
}
} }
} }
}) })
} }
removeDefault := func(key string) { removeDefault := func(version string, key string) {
t.Helper()
updateCRD(func(obj *apiextensionsv1beta1.CustomResourceDefinition) { updateCRD(func(obj *apiextensionsv1beta1.CustomResourceDefinition) {
for _, root := range []string{"spec", "status"} { for _, root := range []string{"spec", "status"} {
props := obj.Spec.Validation.OpenAPIV3Schema.Properties[root].Properties[key] for i := range obj.Spec.Versions {
props.Default = nil if obj.Spec.Versions[i].Name != version {
obj.Spec.Validation.OpenAPIV3Schema.Properties[root].Properties[key] = props continue
}
props := obj.Spec.Versions[i].Schema.OpenAPIV3Schema.Properties[root].Properties[key]
props.Default = nil
obj.Spec.Versions[i].Schema.OpenAPIV3Schema.Properties[root].Properties[key] = props
}
} }
}) })
} }
@ -168,8 +260,12 @@ func TestCustomResourceDefaulting(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Unable to create CR: %v", err) t.Fatalf("Unable to create CR: %v", err)
} }
initialResourceVersion := foo.GetResourceVersion()
t.Logf("CR created: %#v", foo.UnstructuredContent()) t.Logf("CR created: %#v", foo.UnstructuredContent())
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}}) // spec.a and spec.b are defaulted in both versions
// spec.v1beta1 is defaulted when reading the incoming request
// spec.v1beta2 is defaulted when reading the storage response
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"spec", "v1beta1"}, {"spec", "v1beta2"}})
mustNotExist(foo.Object, [][]string{{"status"}}) mustNotExist(foo.Object, [][]string{{"status"}})
t.Logf("Updating status and expecting 'a' and 'b' to show up.") t.Logf("Updating status and expecting 'a' and 'b' to show up.")
@ -179,8 +275,90 @@ func TestCustomResourceDefaulting(t *testing.T) {
} }
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"status", "a"}, {"status", "b"}}) mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"status", "a"}, {"status", "b"}})
t.Logf("Add 'c' default and wait until GET sees it in both status and spec") t.Logf("Add 'c' default to the storage version and wait until GET sees it in both status and spec")
addDefault("c", "C") addDefault("v1beta2", "c", "C")
t.Logf("wait until GET sees 'c' in both status and spec")
if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
obj, err := fooClient.Get(foo.GetName(), metav1.GetOptions{})
if err != nil {
return false, err
}
if _, found, _ := unstructured.NestedString(obj.Object, "spec", "c"); !found {
t.Log("will retry, did not find spec.c in the object")
return false, nil
}
foo = obj
return true, nil
}); err != nil {
t.Fatal(err)
}
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"spec", "c"}, {"status", "a"}, {"status", "b"}, {"status", "c"}})
t.Logf("wait until GET sees 'c' in both status and spec of cached get")
if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
obj, err := fooClient.Get(foo.GetName(), metav1.GetOptions{ResourceVersion: "0"})
if err != nil {
return false, err
}
if _, found, _ := unstructured.NestedString(obj.Object, "spec", "c"); !found {
t.Logf("will retry, did not find spec.c in the cached object")
return false, nil
}
foo = obj
return true, nil
}); err != nil {
t.Fatal(err)
}
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"spec", "c"}, {"status", "a"}, {"status", "b"}, {"status", "c"}})
t.Logf("verify LIST sees 'c' in both status and spec")
foos, err := fooClient.List(metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
for _, foo := range foos.Items {
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"spec", "c"}, {"status", "a"}, {"status", "b"}, {"status", "c"}})
}
t.Logf("verify LIST from cache sees 'c' in both status and spec")
foos, err = fooClient.List(metav1.ListOptions{ResourceVersion: "0"})
if err != nil {
t.Fatal(err)
}
for _, foo := range foos.Items {
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"spec", "c"}, {"status", "a"}, {"status", "b"}, {"status", "c"}})
}
// Omit this test when using the watch cache because changing the CRD resets the watch cache's minimum available resource version.
// The watch cache is populated by list and watch, which are both tested by this test.
// The contents of the watch cache are seen by list with rv=0, which is tested by this test.
if !watchCache {
t.Logf("verify WATCH sees 'c' in both status and spec")
w, err := fooClient.Watch(metav1.ListOptions{ResourceVersion: initialResourceVersion})
if err != nil {
t.Fatal(err)
}
select {
case event := <-w.ResultChan():
if event.Type != watch.Modified {
t.Fatalf("unexpected watch event: %v, %#v", event.Type, event.Object)
}
if e, a := "v1beta1", event.Object.GetObjectKind().GroupVersionKind().Version; e != a {
t.Errorf("watch event for v1beta1 API returned %v", a)
}
mustExist(
event.Object.(*unstructured.Unstructured).Object,
[][]string{{"spec", "a"}, {"spec", "b"}, {"spec", "c"}, {"status", "a"}, {"status", "b"}, {"status", "c"}},
)
case <-time.After(wait.ForeverTestTimeout):
t.Fatal("timed out without getting watch event")
}
}
t.Logf("Add 'c' default to the REST version, remove it from the storage version, and wait until GET no longer sees it in both status and spec")
addDefault("v1beta1", "c", "C")
removeDefault("v1beta2", "c")
if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
obj, err := fooClient.Get(foo.GetName(), metav1.GetOptions{}) obj, err := fooClient.Get(foo.GetName(), metav1.GetOptions{})
if err != nil { if err != nil {
@ -188,22 +366,24 @@ func TestCustomResourceDefaulting(t *testing.T) {
} }
_, found, _ := unstructured.NestedString(obj.Object, "spec", "c") _, found, _ := unstructured.NestedString(obj.Object, "spec", "c")
foo = obj foo = obj
return found, nil return !found, nil
}); err != nil { }); err != nil {
t.Fatal(err) t.Fatal(err)
} }
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"spec", "c"}, {"status", "a"}, {"status", "b"}, {"status", "c"}}) mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"status", "a"}, {"status", "b"}})
mustNotExist(foo.Object, [][]string{{"spec", "c"}, {"status", "c"}})
t.Logf("Updating status, expecting 'c' to be set in spec and status") t.Logf("Updating status, expecting 'c' to be set in status only")
if foo, err = fooClient.UpdateStatus(foo, metav1.UpdateOptions{}); err != nil { if foo, err = fooClient.UpdateStatus(foo, metav1.UpdateOptions{}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"spec", "c"}, {"status", "a"}, {"status", "b"}, {"status", "c"}}) mustExist(foo.Object, [][]string{{"spec", "a"}, {"spec", "b"}, {"status", "a"}, {"status", "b"}, {"status", "c"}})
mustNotExist(foo.Object, [][]string{{"spec", "c"}})
t.Logf("Removing 'a', 'b' and `c` properties. Expecting that 'c' goes away in spec, but not in status. 'a' and 'b' were peristed.") t.Logf("Removing 'a', 'b' and `c` properties from the REST version. Expecting that 'c' goes away in spec, but not in status. 'a' and 'b' were presisted.")
removeDefault("a") removeDefault("v1beta1", "a")
removeDefault("b") removeDefault("v1beta1", "b")
removeDefault("c") removeDefault("v1beta1", "c")
if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
obj, err := fooClient.Get(foo.GetName(), metav1.GetOptions{}) obj, err := fooClient.Get(foo.GetName(), metav1.GetOptions{})
if err != nil { if err != nil {

View File

@ -127,6 +127,16 @@ func (u *Unstructured) UnmarshalJSON(b []byte) error {
return err return err
} }
// NewEmptyInstance returns a new instance of the concrete type containing only kind/apiVersion and no other data.
// This should be called instead of reflect.New() for unstructured types because the go type alone does not preserve kind/apiVersion info.
func (in *Unstructured) NewEmptyInstance() runtime.Unstructured {
out := new(Unstructured)
if in != nil {
out.GetObjectKind().SetGroupVersionKind(in.GetObjectKind().GroupVersionKind())
}
return out
}
func (in *Unstructured) DeepCopy() *Unstructured { func (in *Unstructured) DeepCopy() *Unstructured {
if in == nil { if in == nil {
return nil return nil

View File

@ -52,6 +52,16 @@ func (u *UnstructuredList) EachListItem(fn func(runtime.Object) error) error {
return nil return nil
} }
// NewEmptyInstance returns a new instance of the concrete type containing only kind/apiVersion and no other data.
// This should be called instead of reflect.New() for unstructured types because the go type alone does not preserve kind/apiVersion info.
func (u *UnstructuredList) NewEmptyInstance() runtime.Unstructured {
out := new(UnstructuredList)
if u != nil {
out.SetGroupVersionKind(u.GroupVersionKind())
}
return out
}
// UnstructuredContent returns a map contain an overlay of the Items field onto // UnstructuredContent returns a map contain an overlay of the Items field onto
// the Object field. Items always overwrites overlay. // the Object field. Items always overwrites overlay.
func (u *UnstructuredList) UnstructuredContent() map[string]interface{} { func (u *UnstructuredList) UnstructuredContent() map[string]interface{} {

View File

@ -260,6 +260,9 @@ type Object interface {
// to JSON allowed. // to JSON allowed.
type Unstructured interface { type Unstructured interface {
Object Object
// NewEmptyInstance returns a new instance of the concrete type containing only kind/apiVersion and no other data.
// This should be called instead of reflect.New() for unstructured types because the go type alone does not preserve kind/apiVersion info.
NewEmptyInstance() Unstructured
// UnstructuredContent returns a non-nil map with this object's contents. Values may be // UnstructuredContent returns a non-nil map with this object's contents. Values may be
// []interface{}, map[string]interface{}, or any primitive type. Contents are typically serialized to // []interface{}, map[string]interface{}, or any primitive type. Contents are typically serialized to
// and from JSON. SetUnstructuredContent should be used to mutate the contents. // and from JSON. SetUnstructuredContent should be used to mutate the contents.

View File

@ -113,13 +113,6 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru
// if we specify a target, use generic conversion. // if we specify a target, use generic conversion.
if into != nil { if into != nil {
if into == obj {
if isVersioned {
return versioned, gvk, nil
}
return into, gvk, nil
}
// perform defaulting if requested // perform defaulting if requested
if c.defaulter != nil { if c.defaulter != nil {
// create a copy to ensure defaulting is not applied to the original versioned objects // create a copy to ensure defaulting is not applied to the original versioned objects
@ -133,6 +126,14 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru
} }
} }
// Short-circuit conversion if the into object is same object
if into == obj {
if isVersioned {
return versioned, gvk, nil
}
return into, gvk, nil
}
if err := c.convertor.Convert(obj, into, c.decodeVersion); err != nil { if err := c.convertor.Convert(obj, into, c.decodeVersion); err != nil {
return nil, gvk, err return nil, gvk, err
} }

View File

@ -261,6 +261,14 @@ func (obj *Unstructured) EachListItem(fn func(runtime.Object) error) error {
return nil return nil
} }
func (obj *Unstructured) NewEmptyInstance() runtime.Unstructured {
out := new(Unstructured)
if obj != nil {
out.SetGroupVersionKind(obj.GroupVersionKind())
}
return out
}
func (obj *Unstructured) UnstructuredContent() map[string]interface{} { func (obj *Unstructured) UnstructuredContent() map[string]interface{} {
if obj.Object == nil { if obj.Object == nil {
return make(map[string]interface{}) return make(map[string]interface{})

View File

@ -20,6 +20,7 @@ go_test(
"//staging/src/k8s.io/apimachinery/pkg/api/apitesting:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/apitesting:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/conversion:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",

View File

@ -685,9 +685,15 @@ func (s *store) watch(ctx context.Context, key string, rv string, pred storage.S
func (s *store) getState(getResp *clientv3.GetResponse, key string, v reflect.Value, ignoreNotFound bool) (*objState, error) { func (s *store) getState(getResp *clientv3.GetResponse, key string, v reflect.Value, ignoreNotFound bool) (*objState, error) {
state := &objState{ state := &objState{
obj: reflect.New(v.Type()).Interface().(runtime.Object),
meta: &storage.ResponseMeta{}, meta: &storage.ResponseMeta{},
} }
if u, ok := v.Addr().Interface().(runtime.Unstructured); ok {
state.obj = u.NewEmptyInstance()
} else {
state.obj = reflect.New(v.Type()).Interface().(runtime.Object)
}
if len(getResp.Kvs) == 0 { if len(getResp.Kvs) == 0 {
if !ignoreNotFound { if !ignoreNotFound {
return nil, storage.NewKeyNotFoundError(key, 0) return nil, storage.NewKeyNotFoundError(key, 0)

View File

@ -35,6 +35,7 @@ import (
apitesting "k8s.io/apimachinery/pkg/api/apitesting" apitesting "k8s.io/apimachinery/pkg/api/apitesting"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "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/conversion"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -1349,7 +1350,11 @@ func testSetup(t *testing.T) (context.Context, *store, *integration.ClusterV3) {
func testPropogateStore(ctx context.Context, t *testing.T, store *store, obj *example.Pod) (string, *example.Pod) { func testPropogateStore(ctx context.Context, t *testing.T, store *store, obj *example.Pod) (string, *example.Pod) {
// Setup store with a key and grab the output for returning. // Setup store with a key and grab the output for returning.
key := "/testkey" key := "/testkey"
err := store.conditionalDelete(ctx, key, &example.Pod{}, reflect.ValueOf(example.Pod{}), nil, storage.ValidateAllObjectFunc) v, err := conversion.EnforcePtr(obj)
if err != nil {
panic("unable to convert output object to pointer")
}
err = store.conditionalDelete(ctx, key, &example.Pod{}, v, nil, storage.ValidateAllObjectFunc)
if err != nil && !storage.IsNotFound(err) { if err != nil && !storage.IsNotFound(err) {
t.Fatalf("Cleanup failed: %v", err) t.Fatalf("Cleanup failed: %v", err)
} }