Dynamic client support subresource create/get/update/patch verbs

Kubernetes-commit: e10cdb3b0f42bafcdf1d1a95e6fb14cbfe2b4ab7
This commit is contained in:
Haowei Cai 2018-02-07 16:04:02 -08:00 committed by Kubernetes Publisher
parent 5f85fe426e
commit 93a87a9af9
2 changed files with 131 additions and 25 deletions

View File

@ -145,6 +145,19 @@ type ResourceClient struct {
parameterCodec runtime.ParameterCodec 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. // List returns a list of objects for this resource.
func (rc *ResourceClient) List(opts metav1.ListOptions) (runtime.Object, error) { func (rc *ResourceClient) List(opts metav1.ListOptions) (runtime.Object, error) {
parameterEncoder := rc.parameterCodec parameterEncoder := rc.parameterCodec
@ -166,9 +179,11 @@ func (rc *ResourceClient) Get(name string, opts metav1.GetOptions) (*unstructure
parameterEncoder = defaultParameterEncoder parameterEncoder = defaultParameterEncoder
} }
result := new(unstructured.Unstructured) result := new(unstructured.Unstructured)
resourceName, subresourceName := rc.parseResourceSubresourceName()
err := rc.cl.Get(). err := rc.cl.Get().
NamespaceIfScoped(rc.ns, rc.resource.Namespaced). NamespaceIfScoped(rc.ns, rc.resource.Namespaced).
Resource(rc.resource.Name). Resource(resourceName).
SubResource(subresourceName...).
VersionedParams(&opts, parameterEncoder). VersionedParams(&opts, parameterEncoder).
Name(name). Name(name).
Do(). Do().
@ -205,11 +220,26 @@ func (rc *ResourceClient) DeleteCollection(deleteOptions *metav1.DeleteOptions,
// Create creates the provided resource. // Create creates the provided resource.
func (rc *ResourceClient) Create(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { func (rc *ResourceClient) Create(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
result := new(unstructured.Unstructured) result := new(unstructured.Unstructured)
err := rc.cl.Post(). resourceName, subresourceName := rc.parseResourceSubresourceName()
req := rc.cl.Post().
NamespaceIfScoped(rc.ns, rc.resource.Namespaced). NamespaceIfScoped(rc.ns, rc.resource.Namespaced).
Resource(rc.resource.Name). Resource(resourceName).
Body(obj). Body(obj)
Do(). 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) Into(result)
return result, err return result, err
} }
@ -220,9 +250,15 @@ func (rc *ResourceClient) Update(obj *unstructured.Unstructured) (*unstructured.
if len(obj.GetName()) == 0 { if len(obj.GetName()) == 0 {
return result, errors.New("object missing name") return result, errors.New("object missing name")
} }
resourceName, subresourceName := rc.parseResourceSubresourceName()
err := rc.cl.Put(). err := rc.cl.Put().
NamespaceIfScoped(rc.ns, rc.resource.Namespaced). 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()). Name(obj.GetName()).
Body(obj). Body(obj).
Do(). 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) { func (rc *ResourceClient) Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error) {
result := new(unstructured.Unstructured) result := new(unstructured.Unstructured)
resourceName, subresourceName := rc.parseResourceSubresourceName()
err := rc.cl.Patch(pt). err := rc.cl.Patch(pt).
NamespaceIfScoped(rc.ns, rc.resource.Namespaced). NamespaceIfScoped(rc.ns, rc.resource.Namespaced).
Resource(rc.resource.Name). Resource(resourceName).
SubResource(subresourceName...).
Name(name). Name(name).
Body(data). Body(data).
Do(). Do().

View File

@ -150,6 +150,7 @@ func TestList(t *testing.T) {
func TestGet(t *testing.T) { func TestGet(t *testing.T) {
tcs := []struct { tcs := []struct {
resource string
namespace string namespace string
name string name string
path string path string
@ -157,22 +158,39 @@ func TestGet(t *testing.T) {
want *unstructured.Unstructured want *unstructured.Unstructured
}{ }{
{ {
name: "normal_get", resource: "rtest",
path: "/api/gtest/vtest/rtest/normal_get", name: "normal_get",
resp: getJSON("vTest", "rTest", "normal_get"), path: "/api/gtest/vtest/rtest/normal_get",
want: getObject("vTest", "rTest", "normal_get"), resp: getJSON("vTest", "rTest", "normal_get"),
want: getObject("vTest", "rTest", "normal_get"),
}, },
{ {
resource: "rtest",
namespace: "nstest", namespace: "nstest",
name: "namespaced_get", name: "namespaced_get",
path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_get", path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_get",
resp: getJSON("vTest", "rTest", "namespaced_get"), resp: getJSON("vTest", "rTest", "namespaced_get"),
want: getObject("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 { for _, tc := range tcs {
gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} 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) { cl, srv, err := getClientServer(gv, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" { if r.Method != "GET" {
t.Errorf("Get(%q) got HTTP method %s. wanted GET", tc.name, r.Method) 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) { func TestCreate(t *testing.T) {
tcs := []struct { tcs := []struct {
resource string
name string name string
namespace string namespace string
obj *unstructured.Unstructured obj *unstructured.Unstructured
path string path string
}{ }{
{ {
name: "normal_create", resource: "rtest",
path: "/api/gtest/vtest/rtest", name: "normal_create",
obj: getObject("vTest", "rTest", "normal_create"), path: "/api/gtest/vtest/rtest",
obj: getObject("vTest", "rTest", "normal_create"),
}, },
{ {
resource: "rtest",
name: "namespaced_create", name: "namespaced_create",
namespace: "nstest", namespace: "nstest",
path: "/api/gtest/vtest/namespaces/nstest/rtest", path: "/api/gtest/vtest/namespaces/nstest/rtest",
obj: getObject("vTest", "rTest", "namespaced_create"), 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 { for _, tc := range tcs {
gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} 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) { cl, srv, err := getClientServer(gv, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" { if r.Method != "POST" {
t.Errorf("Create(%q) got HTTP method %s. wanted POST", tc.name, r.Method) 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) { func TestUpdate(t *testing.T) {
tcs := []struct { tcs := []struct {
resource string
name string name string
namespace string namespace string
obj *unstructured.Unstructured obj *unstructured.Unstructured
path string path string
}{ }{
{ {
name: "normal_update", resource: "rtest",
path: "/api/gtest/vtest/rtest/normal_update", name: "normal_update",
obj: getObject("vTest", "rTest", "normal_update"), path: "/api/gtest/vtest/rtest/normal_update",
obj: getObject("vTest", "rTest", "normal_update"),
}, },
{ {
resource: "rtest",
name: "namespaced_update", name: "namespaced_update",
namespace: "nstest", namespace: "nstest",
path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_update", path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_update",
obj: getObject("vTest", "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 { for _, tc := range tcs {
gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} 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) { cl, srv, err := getClientServer(gv, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "PUT" { if r.Method != "PUT" {
t.Errorf("Update(%q) got HTTP method %s. wanted PUT", tc.name, r.Method) 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) { func TestPatch(t *testing.T) {
tcs := []struct { tcs := []struct {
resource string
name string name string
namespace string namespace string
patch []byte patch []byte
@ -499,22 +550,39 @@ func TestPatch(t *testing.T) {
path string path string
}{ }{
{ {
name: "normal_patch", resource: "rtest",
path: "/api/gtest/vtest/rtest/normal_patch", name: "normal_patch",
patch: getJSON("vTest", "rTest", "normal_patch"), path: "/api/gtest/vtest/rtest/normal_patch",
want: getObject("vTest", "rTest", "normal_patch"), patch: getJSON("vTest", "rTest", "normal_patch"),
want: getObject("vTest", "rTest", "normal_patch"),
}, },
{ {
resource: "rtest",
name: "namespaced_patch", name: "namespaced_patch",
namespace: "nstest", namespace: "nstest",
path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_patch", path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_patch",
patch: getJSON("vTest", "rTest", "namespaced_patch"), patch: getJSON("vTest", "rTest", "namespaced_patch"),
want: getObject("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 { for _, tc := range tcs {
gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} 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) { cl, srv, err := getClientServer(gv, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "PATCH" { if r.Method != "PATCH" {
t.Errorf("Patch(%q) got HTTP method %s. wanted PATCH", tc.name, r.Method) t.Errorf("Patch(%q) got HTTP method %s. wanted PATCH", tc.name, r.Method)