From 93a87a9af9652984dcfd148b97fb506fbd867572 Mon Sep 17 00:00:00 2001 From: Haowei Cai Date: Wed, 7 Feb 2018 16:04:02 -0800 Subject: [PATCH] Dynamic client support subresource create/get/update/patch verbs Kubernetes-commit: e10cdb3b0f42bafcdf1d1a95e6fb14cbfe2b4ab7 --- dynamic/client.go | 52 ++++++++++++++++++--- dynamic/client_test.go | 104 ++++++++++++++++++++++++++++++++++------- 2 files changed, 131 insertions(+), 25 deletions(-) diff --git a/dynamic/client.go b/dynamic/client.go index 370c2066..b93a0ff1 100644 --- a/dynamic/client.go +++ b/dynamic/client.go @@ -145,6 +145,19 @@ type ResourceClient struct { parameterCodec runtime.ParameterCodec } +func (rc *ResourceClient) parseResourceSubresourceName() (string, []string) { + var resourceName string + var subresourceName []string + if strings.Contains(rc.resource.Name, "/") { + resourceName = strings.Split(rc.resource.Name, "/")[0] + subresourceName = strings.Split(rc.resource.Name, "/")[1:] + } else { + resourceName = rc.resource.Name + } + + return resourceName, subresourceName +} + // List returns a list of objects for this resource. func (rc *ResourceClient) List(opts metav1.ListOptions) (runtime.Object, error) { parameterEncoder := rc.parameterCodec @@ -166,9 +179,11 @@ func (rc *ResourceClient) Get(name string, opts metav1.GetOptions) (*unstructure parameterEncoder = defaultParameterEncoder } result := new(unstructured.Unstructured) + resourceName, subresourceName := rc.parseResourceSubresourceName() err := rc.cl.Get(). NamespaceIfScoped(rc.ns, rc.resource.Namespaced). - Resource(rc.resource.Name). + Resource(resourceName). + SubResource(subresourceName...). VersionedParams(&opts, parameterEncoder). Name(name). Do(). @@ -205,11 +220,26 @@ func (rc *ResourceClient) DeleteCollection(deleteOptions *metav1.DeleteOptions, // Create creates the provided resource. func (rc *ResourceClient) Create(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { result := new(unstructured.Unstructured) - err := rc.cl.Post(). + resourceName, subresourceName := rc.parseResourceSubresourceName() + req := rc.cl.Post(). NamespaceIfScoped(rc.ns, rc.resource.Namespaced). - Resource(rc.resource.Name). - Body(obj). - Do(). + Resource(resourceName). + Body(obj) + if len(subresourceName) > 0 { + // If the provided resource is a subresource, the POST request should contain + // object name. Examples of subresources that support Create operation: + // core/v1/pods/{name}/binding + // core/v1/pods/{name}/eviction + // extensions/v1beta1/deployments/{name}/rollback + // apps/v1beta1/deployments/{name}/rollback + // NOTE: Currently our system assumes every subresource object has the same + // name as the parent resource object. E.g. a pods/binding object having + // metadada.name "foo" means pod "foo" is being bound. We may need to + // change this if we break the assumption in the future. + req = req.SubResource(subresourceName...). + Name(obj.GetName()) + } + err := req.Do(). Into(result) return result, err } @@ -220,9 +250,15 @@ func (rc *ResourceClient) Update(obj *unstructured.Unstructured) (*unstructured. if len(obj.GetName()) == 0 { return result, errors.New("object missing name") } + resourceName, subresourceName := rc.parseResourceSubresourceName() err := rc.cl.Put(). NamespaceIfScoped(rc.ns, rc.resource.Namespaced). - Resource(rc.resource.Name). + Resource(resourceName). + SubResource(subresourceName...). + // NOTE: Currently our system assumes every subresource object has the same + // name as the parent resource object. E.g. a pods/binding object having + // metadada.name "foo" means pod "foo" is being bound. We may need to + // change this if we break the assumption in the future. Name(obj.GetName()). Body(obj). Do(). @@ -246,9 +282,11 @@ func (rc *ResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, error func (rc *ResourceClient) Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error) { result := new(unstructured.Unstructured) + resourceName, subresourceName := rc.parseResourceSubresourceName() err := rc.cl.Patch(pt). NamespaceIfScoped(rc.ns, rc.resource.Namespaced). - Resource(rc.resource.Name). + Resource(resourceName). + SubResource(subresourceName...). Name(name). Body(data). Do(). diff --git a/dynamic/client_test.go b/dynamic/client_test.go index 1459429c..ffa550ed 100644 --- a/dynamic/client_test.go +++ b/dynamic/client_test.go @@ -150,6 +150,7 @@ func TestList(t *testing.T) { func TestGet(t *testing.T) { tcs := []struct { + resource string namespace string name string path string @@ -157,22 +158,39 @@ func TestGet(t *testing.T) { want *unstructured.Unstructured }{ { - name: "normal_get", - path: "/api/gtest/vtest/rtest/normal_get", - resp: getJSON("vTest", "rTest", "normal_get"), - want: getObject("vTest", "rTest", "normal_get"), + resource: "rtest", + name: "normal_get", + path: "/api/gtest/vtest/rtest/normal_get", + resp: getJSON("vTest", "rTest", "normal_get"), + want: getObject("vTest", "rTest", "normal_get"), }, { + resource: "rtest", namespace: "nstest", name: "namespaced_get", path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_get", resp: getJSON("vTest", "rTest", "namespaced_get"), want: getObject("vTest", "rTest", "namespaced_get"), }, + { + resource: "rtest/srtest", + name: "normal_subresource_get", + path: "/api/gtest/vtest/rtest/normal_subresource_get/srtest", + resp: getJSON("vTest", "srTest", "normal_subresource_get"), + want: getObject("vTest", "srTest", "normal_subresource_get"), + }, + { + resource: "rtest/srtest", + namespace: "nstest", + name: "namespaced_subresource_get", + path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_get/srtest", + resp: getJSON("vTest", "srTest", "namespaced_subresource_get"), + want: getObject("vTest", "srTest", "namespaced_subresource_get"), + }, } for _, tc := range tcs { gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} - resource := &metav1.APIResource{Name: "rtest", Namespaced: len(tc.namespace) != 0} + resource := &metav1.APIResource{Name: tc.resource, Namespaced: len(tc.namespace) != 0} cl, srv, err := getClientServer(gv, func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { t.Errorf("Get(%q) got HTTP method %s. wanted GET", tc.name, r.Method) @@ -303,26 +321,42 @@ func TestDeleteCollection(t *testing.T) { func TestCreate(t *testing.T) { tcs := []struct { + resource string name string namespace string obj *unstructured.Unstructured path string }{ { - name: "normal_create", - path: "/api/gtest/vtest/rtest", - obj: getObject("vTest", "rTest", "normal_create"), + resource: "rtest", + name: "normal_create", + path: "/api/gtest/vtest/rtest", + obj: getObject("vTest", "rTest", "normal_create"), }, { + resource: "rtest", name: "namespaced_create", namespace: "nstest", path: "/api/gtest/vtest/namespaces/nstest/rtest", obj: getObject("vTest", "rTest", "namespaced_create"), }, + { + resource: "rtest/srtest", + name: "normal_subresource_create", + path: "/api/gtest/vtest/rtest/normal_subresource_create/srtest", + obj: getObject("vTest", "srTest", "normal_subresource_create"), + }, + { + resource: "rtest/srtest", + name: "namespaced_subresource_create", + namespace: "nstest", + path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_create/srtest", + obj: getObject("vTest", "srTest", "namespaced_subresource_create"), + }, } for _, tc := range tcs { gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} - resource := &metav1.APIResource{Name: "rtest", Namespaced: len(tc.namespace) != 0} + resource := &metav1.APIResource{Name: tc.resource, Namespaced: len(tc.namespace) != 0} cl, srv, err := getClientServer(gv, func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Errorf("Create(%q) got HTTP method %s. wanted POST", tc.name, r.Method) @@ -362,26 +396,42 @@ func TestCreate(t *testing.T) { func TestUpdate(t *testing.T) { tcs := []struct { + resource string name string namespace string obj *unstructured.Unstructured path string }{ { - name: "normal_update", - path: "/api/gtest/vtest/rtest/normal_update", - obj: getObject("vTest", "rTest", "normal_update"), + resource: "rtest", + name: "normal_update", + path: "/api/gtest/vtest/rtest/normal_update", + obj: getObject("vTest", "rTest", "normal_update"), }, { + resource: "rtest", name: "namespaced_update", namespace: "nstest", path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_update", obj: getObject("vTest", "rTest", "namespaced_update"), }, + { + resource: "rtest/srtest", + name: "normal_subresource_update", + path: "/api/gtest/vtest/rtest/normal_update/srtest", + obj: getObject("vTest", "srTest", "normal_update"), + }, + { + resource: "rtest/srtest", + name: "namespaced_subresource_update", + namespace: "nstest", + path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_update/srtest", + obj: getObject("vTest", "srTest", "namespaced_update"), + }, } for _, tc := range tcs { gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} - resource := &metav1.APIResource{Name: "rtest", Namespaced: len(tc.namespace) != 0} + resource := &metav1.APIResource{Name: tc.resource, Namespaced: len(tc.namespace) != 0} cl, srv, err := getClientServer(gv, func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { t.Errorf("Update(%q) got HTTP method %s. wanted PUT", tc.name, r.Method) @@ -492,6 +542,7 @@ func TestWatch(t *testing.T) { func TestPatch(t *testing.T) { tcs := []struct { + resource string name string namespace string patch []byte @@ -499,22 +550,39 @@ func TestPatch(t *testing.T) { path string }{ { - name: "normal_patch", - path: "/api/gtest/vtest/rtest/normal_patch", - patch: getJSON("vTest", "rTest", "normal_patch"), - want: getObject("vTest", "rTest", "normal_patch"), + resource: "rtest", + name: "normal_patch", + path: "/api/gtest/vtest/rtest/normal_patch", + patch: getJSON("vTest", "rTest", "normal_patch"), + want: getObject("vTest", "rTest", "normal_patch"), }, { + resource: "rtest", name: "namespaced_patch", namespace: "nstest", path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_patch", patch: getJSON("vTest", "rTest", "namespaced_patch"), want: getObject("vTest", "rTest", "namespaced_patch"), }, + { + resource: "rtest/srtest", + name: "normal_subresource_patch", + path: "/api/gtest/vtest/rtest/normal_subresource_patch/srtest", + patch: getJSON("vTest", "srTest", "normal_subresource_patch"), + want: getObject("vTest", "srTest", "normal_subresource_patch"), + }, + { + resource: "rtest/srtest", + name: "namespaced_subresource_patch", + namespace: "nstest", + path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_patch/srtest", + patch: getJSON("vTest", "srTest", "namespaced_subresource_patch"), + want: getObject("vTest", "srTest", "namespaced_subresource_patch"), + }, } for _, tc := range tcs { gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} - resource := &metav1.APIResource{Name: "rtest", Namespaced: len(tc.namespace) != 0} + resource := &metav1.APIResource{Name: tc.resource, Namespaced: len(tc.namespace) != 0} cl, srv, err := getClientServer(gv, func(w http.ResponseWriter, r *http.Request) { if r.Method != "PATCH" { t.Errorf("Patch(%q) got HTTP method %s. wanted PATCH", tc.name, r.Method)