From adaa391ddf42bf31c8e0257b3c638c6b4dadedc5 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Fri, 16 Jun 2023 14:24:57 -0700 Subject: [PATCH] Drop unrecognized fields before update Add a nested store to the proxy store to strip non-Kubernetes fields from the object being updated. The steve formatter and proxy store adds fields to objects when it outputs them to the client, for usability by the UI. It adds the object's fields[1], relationships to other objects[2], a summary of the object's state[3], and additional information in the conditions[4]. These fields are not native to Kubernetes, so when a client submits the object back as an update, Kubernetes reports a warning that they are unrecognized. This change ensures the extra fields are removed before submitting the update. [1] https://github.com/rancher/steve/blob/bf2e9655f5dde8f55b23f67e64f0186fc68789d7/pkg/stores/proxy/proxy_store.go#L189 [2] https://github.com/rancher/steve/blob/bf2e9655f5dde8f55b23f67e64f0186fc68789d7/pkg/resources/common/formatter.go#L106 [3] https://github.com/rancher/steve/blob/bf2e9655f5dde8f55b23f67e64f0186fc68789d7/pkg/resources/common/formatter.go#L100 [4] https://github.com/rancher/steve/blob/bf2e9655f5dde8f55b23f67e64f0186fc68789d7/pkg/resources/common/formatter.go#L108 --- pkg/stores/proxy/proxy_store.go | 24 +++--- pkg/stores/proxy/unformatter.go | 66 ++++++++++++++++ pkg/stores/proxy/unformatter_test.go | 112 +++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 pkg/stores/proxy/unformatter.go create mode 100644 pkg/stores/proxy/unformatter_test.go diff --git a/pkg/stores/proxy/proxy_store.go b/pkg/stores/proxy/proxy_store.go index 52b0cc76..fb01dd81 100644 --- a/pkg/stores/proxy/proxy_store.go +++ b/pkg/stores/proxy/proxy_store.go @@ -88,18 +88,20 @@ type Store struct { // NewProxyStore returns a wrapped types.Store. func NewProxyStore(clientGetter ClientGetter, notifier RelationshipNotifier, lookup accesscontrol.AccessSetLookup, namespaceCache corecontrollers.NamespaceCache) types.Store { return &errorStore{ - Store: &WatchRefresh{ - Store: partition.NewStore( - &rbacPartitioner{ - proxyStore: &Store{ - clientGetter: clientGetter, - notifier: notifier, + Store: &unformatterStore{ + Store: &WatchRefresh{ + Store: partition.NewStore( + &rbacPartitioner{ + proxyStore: &Store{ + clientGetter: clientGetter, + notifier: notifier, + }, }, - }, - lookup, - namespaceCache, - ), - asl: lookup, + lookup, + namespaceCache, + ), + asl: lookup, + }, }, } } diff --git a/pkg/stores/proxy/unformatter.go b/pkg/stores/proxy/unformatter.go new file mode 100644 index 00000000..eb70aae9 --- /dev/null +++ b/pkg/stores/proxy/unformatter.go @@ -0,0 +1,66 @@ +package proxy + +import ( + "github.com/rancher/apiserver/pkg/types" + "github.com/rancher/wrangler/pkg/data" + "github.com/rancher/wrangler/pkg/data/convert" +) + +// unformatterStore removes fields added by the formatter that kubernetes cannot recognize. +type unformatterStore struct { + types.Store +} + +// ByID looks up a single object by its ID. +func (u *unformatterStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + return u.Store.ByID(apiOp, schema, id) +} + +// List returns a list of resources. +func (u *unformatterStore) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) { + return u.Store.List(apiOp, schema) +} + +// Create creates a single object in the store. +func (u *unformatterStore) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (types.APIObject, error) { + return u.Store.Create(apiOp, schema, data) +} + +// Update updates a single object in the store. +func (u *unformatterStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) { + data = unformat(data) + return u.Store.Update(apiOp, schema, data, id) +} + +// Delete deletes an object from a store. +func (u *unformatterStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + return u.Store.Delete(apiOp, schema, id) + +} + +// Watch returns a channel of events for a list or resource. +func (u *unformatterStore) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) { + return u.Store.Watch(apiOp, schema, wr) +} + +func unformat(obj types.APIObject) types.APIObject { + unst, ok := obj.Object.(map[string]interface{}) + if !ok { + return obj + } + data.RemoveValue(unst, "metadata", "fields") + data.RemoveValue(unst, "metadata", "relationships") + data.RemoveValue(unst, "metadata", "state") + conditions, ok := data.GetValue(unst, "status", "conditions") + if ok { + conditionsSlice := convert.ToMapSlice(conditions) + for i := range conditionsSlice { + data.RemoveValue(conditionsSlice[i], "error") + data.RemoveValue(conditionsSlice[i], "transitioning") + data.RemoveValue(conditionsSlice[i], "lastUpdateTime") + } + data.PutValue(unst, conditionsSlice, "status", "conditions") + } + obj.Object = unst + return obj +} diff --git a/pkg/stores/proxy/unformatter_test.go b/pkg/stores/proxy/unformatter_test.go new file mode 100644 index 00000000..da559a1e --- /dev/null +++ b/pkg/stores/proxy/unformatter_test.go @@ -0,0 +1,112 @@ +package proxy + +import ( + "testing" + + "github.com/rancher/apiserver/pkg/types" + "github.com/stretchr/testify/assert" +) + +func Test_unformat(t *testing.T) { + tests := []struct { + name string + obj types.APIObject + want types.APIObject + }{ + { + name: "noop", + obj: types.APIObject{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "noop", + }, + }, + }, + want: types.APIObject{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "noop", + }, + }, + }, + }, + { + name: "remove fields", + obj: types.APIObject{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "foo", + "fields": []string{ + "name", + "address", + "phonenumber", + }, + "relationships": []map[string]interface{}{ + { + "toId": "bar", + "rel": "uses", + }, + }, + "state": map[string]interface{}{ + "error": false, + }, + }, + "status": map[string]interface{}{ + "conditions": []map[string]interface{}{ + { + "type": "Ready", + "status": "True", + "lastUpdateTime": "a minute ago", + "transitioning": false, + "error": false, + }, + { + "type": "Initialized", + "status": "True", + "lastUpdateTime": "yesterday", + "transitioning": false, + "error": false, + }, + }, + }, + }, + }, + want: types.APIObject{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "foo", + }, + "status": map[string]interface{}{ + "conditions": []map[string]interface{}{ + { + "type": "Ready", + "status": "True", + }, + { + "type": "Initialized", + "status": "True", + }, + }, + }, + }, + }, + }, + { + name: "unrecognized object", + obj: types.APIObject{ + Object: "object", + }, + want: types.APIObject{ + Object: "object", + }, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + got := unformat(test.obj) + assert.Equal(t, test.want, got) + }) + } +}