Skip to content

Commit

Permalink
Dynamic client support subresource create/get/update/patch verbs
Browse files Browse the repository at this point in the history
  • Loading branch information
roycaihw committed Feb 13, 2018
1 parent 3904cc7 commit e10cdb3
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 25 deletions.
52 changes: 45 additions & 7 deletions staging/src/k8s.io/client-go/dynamic/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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().
Expand Down Expand Up @@ -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
}
Expand All @@ -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().
Expand All @@ -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().
Expand Down
104 changes: 86 additions & 18 deletions staging/src/k8s.io/client-go/dynamic/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,29 +150,47 @@ func TestList(t *testing.T) {

func TestGet(t *testing.T) {
tcs := []struct {
resource string
namespace string
name string
path string
resp []byte
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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -492,29 +542,47 @@ func TestWatch(t *testing.T) {

func TestPatch(t *testing.T) {
tcs := []struct {
resource string
name string
namespace string
patch []byte
want *unstructured.Unstructured
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)
Expand Down

0 comments on commit e10cdb3

Please sign in to comment.