diff --git a/pkg/client/typed/dynamic/client.go b/pkg/client/typed/dynamic/client.go new file mode 100644 index 0000000000..bd95432cc8 --- /dev/null +++ b/pkg/client/typed/dynamic/client.go @@ -0,0 +1,217 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 provides a client interface to arbitrary Kubernetes +// APIs that exposes common high level operations and exposes common +// metadata. +package dynamic + +import ( + "encoding/json" + "errors" + "io" + "net/url" + "strings" + + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/api/v1" + client "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/conversion/queryparams" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/watch" +) + +// Client is a Kubernetes client that allows you to access metadata +// and manipulate metadata of a Kubernetes API group. +type Client struct { + cl *client.RESTClient +} + +// 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 *client.Config) (*Client, error) { + // avoid changing the original config + confCopy := *conf + conf = &confCopy + + conf.Codec = dynamicCodec{} + + if conf.APIPath == "" { + conf.APIPath = "/api" + } + + if len(conf.UserAgent) == 0 { + conf.UserAgent = client.DefaultKubernetesUserAgent() + } + + if conf.QPS == 0.0 { + conf.QPS = 5.0 + } + if conf.Burst == 0 { + conf.Burst = 10 + } + + cl, err := client.RESTClientFor(conf) + if err != nil { + return nil, err + } + + return &Client{cl: cl}, 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. +func (c *Client) Resource(resource *unversioned.APIResource, namespace string) *ResourceClient { + return &ResourceClient{ + cl: c.cl, + resource: resource, + ns: namespace, + } +} + +// ResourceClient is an API interface to a specific resource under a +// dynamic client. +type ResourceClient struct { + cl *client.RESTClient + resource *unversioned.APIResource + ns string +} + +// namespace applies a namespace to the request if the configured +// resource is a namespaced resource. Otherwise, it just returns the +// passed in request. +func (rc *ResourceClient) namespace(req *client.Request) *client.Request { + if rc.resource.Namespaced { + return req.Namespace(rc.ns) + } + return req +} + +// List returns a list of objects for this resource. +func (rc *ResourceClient) List(opts v1.ListOptions) (*runtime.UnstructuredList, error) { + result := new(runtime.UnstructuredList) + err := rc.namespace(rc.cl.Get()). + Resource(rc.resource.Name). + VersionedParams(&opts, parameterEncoder). + Do(). + Into(result) + return result, err +} + +// Get gets the resource with the specified name. +func (rc *ResourceClient) Get(name string) (*runtime.Unstructured, error) { + result := new(runtime.Unstructured) + err := rc.namespace(rc.cl.Get()). + Resource(rc.resource.Name). + Name(name). + Do(). + Into(result) + return result, err +} + +// Delete deletes the resource with the specified name. +func (rc *ResourceClient) Delete(name string, opts *v1.DeleteOptions) error { + return rc.namespace(rc.cl.Delete()). + Resource(rc.resource.Name). + Name(name). + Body(opts). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (rc *ResourceClient) DeleteCollection(deleteOptions *v1.DeleteOptions, listOptions v1.ListOptions) error { + return rc.namespace(rc.cl.Delete()). + Resource(rc.resource.Name). + VersionedParams(&listOptions, parameterEncoder). + Body(deleteOptions). + Do(). + Error() +} + +// Create creates the provided resource. +func (rc *ResourceClient) Create(obj *runtime.Unstructured) (*runtime.Unstructured, error) { + result := new(runtime.Unstructured) + err := rc.namespace(rc.cl.Post()). + Resource(rc.resource.Name). + Body(obj). + Do(). + Into(result) + return result, err +} + +// Update updates the provided resource. +func (rc *ResourceClient) Update(obj *runtime.Unstructured) (*runtime.Unstructured, error) { + result := new(runtime.Unstructured) + if len(obj.Name) == 0 { + return result, errors.New("object missing name") + } + err := rc.namespace(rc.cl.Put()). + Resource(rc.resource.Name). + Name(obj.Name). + Body(obj). + Do(). + Into(result) + return result, err +} + +// Watch returns a watch.Interface that watches the resource. +func (rc *ResourceClient) Watch(opts v1.ListOptions) (watch.Interface, error) { + return rc.namespace(rc.cl.Get().Prefix("watch")). + Resource(rc.resource.Name). + VersionedParams(&opts, parameterEncoder). + Watch() +} + +// dynamicCodec is a codec that wraps the standard unstructured codec +// with special handling for Status objects. +type dynamicCodec struct{} + +func (dynamicCodec) Decode(data []byte, gvk *unversioned.GroupVersionKind, obj runtime.Object) (runtime.Object, *unversioned.GroupVersionKind, error) { + obj, gvk, err := runtime.UnstructuredJSONScheme.Decode(data, gvk, obj) + if err != nil { + return nil, nil, err + } + + if _, ok := obj.(*unversioned.Status); !ok && strings.ToLower(gvk.Kind) == "status" { + obj = &unversioned.Status{} + err := json.Unmarshal(data, obj) + if err != nil { + return nil, nil, err + } + } + + return obj, gvk, nil +} + +func (dynamicCodec) EncodeToStream(obj runtime.Object, w io.Writer, overrides ...unversioned.GroupVersion) error { + return runtime.UnstructuredJSONScheme.EncodeToStream(obj, w, overrides...) +} + +// paramaterCodec is a codec converts an API object to query +// parameters without trying to convert to the target version. +type parameterCodec struct{} + +func (parameterCodec) EncodeParameters(obj runtime.Object, to unversioned.GroupVersion) (url.Values, error) { + return queryparams.Convert(obj) +} + +func (parameterCodec) DecodeParameters(parameters url.Values, from unversioned.GroupVersion, into runtime.Object) error { + return errors.New("DecodeParameters not implemented on dynamic parameterCodec") +} + +var parameterEncoder runtime.ParameterCodec = parameterCodec{} diff --git a/pkg/client/typed/dynamic/client_test.go b/pkg/client/typed/dynamic/client_test.go new file mode 100644 index 0000000000..7632c559a4 --- /dev/null +++ b/pkg/client/typed/dynamic/client_test.go @@ -0,0 +1,481 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/api/v1" + client "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/watch" + watchjson "k8s.io/kubernetes/pkg/watch/json" +) + +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) *runtime.Unstructured { + return &runtime.Unstructured{ + TypeMeta: runtime.TypeMeta{ + APIVersion: version, + Kind: kind, + }, + Name: name, + Object: map[string]interface{}{ + "apiVersion": version, + "kind": kind, + "metadata": map[string]interface{}{ + "name": name, + }, + }, + } +} + +func getClientServer(gv *unversioned.GroupVersion, h func(http.ResponseWriter, *http.Request)) (*Client, *httptest.Server, error) { + srv := httptest.NewServer(http.HandlerFunc(h)) + cl, err := NewClient(&client.Config{ + Host: srv.URL, + ContentConfig: client.ContentConfig{GroupVersion: 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 *runtime.UnstructuredList + }{ + { + name: "normal_list", + path: "/api/gtest/vtest/rtest", + resp: getListJSON("vTest", "rTestList", + getJSON("vTest", "rTest", "item1"), + getJSON("vTest", "rTest", "item2")), + want: &runtime.UnstructuredList{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "vTest", + Kind: "rTestList", + }, + Items: []*runtime.Unstructured{ + getObject("vTest", "rTest", "item1"), + getObject("vTest", "rTest", "item2"), + }, + }, + }, + { + name: "namespaced_list", + namespace: "nstest", + path: "/api/gtest/vtest/namespaces/nstest/rtest", + resp: getListJSON("vTest", "rTestList", + getJSON("vTest", "rTest", "item1"), + getJSON("vTest", "rTest", "item2")), + want: &runtime.UnstructuredList{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "vTest", + Kind: "rTestList", + }, + Items: []*runtime.Unstructured{ + getObject("vTest", "rTest", "item1"), + getObject("vTest", "rTest", "item2"), + }, + }, + }, + } + for _, tc := range tcs { + gv := &unversioned.GroupVersion{Group: "gtest", Version: "vtest"} + resource := &unversioned.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.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(v1.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 { + namespace string + name string + path string + resp []byte + want *runtime.Unstructured + }{ + { + name: "normal_get", + path: "/api/gtest/vtest/rtest/normal_get", + resp: getJSON("vTest", "rTest", "normal_get"), + want: getObject("vTest", "rTest", "normal_get"), + }, + { + 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"), + }, + } + for _, tc := range tcs { + gv := &unversioned.GroupVersion{Group: "gtest", Version: "vtest"} + resource := &unversioned.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("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.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) + 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) { + statusOK := &unversioned.Status{ + TypeMeta: unversioned.TypeMeta{Kind: "Status"}, + Status: unversioned.StatusSuccess, + } + tcs := []struct { + namespace string + name string + path string + }{ + { + name: "normal_delete", + path: "/api/gtest/vtest/rtest/normal_delete", + }, + { + namespace: "nstest", + name: "namespaced_delete", + path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_delete", + }, + } + for _, tc := range tcs { + gv := &unversioned.GroupVersion{Group: "gtest", Version: "vtest"} + resource := &unversioned.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) + } + + runtime.UnstructuredJSONScheme.EncodeToStream(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, nil) + if err != nil { + t.Errorf("unexpected error when deleting %q: %v", tc.name, err) + continue + } + } +} + +func TestDeleteCollection(t *testing.T) { + statusOK := &unversioned.Status{ + TypeMeta: unversioned.TypeMeta{Kind: "Status"}, + Status: unversioned.StatusSuccess, + } + tcs := []struct { + namespace string + name string + path string + }{ + { + name: "normal_delete_collection", + path: "/api/gtest/vtest/rtest", + }, + { + namespace: "nstest", + name: "namespaced_delete_collection", + path: "/api/gtest/vtest/namespaces/nstest/rtest", + }, + } + for _, tc := range tcs { + gv := &unversioned.GroupVersion{Group: "gtest", Version: "vtest"} + resource := &unversioned.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) + } + + runtime.UnstructuredJSONScheme.EncodeToStream(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, v1.ListOptions{}) + if err != nil { + t.Errorf("unexpected error when deleting collection %q: %v", tc.name, err) + continue + } + } +} + +func TestCreate(t *testing.T) { + tcs := []struct { + name string + namespace string + obj *runtime.Unstructured + path string + }{ + { + name: "normal_create", + path: "/api/gtest/vtest/rtest", + obj: getObject("vTest", "rTest", "normal_create"), + }, + { + name: "namespaced_create", + namespace: "nstest", + path: "/api/gtest/vtest/namespaces/nstest/rtest", + obj: getObject("vTest", "rTest", "namespaced_create"), + }, + } + for _, tc := range tcs { + gv := &unversioned.GroupVersion{Group: "gtest", Version: "vtest"} + resource := &unversioned.APIResource{Name: "rtest", 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) + } + + 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 { + name string + namespace string + obj *runtime.Unstructured + path string + }{ + { + name: "normal_update", + path: "/api/gtest/vtest/rtest/normal_update", + obj: getObject("vTest", "rTest", "normal_update"), + }, + { + name: "namespaced_update", + namespace: "nstest", + path: "/api/gtest/vtest/namespaces/nstest/rtest/namespaced_update", + obj: getObject("vTest", "rTest", "namespaced_update"), + }, + } + for _, tc := range tcs { + gv := &unversioned.GroupVersion{Group: "gtest", Version: "vtest"} + resource := &unversioned.APIResource{Name: "rtest", 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) + } + + 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 + }{ + { + name: "normal_watch", + path: "/api/gtest/vtest/watch/rtest", + events: []watch.Event{ + {Type: watch.Added, Object: getObject("vTest", "rTest", "normal_watch")}, + {Type: watch.Modified, Object: getObject("vTest", "rTest", "normal_watch")}, + {Type: watch.Deleted, Object: getObject("vTest", "rTest", "normal_watch")}, + }, + }, + { + name: "namespaced_watch", + namespace: "nstest", + path: "/api/gtest/vtest/watch/namespaces/nstest/rtest", + events: []watch.Event{ + {Type: watch.Added, Object: getObject("vTest", "rTest", "namespaced_watch")}, + {Type: watch.Modified, Object: getObject("vTest", "rTest", "namespaced_watch")}, + {Type: watch.Deleted, Object: getObject("vTest", "rTest", "namespaced_watch")}, + }, + }, + } + for _, tc := range tcs { + gv := &unversioned.GroupVersion{Group: "gtest", Version: "vtest"} + resource := &unversioned.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) + } + + enc := watchjson.NewEncoder(w, 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(v1.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) + } + } + } +} diff --git a/pkg/runtime/register.go b/pkg/runtime/register.go index 95244913c1..ec58b345de 100644 --- a/pkg/runtime/register.go +++ b/pkg/runtime/register.go @@ -30,8 +30,9 @@ func (obj *TypeMeta) GroupVersionKind() *unversioned.GroupVersionKind { return unversioned.FromAPIVersionAndKind(obj.APIVersion, obj.Kind) } -func (obj *Unknown) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } -func (obj *Unstructured) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *Unknown) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *Unstructured) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *UnstructuredList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } // GetObjectKind implements Object for VersionedObjects, returning an empty ObjectKind // interface if no objects are provided, or the ObjectKind interface of the object in the diff --git a/pkg/runtime/scheme.go b/pkg/runtime/scheme.go index 37bd985aad..1b9fc845a7 100644 --- a/pkg/runtime/scheme.go +++ b/pkg/runtime/scheme.go @@ -474,7 +474,7 @@ func (s *Scheme) ConvertToVersion(in Object, outVersion string) (Object, error) return nil, err } switch in.(type) { - case *Unknown, *Unstructured: + case *Unknown, *Unstructured, *UnstructuredList: old := in.GetObjectKind().GroupVersionKind() defer in.GetObjectKind().SetGroupVersionKind(old) setTargetVersion(in, s, gv) diff --git a/pkg/runtime/types.go b/pkg/runtime/types.go index 3b8cede446..7d55f4e5b9 100644 --- a/pkg/runtime/types.go +++ b/pkg/runtime/types.go @@ -109,11 +109,25 @@ type Unknown struct { // metadata and field mutatation. type Unstructured struct { TypeMeta `json:",inline"` + + // Name is populated from metadata (if present) upon deserialization + Name string + // Object is a JSON compatible map with string, float, int, []interface{}, or map[string]interface{} // children. Object map[string]interface{} } +// UnstructuredList allows lists that do not have Golang structs +// registered to be manipulated generically. This can be used to deal +// with the API lists from a plug-in. +type UnstructuredList struct { + TypeMeta `json:",inline"` + + // Items is a list of unstructured objects. + Items []*Unstructured `json:"items"` +} + // VersionedObjects is used by Decoders to give callers a way to access all versions // of an object during the decoding process. type VersionedObjects struct { diff --git a/pkg/runtime/unstructured.go b/pkg/runtime/unstructured.go index 59dfa2458c..e4cdef8c92 100644 --- a/pkg/runtime/unstructured.go +++ b/pkg/runtime/unstructured.go @@ -30,13 +30,87 @@ var UnstructuredJSONScheme Codec = unstructuredJSONScheme{} type unstructuredJSONScheme struct{} -func (s unstructuredJSONScheme) Decode(data []byte, _ *unversioned.GroupVersionKind, _ Object) (Object, *unversioned.GroupVersionKind, error) { - unstruct := &Unstructured{} +func (s unstructuredJSONScheme) Decode(data []byte, _ *unversioned.GroupVersionKind, obj Object) (Object, *unversioned.GroupVersionKind, error) { + var err error + if obj != nil { + err = s.decodeInto(data, obj) + } else { + obj, err = s.decode(data) + } - m := make(map[string]interface{}) - if err := json.Unmarshal(data, &m); err != nil { + if err != nil { return nil, nil, err } + + gvk := obj.GetObjectKind().GroupVersionKind() + if len(gvk.Kind) == 0 { + return nil, gvk, NewMissingKindErr(string(data)) + } + + return obj, gvk, nil +} + +func (unstructuredJSONScheme) EncodeToStream(obj Object, w io.Writer, overrides ...unversioned.GroupVersion) error { + switch t := obj.(type) { + case *Unstructured: + return json.NewEncoder(w).Encode(t.Object) + case *UnstructuredList: + type encodeList struct { + TypeMeta `json:",inline"` + Items []map[string]interface{} `json:"items"` + } + eList := encodeList{ + TypeMeta: t.TypeMeta, + } + for _, i := range t.Items { + eList.Items = append(eList.Items, i.Object) + } + return json.NewEncoder(w).Encode(eList) + case *Unknown: + _, err := w.Write(t.RawJSON) + return err + default: + return json.NewEncoder(w).Encode(t) + } +} + +func (s unstructuredJSONScheme) decode(data []byte) (Object, error) { + type detector struct { + Items json.RawMessage + } + var det detector + if err := json.Unmarshal(data, &det); err != nil { + return nil, err + } + + if det.Items != nil { + list := &UnstructuredList{} + err := s.decodeToList(data, list) + return list, err + } + + // No Items field, so it wasn't a list. + unstruct := &Unstructured{} + err := s.decodeToUnstructured(data, unstruct) + return unstruct, err +} +func (s unstructuredJSONScheme) decodeInto(data []byte, obj Object) error { + switch x := obj.(type) { + case *Unstructured: + return s.decodeToUnstructured(data, x) + case *UnstructuredList: + return s.decodeToList(data, x) + default: + return json.Unmarshal(data, x) + } +} + +func (unstructuredJSONScheme) decodeToUnstructured(data []byte, unstruct *Unstructured) error { + m := make(map[string]interface{}) + if err := json.Unmarshal(data, &m); err != nil { + return err + } + if v, ok := m["kind"]; ok { if s, ok := v.(string); ok { unstruct.Kind = s @@ -47,30 +121,39 @@ func (s unstructuredJSONScheme) Decode(data []byte, _ *unversioned.GroupVersionK unstruct.APIVersion = s } } - - if len(unstruct.APIVersion) == 0 { - return nil, nil, NewMissingVersionErr(string(data)) - } - gv, err := unversioned.ParseGroupVersion(unstruct.APIVersion) - if err != nil { - return nil, nil, err - } - gvk := gv.WithKind(unstruct.Kind) - if len(unstruct.Kind) == 0 { - return nil, &gvk, NewMissingKindErr(string(data)) + if metadata, ok := m["metadata"]; ok { + if metadata, ok := metadata.(map[string]interface{}); ok { + if name, ok := metadata["name"]; ok { + if name, ok := name.(string); ok { + unstruct.Name = name + } + } + } } unstruct.Object = m - return unstruct, &gvk, nil + + return nil } -func (s unstructuredJSONScheme) EncodeToStream(obj Object, w io.Writer, overrides ...unversioned.GroupVersion) error { - switch t := obj.(type) { - case *Unstructured: - return json.NewEncoder(w).Encode(t.Object) - case *Unknown: - _, err := w.Write(t.RawJSON) - return err - default: - return json.NewEncoder(w).Encode(t) +func (s unstructuredJSONScheme) decodeToList(data []byte, list *UnstructuredList) error { + type decodeList struct { + TypeMeta `json:",inline"` + Items []json.RawMessage } + + var dList decodeList + if err := json.Unmarshal(data, &dList); err != nil { + return err + } + + list.TypeMeta = dList.TypeMeta + list.Items = nil + for _, i := range dList.Items { + unstruct := &Unstructured{} + if err := s.decodeToUnstructured([]byte(i), unstruct); err != nil { + return err + } + list.Items = append(list.Items, unstruct) + } + return nil } diff --git a/pkg/runtime/unstructured_test.go b/pkg/runtime/unstructured_test.go index cca0fe2511..ea1d874402 100644 --- a/pkg/runtime/unstructured_test.go +++ b/pkg/runtime/unstructured_test.go @@ -18,6 +18,7 @@ package runtime_test import ( "fmt" + "reflect" "testing" "k8s.io/kubernetes/pkg/api" @@ -46,3 +47,77 @@ func TestDecodeUnstructured(t *testing.T) { t.Errorf("object not converted: %#v", pl.Items[2]) } } + +func TestDecode(t *testing.T) { + tcs := []struct { + json []byte + want runtime.Object + }{ + { + json: []byte(`{"apiVersion": "test", "kind": "test_kind"}`), + want: &runtime.Unstructured{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "test", + Kind: "test_kind", + }, + Object: map[string]interface{}{"apiVersion": "test", "kind": "test_kind"}, + }, + }, + { + json: []byte(`{"apiVersion": "test", "kind": "test_list", "items": []}`), + want: &runtime.UnstructuredList{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "test", + Kind: "test_list", + }, + }, + }, + { + json: []byte(`{"items": [{"metadata": {"name": "object1"}, "apiVersion": "test", "kind": "test_kind"}, {"metadata": {"name": "object2"}, "apiVersion": "test", "kind": "test_kind"}], "apiVersion": "test", "kind": "test_list"}`), + want: &runtime.UnstructuredList{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "test", + Kind: "test_list", + }, + Items: []*runtime.Unstructured{ + { + TypeMeta: runtime.TypeMeta{ + APIVersion: "test", + Kind: "test_kind", + }, + Name: "object1", + Object: map[string]interface{}{ + "metadata": map[string]interface{}{"name": "object1"}, + "apiVersion": "test", + "kind": "test_kind", + }, + }, + { + TypeMeta: runtime.TypeMeta{ + APIVersion: "test", + Kind: "test_kind", + }, + Name: "object2", + Object: map[string]interface{}{ + "metadata": map[string]interface{}{"name": "object2"}, + "apiVersion": "test", + "kind": "test_kind", + }, + }, + }, + }, + }, + } + + for _, tc := range tcs { + got, _, err := runtime.UnstructuredJSONScheme.Decode(tc.json, nil, nil) + if err != nil { + t.Errorf("Unexpected error for %q: %v", string(tc.json), err) + continue + } + + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("Decode(%q) want: %v\ngot: %v", string(tc.json), tc.want, got) + } + } +} diff --git a/test/integration/dynamic_client_test.go b/test/integration/dynamic_client_test.go new file mode 100644 index 0000000000..1a18916fa6 --- /dev/null +++ b/test/integration/dynamic_client_test.go @@ -0,0 +1,149 @@ +// +build integration,!no-etcd + +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 integration + +import ( + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/testapi" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/client/typed/dynamic" + uclient "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/test/integration/framework" +) + +func TestDynamicClient(t *testing.T) { + _, s := framework.RunAMaster(t) + defer s.Close() + + framework.DeleteAllEtcdKeys() + gv := testapi.Default.GroupVersion() + config := &uclient.Config{ + Host: s.URL, + ContentConfig: uclient.ContentConfig{GroupVersion: gv}, + } + + client := uclient.NewOrDie(config) + dynamicClient, err := dynamic.NewClient(config) + _ = dynamicClient + if err != nil { + t.Fatalf("unexpected error creating dynamic client: %v", err) + } + + // Find the Pod resource + resources, err := client.Discovery().ServerResourcesForGroupVersion(gv.String()) + if err != nil { + t.Fatalf("unexpected error listing resources: %v", err) + } + + var resource unversioned.APIResource + for _, r := range resources.APIResources { + if r.Kind == "Pod" { + resource = r + break + } + } + + if len(resource.Name) == 0 { + t.Fatalf("could not find the pod resource in group/version %q", gv.String()) + } + + // Create a Pod with the normal client + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + GenerateName: "test", + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: "test", + Image: "test-image", + }, + }, + }, + } + + actual, err := client.Pods(framework.TestNS).Create(pod) + if err != nil { + t.Fatalf("unexpected error when creating pod: %v", err) + } + + // check dynamic list + unstructuredList, err := dynamicClient.Resource(&resource, framework.TestNS).List(v1.ListOptions{}) + if err != nil { + t.Fatalf("unexpected error when listing pods: %v", err) + } + + if len(unstructuredList.Items) != 1 { + t.Fatalf("expected one pod, got %d", len(unstructuredList.Items)) + } + + got, err := unstructuredToPod(unstructuredList.Items[0]) + if err != nil { + t.Fatalf("unexpected error converting Unstructured to api.Pod: %v", err) + } + + if !reflect.DeepEqual(actual, got) { + t.Fatalf("unexpected pod in list. wanted %#v, got %#v", actual, got) + } + + // check dynamic get + unstruct, err := dynamicClient.Resource(&resource, framework.TestNS).Get(actual.Name) + if err != nil { + t.Fatalf("unexpected error when getting pod %q: %v", actual.Name, err) + } + + got, err = unstructuredToPod(unstruct) + if err != nil { + t.Fatalf("unexpected error converting Unstructured to api.Pod: %v", err) + } + + if !reflect.DeepEqual(actual, got) { + t.Fatalf("unexpected pod in list. wanted %#v, got %#v", actual, got) + } + + // delete the pod dynamically + err = dynamicClient.Resource(&resource, framework.TestNS).Delete(actual.Name, nil) + if err != nil { + t.Fatalf("unexpected error when deleting pod: %v", err) + } + + list, err := client.Pods(framework.TestNS).List(api.ListOptions{}) + if err != nil { + t.Fatalf("unexpected error when listing pods: %v", err) + } + + if len(list.Items) != 0 { + t.Fatalf("expected zero pods, got %d", len(list.Items)) + } +} + +func unstructuredToPod(obj *runtime.Unstructured) (*api.Pod, error) { + json, err := runtime.Encode(runtime.UnstructuredJSONScheme, obj) + if err != nil { + return nil, err + } + pod := new(api.Pod) + err = runtime.DecodeInto(testapi.Default.Codec(), json, pod) + return pod, err +}