Commit 4c58302b authored by Kris's avatar Kris

Adding dynamic client

parent afc55647
/*
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{}
...@@ -32,6 +32,7 @@ func (obj *TypeMeta) GroupVersionKind() *unversioned.GroupVersionKind { ...@@ -32,6 +32,7 @@ func (obj *TypeMeta) GroupVersionKind() *unversioned.GroupVersionKind {
func (obj *Unknown) 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 *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 // GetObjectKind implements Object for VersionedObjects, returning an empty ObjectKind
// interface if no objects are provided, or the ObjectKind interface of the object in the // interface if no objects are provided, or the ObjectKind interface of the object in the
......
...@@ -474,7 +474,7 @@ func (s *Scheme) ConvertToVersion(in Object, outVersion string) (Object, error) ...@@ -474,7 +474,7 @@ func (s *Scheme) ConvertToVersion(in Object, outVersion string) (Object, error)
return nil, err return nil, err
} }
switch in.(type) { switch in.(type) {
case *Unknown, *Unstructured: case *Unknown, *Unstructured, *UnstructuredList:
old := in.GetObjectKind().GroupVersionKind() old := in.GetObjectKind().GroupVersionKind()
defer in.GetObjectKind().SetGroupVersionKind(old) defer in.GetObjectKind().SetGroupVersionKind(old)
setTargetVersion(in, s, gv) setTargetVersion(in, s, gv)
......
...@@ -109,11 +109,25 @@ type Unknown struct { ...@@ -109,11 +109,25 @@ type Unknown struct {
// metadata and field mutatation. // metadata and field mutatation.
type Unstructured struct { type Unstructured struct {
TypeMeta `json:",inline"` 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{} // Object is a JSON compatible map with string, float, int, []interface{}, or map[string]interface{}
// children. // children.
Object map[string]interface{} 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 // VersionedObjects is used by Decoders to give callers a way to access all versions
// of an object during the decoding process. // of an object during the decoding process.
type VersionedObjects struct { type VersionedObjects struct {
......
...@@ -30,13 +30,87 @@ var UnstructuredJSONScheme Codec = unstructuredJSONScheme{} ...@@ -30,13 +30,87 @@ var UnstructuredJSONScheme Codec = unstructuredJSONScheme{}
type unstructuredJSONScheme struct{} type unstructuredJSONScheme struct{}
func (s unstructuredJSONScheme) Decode(data []byte, _ *unversioned.GroupVersionKind, _ Object) (Object, *unversioned.GroupVersionKind, error) { 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)
}
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{} 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{}) m := make(map[string]interface{})
if err := json.Unmarshal(data, &m); err != nil { if err := json.Unmarshal(data, &m); err != nil {
return nil, nil, err return err
} }
if v, ok := m["kind"]; ok { if v, ok := m["kind"]; ok {
if s, ok := v.(string); ok { if s, ok := v.(string); ok {
unstruct.Kind = s unstruct.Kind = s
...@@ -47,30 +121,39 @@ func (s unstructuredJSONScheme) Decode(data []byte, _ *unversioned.GroupVersionK ...@@ -47,30 +121,39 @@ func (s unstructuredJSONScheme) Decode(data []byte, _ *unversioned.GroupVersionK
unstruct.APIVersion = s unstruct.APIVersion = s
} }
} }
if metadata, ok := m["metadata"]; ok {
if len(unstruct.APIVersion) == 0 { if metadata, ok := metadata.(map[string]interface{}); ok {
return nil, nil, NewMissingVersionErr(string(data)) if name, ok := metadata["name"]; ok {
if name, ok := name.(string); ok {
unstruct.Name = name
}
} }
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))
} }
unstruct.Object = m unstruct.Object = m
return unstruct, &gvk, nil
return nil
} }
func (s unstructuredJSONScheme) EncodeToStream(obj Object, w io.Writer, overrides ...unversioned.GroupVersion) error { func (s unstructuredJSONScheme) decodeToList(data []byte, list *UnstructuredList) error {
switch t := obj.(type) { type decodeList struct {
case *Unstructured: TypeMeta `json:",inline"`
return json.NewEncoder(w).Encode(t.Object) Items []json.RawMessage
case *Unknown: }
_, err := w.Write(t.RawJSON)
var dList decodeList
if err := json.Unmarshal(data, &dList); err != nil {
return err return err
default:
return json.NewEncoder(w).Encode(t)
} }
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
} }
...@@ -18,6 +18,7 @@ package runtime_test ...@@ -18,6 +18,7 @@ package runtime_test
import ( import (
"fmt" "fmt"
"reflect"
"testing" "testing"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
...@@ -46,3 +47,77 @@ func TestDecodeUnstructured(t *testing.T) { ...@@ -46,3 +47,77 @@ func TestDecodeUnstructured(t *testing.T) {
t.Errorf("object not converted: %#v", pl.Items[2]) 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)
}
}
}
// +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
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment