diff --git a/dynamic/bad_debt.go b/deprecated-dynamic/bad_debt.go similarity index 98% rename from dynamic/bad_debt.go rename to deprecated-dynamic/bad_debt.go index 8492d56a..51e4a583 100644 --- a/dynamic/bad_debt.go +++ b/deprecated-dynamic/bad_debt.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package dynamic +package deprecated_dynamic import ( "encoding/json" diff --git a/dynamic/client.go b/deprecated-dynamic/client.go similarity index 73% rename from dynamic/client.go rename to deprecated-dynamic/client.go index 43db68c4..46c7535a 100644 --- a/dynamic/client.go +++ b/deprecated-dynamic/client.go @@ -17,7 +17,7 @@ limitations under the License. // Package dynamic provides a client interface to arbitrary Kubernetes // APIs that exposes common high level operations and exposes common // metadata. -package dynamic +package deprecated_dynamic import ( "strings" @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" restclient "k8s.io/client-go/rest" ) @@ -65,13 +66,13 @@ type ResourceInterface interface { // and manipulate metadata of a Kubernetes API group, and implements Interface. type Client struct { version schema.GroupVersion - delegate DynamicInterface + delegate dynamic.DynamicInterface } // NewClient returns a new client based on the passed in config. The // codec is ignored, as the dynamic client uses it's own codec. func NewClient(conf *restclient.Config, version schema.GroupVersion) (*Client, error) { - delegate, err := NewForConfig(conf) + delegate, err := dynamic.NewForConfig(conf) if err != nil { return nil, err } @@ -84,24 +85,41 @@ func NewClient(conf *restclient.Config, version schema.GroupVersion) (*Client, e // is ignored. The ResourceInterface inherits the parameter codec of c. func (c *Client) Resource(resource *metav1.APIResource, namespace string) ResourceInterface { resourceTokens := strings.SplitN(resource.Name, "/", 2) - subresource := "" + subresources := []string{} if len(resourceTokens) > 1 { - subresource = resourceTokens[1] + subresources = strings.Split(resourceTokens[1], "/") } if len(namespace) == 0 { - return oldResourceShim(c.delegate.ClusterSubresource(c.version.WithResource(resourceTokens[0]), subresource)) + return oldResourceShim(c.delegate.Resource(c.version.WithResource(resourceTokens[0])), subresources) } - return oldResourceShim(c.delegate.NamespacedSubresource(c.version.WithResource(resourceTokens[0]), subresource, namespace)) + return oldResourceShim(c.delegate.Resource(c.version.WithResource(resourceTokens[0])).Namespace(namespace), subresources) } // the old interfaces used the wrong type for lists. this fixes that -func oldResourceShim(in DynamicResourceInterface) ResourceInterface { - return oldResourceShimType{DynamicResourceInterface: in} +func oldResourceShim(in dynamic.DynamicResourceInterface, subresources []string) ResourceInterface { + return oldResourceShimType{DynamicResourceInterface: in, subresources: subresources} } type oldResourceShimType struct { - DynamicResourceInterface + dynamic.DynamicResourceInterface + subresources []string +} + +func (s oldResourceShimType) Create(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + return s.DynamicResourceInterface.Create(obj, s.subresources...) +} + +func (s oldResourceShimType) Update(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + return s.DynamicResourceInterface.Update(obj, s.subresources...) +} + +func (s oldResourceShimType) Delete(name string, opts *metav1.DeleteOptions) error { + return s.DynamicResourceInterface.Delete(name, opts, s.subresources...) +} + +func (s oldResourceShimType) Get(name string, opts metav1.GetOptions) (*unstructured.Unstructured, error) { + return s.DynamicResourceInterface.Get(name, opts, s.subresources...) } func (s oldResourceShimType) List(opts metav1.ListOptions) (runtime.Object, error) { @@ -109,5 +127,5 @@ func (s oldResourceShimType) List(opts metav1.ListOptions) (runtime.Object, erro } func (s oldResourceShimType) Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error) { - return s.DynamicResourceInterface.Patch(name, pt, data) + return s.DynamicResourceInterface.Patch(name, pt, data, s.subresources...) } diff --git a/dynamic/client_pool.go b/deprecated-dynamic/client_pool.go similarity index 99% rename from dynamic/client_pool.go rename to deprecated-dynamic/client_pool.go index f4d258be..36dc54ce 100644 --- a/dynamic/client_pool.go +++ b/deprecated-dynamic/client_pool.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package dynamic +package deprecated_dynamic import ( "sync" diff --git a/deprecated-dynamic/client_test.go b/deprecated-dynamic/client_test.go new file mode 100644 index 00000000..79047452 --- /dev/null +++ b/deprecated-dynamic/client_test.go @@ -0,0 +1,623 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package deprecated_dynamic + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer/streaming" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + restclient "k8s.io/client-go/rest" + restclientwatch "k8s.io/client-go/rest/watch" +) + +func getJSON(version, kind, name string) []byte { + return []byte(fmt.Sprintf(`{"apiVersion": %q, "kind": %q, "metadata": {"name": %q}}`, version, kind, name)) +} + +func getListJSON(version, kind string, items ...[]byte) []byte { + json := fmt.Sprintf(`{"apiVersion": %q, "kind": %q, "items": [%s]}`, + version, kind, bytes.Join(items, []byte(","))) + return []byte(json) +} + +func getObject(version, kind, name string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": version, + "kind": kind, + "metadata": map[string]interface{}{ + "name": name, + }, + }, + } +} + +func getClientServer(gv *schema.GroupVersion, h func(http.ResponseWriter, *http.Request)) (Interface, *httptest.Server, error) { + srv := httptest.NewServer(http.HandlerFunc(h)) + cl, err := NewClient(&restclient.Config{ + Host: srv.URL, + ContentConfig: restclient.ContentConfig{GroupVersion: gv}, + }, *gv) + if err != nil { + srv.Close() + return nil, nil, err + } + return cl, srv, nil +} + +func TestList(t *testing.T) { + tcs := []struct { + name string + namespace string + path string + resp []byte + want *unstructured.UnstructuredList + }{ + { + name: "normal_list", + path: "/apis/gtest/vtest/rtest", + resp: getListJSON("vTest", "rTestList", + getJSON("vTest", "rTest", "item1"), + getJSON("vTest", "rTest", "item2")), + want: &unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "apiVersion": "vTest", + "kind": "rTestList", + }, + Items: []unstructured.Unstructured{ + *getObject("vTest", "rTest", "item1"), + *getObject("vTest", "rTest", "item2"), + }, + }, + }, + { + name: "namespaced_list", + namespace: "nstest", + path: "/apis/gtest/vtest/namespaces/nstest/rtest", + resp: getListJSON("vTest", "rTestList", + getJSON("vTest", "rTest", "item1"), + getJSON("vTest", "rTest", "item2")), + want: &unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "apiVersion": "vTest", + "kind": "rTestList", + }, + Items: []unstructured.Unstructured{ + *getObject("vTest", "rTest", "item1"), + *getObject("vTest", "rTest", "item2"), + }, + }, + }, + } + for _, tc := range tcs { + gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} + resource := &metav1.APIResource{Name: "rtest", Namespaced: len(tc.namespace) != 0} + cl, srv, err := getClientServer(gv, func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("List(%q) got HTTP method %s. wanted GET", tc.name, r.Method) + } + + if r.URL.Path != tc.path { + t.Errorf("List(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) + } + + w.Header().Set("Content-Type", runtime.ContentTypeJSON) + w.Write(tc.resp) + }) + if err != nil { + t.Errorf("unexpected error when creating client: %v", err) + continue + } + defer srv.Close() + + got, err := cl.Resource(resource, tc.namespace).List(metav1.ListOptions{}) + if err != nil { + t.Errorf("unexpected error when listing %q: %v", tc.name, err) + continue + } + + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("List(%q) want: %v\ngot: %v", tc.name, tc.want, got) + } + } +} + +func TestGet(t *testing.T) { + tcs := []struct { + resource string + namespace string + name string + path string + resp []byte + want *unstructured.Unstructured + }{ + { + resource: "rtest", + name: "normal_get", + path: "/apis/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: "/apis/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: "/apis/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: "/apis/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: 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) + } + + if r.URL.Path != tc.path { + t.Errorf("Get(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) + } + + w.Header().Set("Content-Type", runtime.ContentTypeJSON) + w.Write(tc.resp) + }) + if err != nil { + t.Errorf("unexpected error when creating client: %v", err) + continue + } + defer srv.Close() + + got, err := cl.Resource(resource, tc.namespace).Get(tc.name, metav1.GetOptions{}) + if err != nil { + t.Errorf("unexpected error when getting %q: %v", tc.name, err) + continue + } + + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("Get(%q) want: %v\ngot: %v", tc.name, tc.want, got) + } + } +} + +func TestDelete(t *testing.T) { + background := metav1.DeletePropagationBackground + uid := types.UID("uid") + + statusOK := &metav1.Status{ + TypeMeta: metav1.TypeMeta{Kind: "Status"}, + Status: metav1.StatusSuccess, + } + tcs := []struct { + namespace string + name string + path string + deleteOptions *metav1.DeleteOptions + }{ + { + name: "normal_delete", + path: "/apis/gtest/vtest/rtest/normal_delete", + }, + { + namespace: "nstest", + name: "namespaced_delete", + path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete", + }, + { + namespace: "nstest", + name: "namespaced_delete_with_options", + path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete_with_options", + deleteOptions: &metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &uid}, PropagationPolicy: &background}, + }, + } + for _, tc := range tcs { + gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} + resource := &metav1.APIResource{Name: "rtest", Namespaced: len(tc.namespace) != 0} + cl, srv, err := getClientServer(gv, func(w http.ResponseWriter, r *http.Request) { + if r.Method != "DELETE" { + t.Errorf("Delete(%q) got HTTP method %s. wanted DELETE", tc.name, r.Method) + } + + if r.URL.Path != tc.path { + t.Errorf("Delete(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) + } + + w.Header().Set("Content-Type", runtime.ContentTypeJSON) + unstructured.UnstructuredJSONScheme.Encode(statusOK, w) + }) + if err != nil { + t.Errorf("unexpected error when creating client: %v", err) + continue + } + defer srv.Close() + + err = cl.Resource(resource, tc.namespace).Delete(tc.name, tc.deleteOptions) + if err != nil { + t.Errorf("unexpected error when deleting %q: %v", tc.name, err) + continue + } + } +} + +func TestDeleteCollection(t *testing.T) { + statusOK := &metav1.Status{ + TypeMeta: metav1.TypeMeta{Kind: "Status"}, + Status: metav1.StatusSuccess, + } + tcs := []struct { + namespace string + name string + path string + }{ + { + name: "normal_delete_collection", + path: "/apis/gtest/vtest/rtest", + }, + { + namespace: "nstest", + name: "namespaced_delete_collection", + path: "/apis/gtest/vtest/namespaces/nstest/rtest", + }, + } + for _, tc := range tcs { + gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} + resource := &metav1.APIResource{Name: "rtest", Namespaced: len(tc.namespace) != 0} + cl, srv, err := getClientServer(gv, func(w http.ResponseWriter, r *http.Request) { + if r.Method != "DELETE" { + t.Errorf("DeleteCollection(%q) got HTTP method %s. wanted DELETE", tc.name, r.Method) + } + + if r.URL.Path != tc.path { + t.Errorf("DeleteCollection(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) + } + + w.Header().Set("Content-Type", runtime.ContentTypeJSON) + unstructured.UnstructuredJSONScheme.Encode(statusOK, w) + }) + if err != nil { + t.Errorf("unexpected error when creating client: %v", err) + continue + } + defer srv.Close() + + err = cl.Resource(resource, tc.namespace).DeleteCollection(nil, metav1.ListOptions{}) + if err != nil { + t.Errorf("unexpected error when deleting collection %q: %v", tc.name, err) + continue + } + } +} + +func TestCreate(t *testing.T) { + tcs := []struct { + resource string + name string + namespace string + obj *unstructured.Unstructured + path string + }{ + { + resource: "rtest", + name: "normal_create", + path: "/apis/gtest/vtest/rtest", + obj: getObject("gtest/vTest", "rTest", "normal_create"), + }, + { + resource: "rtest", + name: "namespaced_create", + namespace: "nstest", + path: "/apis/gtest/vtest/namespaces/nstest/rtest", + obj: getObject("gtest/vTest", "rTest", "namespaced_create"), + }, + } + for _, tc := range tcs { + gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} + 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) + } + + if r.URL.Path != tc.path { + t.Errorf("Create(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) + } + + w.Header().Set("Content-Type", runtime.ContentTypeJSON) + data, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Create(%q) unexpected error reading body: %v", tc.name, err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(data) + }) + if err != nil { + t.Errorf("unexpected error when creating client: %v", err) + continue + } + defer srv.Close() + + got, err := cl.Resource(resource, tc.namespace).Create(tc.obj) + if err != nil { + t.Errorf("unexpected error when creating %q: %v", tc.name, err) + continue + } + + if !reflect.DeepEqual(got, tc.obj) { + t.Errorf("Create(%q) want: %v\ngot: %v", tc.name, tc.obj, got) + } + } +} + +func TestUpdate(t *testing.T) { + tcs := []struct { + resource string + name string + namespace string + obj *unstructured.Unstructured + path string + }{ + { + resource: "rtest", + name: "normal_update", + path: "/apis/gtest/vtest/rtest/normal_update", + obj: getObject("gtest/vTest", "rTest", "normal_update"), + }, + { + resource: "rtest", + name: "namespaced_update", + namespace: "nstest", + path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_update", + obj: getObject("gtest/vTest", "rTest", "namespaced_update"), + }, + { + resource: "rtest/srtest", + name: "normal_subresource_update", + path: "/apis/gtest/vtest/rtest/normal_update/srtest", + obj: getObject("gtest/vTest", "srTest", "normal_update"), + }, + { + resource: "rtest/srtest", + name: "namespaced_subresource_update", + namespace: "nstest", + path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_update/srtest", + obj: getObject("gtest/vTest", "srTest", "namespaced_update"), + }, + } + for _, tc := range tcs { + gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} + 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) + } + + if r.URL.Path != tc.path { + t.Errorf("Update(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) + } + + w.Header().Set("Content-Type", runtime.ContentTypeJSON) + data, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Update(%q) unexpected error reading body: %v", tc.name, err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(data) + }) + if err != nil { + t.Errorf("unexpected error when creating client: %v", err) + continue + } + defer srv.Close() + + got, err := cl.Resource(resource, tc.namespace).Update(tc.obj) + if err != nil { + t.Errorf("unexpected error when updating %q: %v", tc.name, err) + continue + } + + if !reflect.DeepEqual(got, tc.obj) { + t.Errorf("Update(%q) want: %v\ngot: %v", tc.name, tc.obj, got) + } + } +} + +func TestWatch(t *testing.T) { + tcs := []struct { + name string + namespace string + events []watch.Event + path string + query string + }{ + { + name: "normal_watch", + path: "/apis/gtest/vtest/rtest", + query: "watch=true", + events: []watch.Event{ + {Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "normal_watch")}, + {Type: watch.Modified, Object: getObject("gtest/vTest", "rTest", "normal_watch")}, + {Type: watch.Deleted, Object: getObject("gtest/vTest", "rTest", "normal_watch")}, + }, + }, + { + name: "namespaced_watch", + namespace: "nstest", + path: "/apis/gtest/vtest/namespaces/nstest/rtest", + query: "watch=true", + events: []watch.Event{ + {Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")}, + {Type: watch.Modified, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")}, + {Type: watch.Deleted, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")}, + }, + }, + } + for _, tc := range tcs { + gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} + resource := &metav1.APIResource{Name: "rtest", Namespaced: len(tc.namespace) != 0} + cl, srv, err := getClientServer(gv, func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Watch(%q) got HTTP method %s. wanted GET", tc.name, r.Method) + } + + if r.URL.Path != tc.path { + t.Errorf("Watch(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) + } + if r.URL.RawQuery != tc.query { + t.Errorf("Watch(%q) got query %s. wanted %s", tc.name, r.URL.RawQuery, tc.query) + } + + enc := restclientwatch.NewEncoder(streaming.NewEncoder(w, dynamicCodec{}), dynamicCodec{}) + for _, e := range tc.events { + enc.Encode(&e) + } + }) + if err != nil { + t.Errorf("unexpected error when creating client: %v", err) + continue + } + defer srv.Close() + + watcher, err := cl.Resource(resource, tc.namespace).Watch(metav1.ListOptions{}) + if err != nil { + t.Errorf("unexpected error when watching %q: %v", tc.name, err) + continue + } + + for _, want := range tc.events { + got := <-watcher.ResultChan() + if !reflect.DeepEqual(got, want) { + t.Errorf("Watch(%q) want: %v\ngot: %v", tc.name, want, got) + } + } + } +} + +func TestPatch(t *testing.T) { + tcs := []struct { + resource string + name string + namespace string + patch []byte + want *unstructured.Unstructured + path string + }{ + { + resource: "rtest", + name: "normal_patch", + path: "/apis/gtest/vtest/rtest/normal_patch", + patch: getJSON("gtest/vTest", "rTest", "normal_patch"), + want: getObject("gtest/vTest", "rTest", "normal_patch"), + }, + { + resource: "rtest", + name: "namespaced_patch", + namespace: "nstest", + path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_patch", + patch: getJSON("gtest/vTest", "rTest", "namespaced_patch"), + want: getObject("gtest/vTest", "rTest", "namespaced_patch"), + }, + { + resource: "rtest/srtest", + name: "normal_subresource_patch", + path: "/apis/gtest/vtest/rtest/normal_subresource_patch/srtest", + patch: getJSON("gtest/vTest", "srTest", "normal_subresource_patch"), + want: getObject("gtest/vTest", "srTest", "normal_subresource_patch"), + }, + { + resource: "rtest/srtest", + name: "namespaced_subresource_patch", + namespace: "nstest", + path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_patch/srtest", + patch: getJSON("gtest/vTest", "srTest", "namespaced_subresource_patch"), + want: getObject("gtest/vTest", "srTest", "namespaced_subresource_patch"), + }, + } + for _, tc := range tcs { + gv := &schema.GroupVersion{Group: "gtest", Version: "vtest"} + 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) + } + + if r.URL.Path != tc.path { + t.Errorf("Patch(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) + } + + content := r.Header.Get("Content-Type") + if content != string(types.StrategicMergePatchType) { + t.Errorf("Patch(%q) got Content-Type %s. wanted %s", tc.name, content, types.StrategicMergePatchType) + } + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Patch(%q) unexpected error reading body: %v", tc.name, err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(data) + }) + if err != nil { + t.Errorf("unexpected error when creating client: %v", err) + continue + } + defer srv.Close() + + got, err := cl.Resource(resource, tc.namespace).Patch(tc.name, types.StrategicMergePatchType, tc.patch) + if err != nil { + t.Errorf("unexpected error when patching %q: %v", tc.name, err) + continue + } + + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("Patch(%q) want: %v\ngot: %v", tc.name, tc.want, got) + } + } +} diff --git a/dynamic/dynamic_util.go b/dynamic/dynamic_util.go deleted file mode 100644 index 570f9f17..00000000 --- a/dynamic/dynamic_util.go +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package dynamic - -import ( - "fmt" - - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -// NewDiscoveryRESTMapper returns a RESTMapper based on discovery information. -func NewDiscoveryRESTMapper(resources []*metav1.APIResourceList) (*meta.DefaultRESTMapper, error) { - rm := meta.NewDefaultRESTMapper(nil) - for _, resourceList := range resources { - gv, err := schema.ParseGroupVersion(resourceList.GroupVersion) - if err != nil { - return nil, err - } - - for _, resource := range resourceList.APIResources { - gvk := gv.WithKind(resource.Kind) - scope := meta.RESTScopeRoot - if resource.Namespaced { - scope = meta.RESTScopeNamespace - } - rm.Add(gvk, scope) - } - } - return rm, nil -} - -// ObjectTyper provides an ObjectTyper implementation for -// unstructured.Unstructured object based on discovery information. -type ObjectTyper struct { - registered map[schema.GroupVersionKind]bool -} - -// NewObjectTyper constructs an ObjectTyper from discovery information. -func NewObjectTyper(resources []*metav1.APIResourceList) (runtime.ObjectTyper, error) { - ot := &ObjectTyper{registered: make(map[schema.GroupVersionKind]bool)} - for _, resourceList := range resources { - gv, err := schema.ParseGroupVersion(resourceList.GroupVersion) - if err != nil { - return nil, err - } - - for _, resource := range resourceList.APIResources { - ot.registered[gv.WithKind(resource.Kind)] = true - } - } - return ot, nil -} - -// ObjectKinds returns a slice of one element with the -// group,version,kind of the provided object, or an error if the -// object is not *unstructured.Unstructured or has no group,version,kind -// information. -func (ot *ObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) { - if _, ok := obj.(*unstructured.Unstructured); !ok { - return nil, false, fmt.Errorf("type %T is invalid for determining dynamic object types", obj) - } - return []schema.GroupVersionKind{obj.GetObjectKind().GroupVersionKind()}, false, nil -} - -// Recognizes returns true if the provided group,version,kind was in -// the discovery information. -func (ot *ObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool { - return ot.registered[gvk] -} diff --git a/dynamic/dynamic_util_test.go b/dynamic/dynamic_util_test.go deleted file mode 100644 index 37113d4a..00000000 --- a/dynamic/dynamic_util_test.go +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package dynamic - -import ( - "testing" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -func TestDiscoveryRESTMapper(t *testing.T) { - resources := []*metav1.APIResourceList{ - { - GroupVersion: "test/beta1", - APIResources: []metav1.APIResource{ - { - Name: "test_kinds", - Namespaced: true, - Kind: "test_kind", - }, - }, - }, - } - - gvk := schema.GroupVersionKind{ - Group: "test", - Version: "beta1", - Kind: "test_kind", - } - - mapper, err := NewDiscoveryRESTMapper(resources) - if err != nil { - t.Fatalf("unexpected error creating mapper: %s", err) - } - - for _, res := range []schema.GroupVersionResource{ - { - Group: "test", - Version: "beta1", - Resource: "test_kinds", - }, - { - Version: "beta1", - Resource: "test_kinds", - }, - { - Group: "test", - Resource: "test_kinds", - }, - { - Resource: "test_kinds", - }, - } { - got, err := mapper.KindFor(res) - if err != nil { - t.Errorf("KindFor(%#v) unexpected error: %s", res, err) - continue - } - - if got != gvk { - t.Errorf("KindFor(%#v) = %#v; want %#v", res, got, gvk) - } - } -} diff --git a/dynamic/fake/client.go b/dynamic/fake/client.go deleted file mode 100644 index 8399076c..00000000 --- a/dynamic/fake/client.go +++ /dev/null @@ -1,163 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package fake provides a fake client interface to arbitrary Kubernetes -// APIs that exposes common high level operations and exposes common -// metadata. -package fake - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/testing" - "k8s.io/client-go/util/flowcontrol" -) - -// FakeClient is a fake implementation of dynamic.Interface. -type FakeClient struct { - GroupVersion schema.GroupVersion - - *testing.Fake -} - -// GetRateLimiter returns the rate limiter for this client. -func (c *FakeClient) GetRateLimiter() flowcontrol.RateLimiter { - return nil -} - -// Resource returns an API interface to the specified resource for this client's -// group and version. If resource is not a namespaced resource, then namespace -// is ignored. The ResourceClient inherits the parameter codec of this client -func (c *FakeClient) Resource(resource *metav1.APIResource, namespace string) dynamic.ResourceInterface { - return &FakeResourceClient{ - Resource: c.GroupVersion.WithResource(resource.Name), - Kind: c.GroupVersion.WithKind(resource.Kind), - Namespace: namespace, - - Fake: c.Fake, - } -} - -// ParameterCodec returns a client with the provided parameter codec. -func (c *FakeClient) ParameterCodec(parameterCodec runtime.ParameterCodec) dynamic.Interface { - return &FakeClient{ - Fake: c.Fake, - } -} - -// FakeResourceClient is a fake implementation of dynamic.ResourceInterface -type FakeResourceClient struct { - Resource schema.GroupVersionResource - Kind schema.GroupVersionKind - Namespace string - - *testing.Fake -} - -// List returns a list of objects for this resource. -func (c *FakeResourceClient) List(opts metav1.ListOptions) (runtime.Object, error) { - obj, err := c.Fake. - Invokes(testing.NewListAction(c.Resource, c.Kind, c.Namespace, opts), &unstructured.UnstructuredList{}) - - if obj == nil { - return nil, err - } - - label, _, _ := testing.ExtractFromListOptions(opts) - if label == nil { - label = labels.Everything() - } - list := &unstructured.UnstructuredList{} - for _, item := range obj.(*unstructured.UnstructuredList).Items { - if label.Matches(labels.Set(item.GetLabels())) { - list.Items = append(list.Items, item) - } - } - return list, err -} - -// Get gets the resource with the specified name. -func (c *FakeResourceClient) Get(name string, opts metav1.GetOptions) (*unstructured.Unstructured, error) { - obj, err := c.Fake. - Invokes(testing.NewGetAction(c.Resource, c.Namespace, name), &unstructured.Unstructured{}) - - if obj == nil { - return nil, err - } - - return obj.(*unstructured.Unstructured), err -} - -// Delete deletes the resource with the specified name. -func (c *FakeResourceClient) Delete(name string, opts *metav1.DeleteOptions) error { - _, err := c.Fake. - Invokes(testing.NewDeleteAction(c.Resource, c.Namespace, name), &unstructured.Unstructured{}) - - return err -} - -// DeleteCollection deletes a collection of objects. -func (c *FakeResourceClient) DeleteCollection(deleteOptions *metav1.DeleteOptions, listOptions metav1.ListOptions) error { - _, err := c.Fake. - Invokes(testing.NewDeleteCollectionAction(c.Resource, c.Namespace, listOptions), &unstructured.Unstructured{}) - - return err -} - -// Create creates the provided resource. -func (c *FakeResourceClient) Create(inObj *unstructured.Unstructured) (*unstructured.Unstructured, error) { - obj, err := c.Fake. - Invokes(testing.NewCreateAction(c.Resource, c.Namespace, inObj), &unstructured.Unstructured{}) - - if obj == nil { - return nil, err - } - return obj.(*unstructured.Unstructured), err -} - -// Update updates the provided resource. -func (c *FakeResourceClient) Update(inObj *unstructured.Unstructured) (*unstructured.Unstructured, error) { - obj, err := c.Fake. - Invokes(testing.NewUpdateAction(c.Resource, c.Namespace, inObj), &unstructured.Unstructured{}) - - if obj == nil { - return nil, err - } - return obj.(*unstructured.Unstructured), err -} - -// Watch returns a watch.Interface that watches the resource. -func (c *FakeResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, error) { - return c.Fake. - InvokesWatch(testing.NewWatchAction(c.Resource, c.Namespace, opts)) -} - -// Patch patches the provided resource. -func (c *FakeResourceClient) Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error) { - obj, err := c.Fake. - Invokes(testing.NewPatchAction(c.Resource, c.Namespace, name, data), &unstructured.Unstructured{}) - - if obj == nil { - return nil, err - } - return obj.(*unstructured.Unstructured), err -} diff --git a/dynamic/fake/client_pool.go b/dynamic/fake/client_pool.go deleted file mode 100644 index 7ec11489..00000000 --- a/dynamic/fake/client_pool.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package fake provides a fake client interface to arbitrary Kubernetes -// APIs that exposes common high level operations and exposes common -// metadata. -package fake - -import ( - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/testing" -) - -// FakeClientPool provides a fake implementation of dynamic.ClientPool. -// It assumes resource GroupVersions are the same as their corresponding kind GroupVersions. -type FakeClientPool struct { - testing.Fake -} - -// ClientForGroupVersionKind returns a client configured for the specified groupVersionResource. -// Resource may be empty. -func (p *FakeClientPool) ClientForGroupVersionResource(resource schema.GroupVersionResource) (dynamic.Interface, error) { - return p.ClientForGroupVersionKind(resource.GroupVersion().WithKind("")) -} - -// ClientForGroupVersionKind returns a client configured for the specified groupVersionKind. -// Kind may be empty. -func (p *FakeClientPool) ClientForGroupVersionKind(kind schema.GroupVersionKind) (dynamic.Interface, error) { - // we can just create a new client every time for testing purposes - return &FakeClient{ - GroupVersion: kind.GroupVersion(), - Fake: &p.Fake, - }, nil -} diff --git a/dynamic/interface.go b/dynamic/interface.go new file mode 100644 index 00000000..4503af6b --- /dev/null +++ b/dynamic/interface.go @@ -0,0 +1,34 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dynamic + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// APIPathResolverFunc knows how to convert a groupVersion to its API path. The Kind field is optional. +// TODO find a better place to move this for existing callers +type APIPathResolverFunc func(kind schema.GroupVersionKind) string + +// LegacyAPIPathResolverFunc can resolve paths properly with the legacy API. +// TODO find a better place to move this for existing callers +func LegacyAPIPathResolverFunc(kind schema.GroupVersionKind) string { + if len(kind.Group) == 0 { + return "/api" + } + return "/apis" +}