1
0
mirror of https://github.com/rancher/steve.git synced 2025-09-01 15:37:31 +00:00

Adding Group, Version, Kind in Update proxy_store.go (#270)

* adding gvk completion to update method in proxy_store
This commit is contained in:
Felipe Gehrke
2024-09-13 15:19:58 -03:00
committed by GitHub
parent f02f3f9411
commit 99e479ba0f
5 changed files with 726 additions and 9 deletions

View File

@@ -4,6 +4,7 @@ package partition
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"reflect" "reflect"
@@ -389,7 +390,7 @@ func ToAPIEvent(apiOp *types.APIRequest, schema *types.APISchema, event watch.Ev
if event.Type == watch.Error { if event.Type == watch.Error {
status, _ := event.Object.(*metav1.Status) status, _ := event.Object.(*metav1.Status)
apiEvent.Error = fmt.Errorf(status.Message) apiEvent.Error = errors.New(status.Message)
return apiEvent return apiEvent
} }

View File

@@ -40,8 +40,9 @@ import (
) )
const ( const (
watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS" watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS"
errNamespaceRequired = "metadata.namespace is required" errNamespaceRequired = "metadata.namespace is required"
errResourceVersionRequired = "metadata.resourceVersion is required for update"
) )
var ( var (
@@ -515,9 +516,12 @@ func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, params
resourceVersion := input.String("metadata", "resourceVersion") resourceVersion := input.String("metadata", "resourceVersion")
if resourceVersion == "" { if resourceVersion == "" {
return nil, nil, fmt.Errorf("metadata.resourceVersion is required for update") return nil, nil, errors.New(errResourceVersionRequired)
} }
gvk := attributes.GVK(schema)
input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind()
opts := metav1.UpdateOptions{} opts := metav1.UpdateOptions{}
if err := decodeParams(apiOp, &opts); err != nil { if err := decodeParams(apiOp, &opts); err != nil {
return nil, nil, err return nil, nil, err

View File

@@ -343,7 +343,7 @@ func TestCreate(t *testing.T) {
}, },
}, },
{ {
name: "missing namespace in the params / should copy from apiOp", name: "missing namespace in the params (should copy from apiOp)",
input: input{ input: input{
apiOp: &types.APIRequest{ apiOp: &types.APIRequest{
Schema: &types.APISchema{ Schema: &types.APISchema{
@@ -466,7 +466,7 @@ func TestCreate(t *testing.T) {
for _, tt := range testCases { for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
testClientFactory, err := client.NewFactory(&rest.Config{}, false) testClientFactory, err := client.NewFactory(&rest.Config{}, false)
assert.Nil(t, err) assert.NoError(t, err)
fakeClient := fake.NewSimpleDynamicClient(runtime.NewScheme()) fakeClient := fake.NewSimpleDynamicClient(runtime.NewScheme())
@@ -488,3 +488,357 @@ func TestCreate(t *testing.T) {
}) })
} }
} }
func TestUpdate(t *testing.T) {
type input struct {
apiOp *types.APIRequest
schema *types.APISchema
params types.APIObject
id string
}
type expected struct {
value *unstructured.Unstructured
warning []types.Warning
err error
}
sampleCreateInput := input{
apiOp: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
Method: http.MethodPost,
},
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Method: http.MethodPost,
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"kind": "Secret",
"version": "v1",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"kind": "Secret",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
},
},
}
testCases := []struct {
name string
updateCallbackFunc clientgotesting.ReactionFunc
createInput *input
updateInput input
expected expected
}{
{
name: "update - usual request",
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
createInput: &sampleCreateInput,
updateInput: input{
apiOp: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
Method: http.MethodPut,
},
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Method: http.MethodPut,
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v2",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v2",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
},
},
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v2",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "update - different apiVersion and kind (params and schema) - should copy from schema",
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
createInput: &sampleCreateInput,
updateInput: input{
apiOp: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
Method: http.MethodPut,
},
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Method: http.MethodPut,
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v2",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
},
},
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v2",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "update - missing apiVersion and kind in params - should copy from schema",
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
createInput: &sampleCreateInput,
updateInput: input{
apiOp: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
Method: http.MethodPost,
},
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Method: http.MethodPost,
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v2",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
},
},
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v2",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "update - missing resource version",
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
updateInput: input{
apiOp: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
Method: http.MethodPut,
},
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Method: http.MethodPut,
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
},
},
},
},
expected: expected{
value: nil,
warning: nil,
err: errors.New(errResourceVersionRequired),
},
},
{
name: "update - error request",
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, apierrors.NewUnauthorized("sample reason")
},
createInput: &sampleCreateInput,
updateInput: input{
apiOp: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
Method: http.MethodPut,
},
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Method: http.MethodPut,
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v2",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
},
},
},
expected: expected{
value: nil,
warning: nil,
err: apierrors.NewUnauthorized("sample reason"),
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
testClientFactory, err := client.NewFactory(&rest.Config{}, false)
assert.NoError(t, err)
fakeClient := fake.NewSimpleDynamicClient(runtime.NewScheme())
if tt.updateCallbackFunc != nil {
fakeClient.PrependReactor("update", "*", tt.updateCallbackFunc)
}
testStore := Store{
clientGetter: &testFactory{Factory: testClientFactory,
fakeClient: fakeClient,
},
}
// Creating the object first, so we can update it later (this function is not the SUT)
if tt.createInput != nil {
_, _, err = testStore.Create(tt.createInput.apiOp, tt.createInput.schema, tt.createInput.params)
assert.NoError(t, err)
}
value, warning, err := testStore.Update(tt.updateInput.apiOp, tt.updateInput.schema, tt.updateInput.params, tt.updateInput.id)
assert.Equal(t, tt.expected.value, value)
assert.Equal(t, tt.expected.warning, warning)
if tt.expected.err != nil {
assert.Equal(t, tt.expected.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -50,8 +50,9 @@ import (
) )
const ( const (
watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS" watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS"
errNamespaceRequired = "metadata.namespace or apiOp.namespace are required" errNamespaceRequired = "metadata.namespace or apiOp.namespace are required"
errResourceVersionRequired = "metadata.resourceVersion is required for update"
) )
var ( var (
@@ -598,9 +599,12 @@ func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, params
resourceVersion := input.String("metadata", "resourceVersion") resourceVersion := input.String("metadata", "resourceVersion")
if resourceVersion == "" { if resourceVersion == "" {
return nil, nil, fmt.Errorf("metadata.resourceVersion is required for update") return nil, nil, errors.New(errResourceVersionRequired)
} }
gvk := attributes.GVK(schema)
input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind()
opts := metav1.UpdateOptions{} opts := metav1.UpdateOptions{}
if err := decodeParams(apiOp, &opts); err != nil { if err := decodeParams(apiOp, &opts); err != nil {
return nil, nil, err return nil, nil, err

View File

@@ -1104,3 +1104,357 @@ func TestCreate(t *testing.T) {
}) })
} }
} }
func TestUpdate(t *testing.T) {
type input struct {
apiOp *types.APIRequest
schema *types.APISchema
params types.APIObject
id string
}
type expected struct {
value *unstructured.Unstructured
warning []types.Warning
err error
}
sampleCreateInput := input{
apiOp: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
Method: http.MethodPost,
},
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Method: http.MethodPost,
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"kind": "Secret",
"version": "v1",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"kind": "Secret",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
},
},
}
testCases := []struct {
name string
updateCallbackFunc clientgotesting.ReactionFunc
createInput *input
updateInput input
expected expected
}{
{
name: "update - usual request",
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
createInput: &sampleCreateInput,
updateInput: input{
apiOp: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
Method: http.MethodPut,
},
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Method: http.MethodPut,
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v2",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v2",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
},
},
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v2",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "update - different apiVersion and kind (params and schema) - should copy from schema",
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
createInput: &sampleCreateInput,
updateInput: input{
apiOp: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
Method: http.MethodPut,
},
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Method: http.MethodPut,
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v2",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
},
},
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v2",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "update - missing apiVersion and kind in params - should copy from schema",
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
createInput: &sampleCreateInput,
updateInput: input{
apiOp: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
Method: http.MethodPost,
},
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Method: http.MethodPost,
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v2",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
},
},
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v2",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "update - missing resource version",
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
updateInput: input{
apiOp: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
Method: http.MethodPut,
},
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Method: http.MethodPut,
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
},
},
},
},
expected: expected{
value: nil,
warning: nil,
err: errors.New(errResourceVersionRequired),
},
},
{
name: "update - error request",
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, apierrors.NewUnauthorized("sample reason")
},
createInput: &sampleCreateInput,
updateInput: input{
apiOp: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
Method: http.MethodPut,
},
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Method: http.MethodPut,
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v2",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
"resourceVersion": "1",
},
},
},
},
expected: expected{
value: nil,
warning: nil,
err: apierrors.NewUnauthorized("sample reason"),
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
testClientFactory, err := client.NewFactory(&rest.Config{}, false)
assert.NoError(t, err)
fakeClient := fake.NewSimpleDynamicClient(runtime.NewScheme())
if tt.updateCallbackFunc != nil {
fakeClient.PrependReactor("update", "*", tt.updateCallbackFunc)
}
testStore := Store{
clientGetter: &testFactory{Factory: testClientFactory,
fakeClient: fakeClient,
},
}
// Creating the object first, so we can update it later (this function is not the SUT)
if tt.createInput != nil {
_, _, err = testStore.Create(tt.createInput.apiOp, tt.createInput.schema, tt.createInput.params)
assert.NoError(t, err)
}
value, warning, err := testStore.Update(tt.updateInput.apiOp, tt.updateInput.schema, tt.updateInput.params, tt.updateInput.id)
assert.Equal(t, tt.expected.value, value)
assert.Equal(t, tt.expected.warning, warning)
if tt.expected.err != nil {
assert.Equal(t, tt.expected.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
}
}