Skip to content

Commit

Permalink
Drop unrecognized fields before update
Browse files Browse the repository at this point in the history
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
  • Loading branch information
cmurphy committed Jul 5, 2023
1 parent bf2e965 commit aca26c2
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 11 deletions.
24 changes: 13 additions & 11 deletions pkg/stores/proxy/proxy_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
}
}
Expand Down
66 changes: 66 additions & 0 deletions pkg/stores/proxy/unformatter.go
Original file line number Diff line number Diff line change
@@ -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
}
110 changes: 110 additions & 0 deletions pkg/stores/proxy/unformatter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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 {
t.Run(test.name, func(t *testing.T) {
got := unformat(test.obj)
assert.Equal(t, test.want, got)
})
}
}

0 comments on commit aca26c2

Please sign in to comment.