From d9ce524d635ee9ab6eac37ca91a0216d5d9619b9 Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 4 Aug 2016 10:26:58 -0700 Subject: [PATCH 01/10] Expose dynamic client's content config --- pkg/client/typed/dynamic/client.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/pkg/client/typed/dynamic/client.go b/pkg/client/typed/dynamic/client.go index 72c460b255d..8b065e3a1f5 100644 --- a/pkg/client/typed/dynamic/client.go +++ b/pkg/client/typed/dynamic/client.go @@ -55,9 +55,12 @@ func NewClient(conf *restclient.Config) (*Client, error) { confCopy := *conf conf = &confCopy - // TODO: it's questionable that this should be using anything other than unstructured schema and JSON - conf.ContentType = runtime.ContentTypeJSON - conf.AcceptContentTypes = runtime.ContentTypeJSON + contentConfig := ContentConfig() + contentConfig.GroupVersion = conf.GroupVersion + if conf.NegotiatedSerializer != nil { + contentConfig.NegotiatedSerializer = conf.NegotiatedSerializer + } + conf.ContentConfig = contentConfig if conf.APIPath == "" { conf.APIPath = "/api" @@ -66,10 +69,6 @@ func NewClient(conf *restclient.Config) (*Client, error) { if len(conf.UserAgent) == 0 { conf.UserAgent = restclient.DefaultKubernetesUserAgent() } - if conf.NegotiatedSerializer == nil { - streamingInfo, _ := api.Codecs.StreamingSerializerForMediaType("application/json;stream=watch", nil) - conf.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: dynamicCodec{}}, streamingInfo) - } cl, err := restclient.RESTClientFor(conf) if err != nil { @@ -255,6 +254,18 @@ func (dynamicCodec) Encode(obj runtime.Object, w io.Writer) error { return runtime.UnstructuredJSONScheme.Encode(obj, w) } +// ContentConfig returns a restclient.ContentConfig for dynamic types. +func ContentConfig() restclient.ContentConfig { + // TODO: it's questionable that this should be using anything other than unstructured schema and JSON + codec := dynamicCodec{} + streamingInfo, _ := api.Codecs.StreamingSerializerForMediaType("application/json;stream=watch", nil) + return restclient.ContentConfig{ + AcceptContentTypes: runtime.ContentTypeJSON, + ContentType: runtime.ContentTypeJSON, + NegotiatedSerializer: serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec}, streamingInfo), + } +} + // paramaterCodec is a codec converts an API object to query // parameters without trying to convert to the target version. type parameterCodec struct{} From e5c21548837c4b5b9a23568c2cfc62a0cc04af18 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 8 Aug 2016 15:08:14 -0700 Subject: [PATCH 02/10] Eliminate redundant dynamic client type This will allow people to override the default parameter codec and still pass the resulting client with something that accepts *dynamic.Client. --- pkg/client/typed/dynamic/client.go | 37 +++++++++--------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/pkg/client/typed/dynamic/client.go b/pkg/client/typed/dynamic/client.go index 8b065e3a1f5..1565261c5e9 100644 --- a/pkg/client/typed/dynamic/client.go +++ b/pkg/client/typed/dynamic/client.go @@ -40,11 +40,7 @@ import ( // Client is a Kubernetes client that allows you to access metadata // and manipulate metadata of a Kubernetes API group. type Client struct { - cl *restclient.RESTClient -} - -type ClientWithParameterCodec struct { - client *Client + cl *restclient.RESTClient parameterCodec runtime.ParameterCodec } @@ -85,35 +81,24 @@ func (c *Client) GetRateLimiter() flowcontrol.RateLimiter { // 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. +// is ignored. The ResourceClient inherits the parameter codec of c. func (c *Client) Resource(resource *unversioned.APIResource, namespace string) *ResourceClient { return &ResourceClient{ - cl: c.cl, - resource: resource, - ns: namespace, - } -} - -// ParameterCodec wraps a parameterCodec around the Client. -func (c *Client) ParameterCodec(parameterCodec runtime.ParameterCodec) *ClientWithParameterCodec { - return &ClientWithParameterCodec{ - client: c, - parameterCodec: parameterCodec, - } -} - -// 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 c. -func (c *ClientWithParameterCodec) Resource(resource *unversioned.APIResource, namespace string) *ResourceClient { - return &ResourceClient{ - cl: c.client.cl, + cl: c.cl, resource: resource, ns: namespace, parameterCodec: c.parameterCodec, } } +// ParameterCodec returns a client with the provided parameter codec. +func (c *Client) ParameterCodec(parameterCodec runtime.ParameterCodec) *Client { + return &Client{ + cl: c.cl, + parameterCodec: parameterCodec, + } +} + // ResourceClient is an API interface to a specific resource under a // dynamic client. type ResourceClient struct { From 69e0740b93e4679551b69529912bee7b8cc504ee Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 8 Aug 2016 15:09:47 -0700 Subject: [PATCH 03/10] Properly decode lists into VersionedObjects --- pkg/runtime/unstructured.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/runtime/unstructured.go b/pkg/runtime/unstructured.go index 6cc34432c05..153dccda6c8 100644 --- a/pkg/runtime/unstructured.go +++ b/pkg/runtime/unstructured.go @@ -103,10 +103,9 @@ func (s unstructuredJSONScheme) decodeInto(data []byte, obj Object) error { case *UnstructuredList: return s.decodeToList(data, x) case *VersionedObjects: - u := new(Unstructured) - err := s.decodeToUnstructured(data, u) + o, err := s.decode(data) if err == nil { - x.Objects = []Object{u} + x.Objects = []Object{o} } return err default: From 361f13ddf00cae87ed0e2ae57d4ba963297e9ab1 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 15 Aug 2016 22:06:11 -0700 Subject: [PATCH 04/10] Add JSON encoding handlers to unstructured objects --- pkg/runtime/types.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pkg/runtime/types.go b/pkg/runtime/types.go index 5033c0dc2c9..239c65b9d87 100644 --- a/pkg/runtime/types.go +++ b/pkg/runtime/types.go @@ -17,6 +17,7 @@ limitations under the License. package runtime import ( + "bytes" "fmt" "github.com/golang/glog" @@ -138,6 +139,21 @@ type Unstructured struct { Object map[string]interface{} } +// MarshalJSON ensures that the unstructured object produces proper +// JSON when passed to Go's standard JSON library. +func (u *Unstructured) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + err := UnstructuredJSONScheme.Encode(u, &buf) + return buf.Bytes(), err +} + +// UnmarshalJSON ensures that the unstructured object properly decodes +// JSON when passed to Go's standard JSON library. +func (u *Unstructured) UnmarshalJSON(b []byte) error { + _, _, err := UnstructuredJSONScheme.Decode(b, nil, u) + return err +} + func getNestedField(obj map[string]interface{}, fields ...string) interface{} { var val interface{} = obj for _, field := range fields { @@ -450,6 +466,21 @@ type UnstructuredList struct { Items []*Unstructured `json:"items"` } +// MarshalJSON ensures that the unstructured list object produces proper +// JSON when passed to Go's standard JSON library. +func (u *UnstructuredList) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + err := UnstructuredJSONScheme.Encode(u, &buf) + return buf.Bytes(), err +} + +// UnmarshalJSON ensures that the unstructured list object properly +// decodes JSON when passed to Go's standard JSON library. +func (u *UnstructuredList) UnmarshalJSON(b []byte) error { + _, _, err := UnstructuredJSONScheme.Decode(b, nil, u) + return err +} + func (u *UnstructuredList) setNestedField(value interface{}, fields ...string) { if u.Object == nil { u.Object = make(map[string]interface{}) From 3999f071d1695edfc52ef6cb0a9f6a1346d83655 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 8 Aug 2016 15:13:21 -0700 Subject: [PATCH 05/10] Add generic "List" type for all versions --- pkg/client/typed/discovery/restmapper.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/client/typed/discovery/restmapper.go b/pkg/client/typed/discovery/restmapper.go index 562a957a938..7f6a0d1f40c 100644 --- a/pkg/client/typed/discovery/restmapper.go +++ b/pkg/client/typed/discovery/restmapper.go @@ -80,6 +80,8 @@ func NewRESTMapper(groupResources []*APIGroupResources, versionInterfaces meta.V // TODO only do this if it supports listing versionMapper.Add(gv.WithKind(resource.Kind+"List"), scope) } + // TODO why is this type not in discovery (at least for "v1") + versionMapper.Add(gv.WithKind("List"), meta.RESTScopeRoot) unionMapper = append(unionMapper, versionMapper) } } From 63a512fe472d72c70c3ec799509fb5eeae6d2261 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 8 Aug 2016 15:16:10 -0700 Subject: [PATCH 06/10] Add discovery mapper and dynamic typer to kubectl --- pkg/api/meta/unstructured.go | 31 +++++++ pkg/client/typed/discovery/unstructured.go | 95 ++++++++++++++++++++++ pkg/kubectl/cmd/cmd_test.go | 11 +++ pkg/kubectl/cmd/util/factory.go | 58 +++++++++++++ 4 files changed, 195 insertions(+) create mode 100644 pkg/api/meta/unstructured.go create mode 100644 pkg/client/typed/discovery/unstructured.go diff --git a/pkg/api/meta/unstructured.go b/pkg/api/meta/unstructured.go new file mode 100644 index 00000000000..784cbf05c30 --- /dev/null +++ b/pkg/api/meta/unstructured.go @@ -0,0 +1,31 @@ +/* +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 meta + +import ( + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/runtime" +) + +// InterfacesForUnstructured returns VersionInterfaces suitable for +// dealing with runtime.Unstructured objects. +func InterfacesForUnstructured(unversioned.GroupVersion) (*VersionInterfaces, error) { + return &VersionInterfaces{ + ObjectConvertor: &runtime.UnstructuredObjectConverter{}, + MetadataAccessor: NewAccessor(), + }, nil +} diff --git a/pkg/client/typed/discovery/unstructured.go b/pkg/client/typed/discovery/unstructured.go new file mode 100644 index 00000000000..afa74e7dee2 --- /dev/null +++ b/pkg/client/typed/discovery/unstructured.go @@ -0,0 +1,95 @@ +/* +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 discovery + +import ( + "fmt" + + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/runtime" +) + +// UnstructuredObjectTyper provides a runtime.ObjectTyper implmentation for +// runtime.Unstructured object based on discovery information. +type UnstructuredObjectTyper struct { + registered map[unversioned.GroupVersionKind]bool +} + +// NewUnstructuredObjectTyper returns a runtime.ObjectTyper for +// unstructred objects based on discovery information. +func NewUnstructuredObjectTyper(groupResources []*APIGroupResources) *UnstructuredObjectTyper { + dot := &UnstructuredObjectTyper{registered: make(map[unversioned.GroupVersionKind]bool)} + for _, group := range groupResources { + for _, discoveryVersion := range group.Group.Versions { + resources, ok := group.VersionedResources[discoveryVersion.Version] + if !ok { + continue + } + + gv := unversioned.GroupVersion{Group: group.Group.Name, Version: discoveryVersion.Version} + for _, resource := range resources { + dot.registered[gv.WithKind(resource.Kind)] = true + } + } + } + return dot +} + +// ObjectKind returns the group,version,kind of the provided object, or an error +// if the object in not *runtime.Unstructured or has no group,version,kind +// information. +func (d *UnstructuredObjectTyper) ObjectKind(obj runtime.Object) (unversioned.GroupVersionKind, error) { + if _, ok := obj.(*runtime.Unstructured); !ok { + return unversioned.GroupVersionKind{}, fmt.Errorf("type %T is invalid for dynamic object typer", obj) + } + + return obj.GetObjectKind().GroupVersionKind(), 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 *runtime.Unstructured or +// has no group,version,kind information. unversionedType will always be false +// because runtime.Unstructured object should always have group,version,kind +// information set. +func (d *UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) (gvks []unversioned.GroupVersionKind, unversionedType bool, err error) { + gvk, err := d.ObjectKind(obj) + if err != nil { + return nil, false, err + } + + return []unversioned.GroupVersionKind{gvk}, false, nil +} + +// Recognizes returns true if the provided group,version,kind was in the +// discovery information. +func (d *UnstructuredObjectTyper) Recognizes(gvk unversioned.GroupVersionKind) bool { + return d.registered[gvk] +} + +// IsUnversioned returns false always because *runtime.Unstructured objects +// should always have group,version,kind information set. ok will be true if the +// object's group,version,kind is registered. +func (d *UnstructuredObjectTyper) IsUnversioned(obj runtime.Object) (unversioned bool, ok bool) { + gvk, err := d.ObjectKind(obj) + if err != nil { + return false, false + } + + return false, d.registered[gvk] +} + +var _ runtime.ObjectTyper = &UnstructuredObjectTyper{} diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index 05b8476eb3a..f8af22f68eb 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -34,6 +34,7 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/client/restclient" + "k8s.io/kubernetes/pkg/client/typed/discovery" client "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/client/unversioned/fake" "k8s.io/kubernetes/pkg/kubectl" @@ -275,6 +276,13 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec, runtime.Neg Object: func(discovery bool) (meta.RESTMapper, runtime.ObjectTyper) { return testapi.Default.RESTMapper(), api.Scheme }, + UnstructuredObject: func() (meta.RESTMapper, runtime.ObjectTyper, error) { + groupResources := testDynamicResources() + mapper := discovery.NewRESTMapper(groupResources, meta.InterfacesForUnstructured) + typer := discovery.NewUnstructuredObjectTyper(groupResources) + + return kubectl.ShortcutExpander{RESTMapper: mapper}, typer, nil + }, Client: func() (*client.Client, error) { // Swap out the HTTP client out of the client with the fake's version. fakeClient := t.Client.(*fake.RESTClient) @@ -286,6 +294,9 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec, runtime.Neg ClientForMapping: func(*meta.RESTMapping) (resource.RESTClient, error) { return t.Client, t.Err }, + UnstructuredClientForMapping: func(*meta.RESTMapping) (resource.RESTClient, error) { + return t.Client, t.Err + }, Decoder: func(bool) runtime.Decoder { return testapi.Default.Codec() }, diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index d61ed1004b9..d2518491edc 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -53,6 +53,8 @@ import ( "k8s.io/kubernetes/pkg/apis/policy" "k8s.io/kubernetes/pkg/apis/rbac" "k8s.io/kubernetes/pkg/client/restclient" + "k8s.io/kubernetes/pkg/client/typed/discovery" + "k8s.io/kubernetes/pkg/client/typed/dynamic" client "k8s.io/kubernetes/pkg/client/unversioned" clientset "k8s.io/kubernetes/pkg/client/unversioned/adapters/internalclientset" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" @@ -83,6 +85,9 @@ type Factory struct { // Returns interfaces for dealing with arbitrary runtime.Objects. If thirdPartyDiscovery is true, performs API calls // to discovery dynamic API objects registered by third parties. Object func(thirdPartyDiscovery bool) (meta.RESTMapper, runtime.ObjectTyper) + // Returns interfaces for dealing with arbitrary + // runtime.Unstructured. This performs API calls to discover types. + UnstructuredObject func() (meta.RESTMapper, runtime.ObjectTyper, error) // Returns interfaces for decoding objects - if toInternal is set, decoded objects will be converted // into their internal form (if possible). Eventually the internal form will be removed as an option, // and only versioned objects will be returned. @@ -96,6 +101,8 @@ type Factory struct { // Returns a RESTClient for working with the specified RESTMapping or an error. This is intended // for working with arbitrary resources and is not guaranteed to point to a Kubernetes APIServer. ClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) + // Returns a RESTClient for working with Unstructured objects. + UnstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) // Returns a Describer for displaying the specified RESTMapping type or an error. Describer func(mapping *meta.RESTMapping) (kubectl.Describer, error) // Returns a Printer for formatting objects of the given type or an error. @@ -360,6 +367,40 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { } return priorityRESTMapper, api.Scheme }, + UnstructuredObject: func() (meta.RESTMapper, runtime.ObjectTyper, error) { + cfg, err := clients.ClientConfigForVersion(nil) + if err != nil { + return nil, nil, err + } + + dc, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + return nil, nil, err + } + + groupResources, err := discovery.GetAPIGroupResources(dc) + if err != nil { + return nil, nil, err + } + + // Register unknown APIs as third party for now to make + // validation happy. TODO perhaps make a dynamic schema + // validator to avoid this. + for _, group := range groupResources { + for _, version := range group.Group.Versions { + gv := unversioned.GroupVersion{Group: group.Group.Name, Version: version.Version} + if !registered.IsRegisteredVersion(gv) { + registered.AddThirdPartyAPIGroupVersions(gv) + } + } + } + + mapper := discovery.NewRESTMapper(groupResources, meta.InterfacesForUnstructured) + + typer := discovery.NewUnstructuredObjectTyper(groupResources) + + return kubectl.ShortcutExpander{RESTMapper: mapper}, typer, nil + }, Client: func() (*client.Client, error) { return clients.ClientForVersion(nil) }, @@ -391,6 +432,23 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { } return restclient.RESTClientFor(cfg) }, + UnstructuredClientForMapping: func(mapping *meta.RESTMapping) (resource.RESTClient, error) { + cfg, err := clientConfig.ClientConfig() + if err != nil { + return nil, err + } + if err := restclient.SetKubernetesDefaults(cfg); err != nil { + return nil, err + } + cfg.APIPath = "/apis" + if mapping.GroupVersionKind.Group == api.GroupName { + cfg.APIPath = "/api" + } + gv := mapping.GroupVersionKind.GroupVersion() + cfg.ContentConfig = dynamic.ContentConfig() + cfg.GroupVersion = &gv + return restclient.RESTClientFor(cfg) + }, Describer: func(mapping *meta.RESTMapping) (kubectl.Describer, error) { mappingVersion := mapping.GroupVersionKind.GroupVersion() if mapping.GroupVersionKind.Group == federation.GroupName { From d1aa3dd0b4d8af2ef3a3375a19eab9aebe821b4a Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 11 Aug 2016 09:55:37 -0700 Subject: [PATCH 07/10] Fix thirdparty codec to return GVK --- pkg/registry/thirdpartyresourcedata/codec.go | 35 +++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/pkg/registry/thirdpartyresourcedata/codec.go b/pkg/registry/thirdpartyresourcedata/codec.go index ca190c1f37f..b96d789ca57 100644 --- a/pkg/registry/thirdpartyresourcedata/codec.go +++ b/pkg/registry/thirdpartyresourcedata/codec.go @@ -269,35 +269,42 @@ func parseObject(data []byte) (map[string]interface{}, error) { return mapObj, nil } -func (t *thirdPartyResourceDataDecoder) populate(data []byte) (runtime.Object, error) { +func (t *thirdPartyResourceDataDecoder) populate(data []byte) (runtime.Object, *unversioned.GroupVersionKind, error) { mapObj, err := parseObject(data) if err != nil { - return nil, err + return nil, nil, err } return t.populateFromObject(mapObj, data) } -func (t *thirdPartyResourceDataDecoder) populateFromObject(mapObj map[string]interface{}, data []byte) (runtime.Object, error) { +func (t *thirdPartyResourceDataDecoder) populateFromObject(mapObj map[string]interface{}, data []byte) (runtime.Object, *unversioned.GroupVersionKind, error) { typeMeta := unversioned.TypeMeta{} if err := json.Unmarshal(data, &typeMeta); err != nil { - return nil, err + return nil, nil, err } + + gv, err := unversioned.ParseGroupVersion(typeMeta.APIVersion) + if err != nil { + return nil, nil, err + } + gvk := gv.WithKind(typeMeta.Kind) + isList := strings.HasSuffix(typeMeta.Kind, "List") switch { case !isList && (len(t.kind) == 0 || typeMeta.Kind == t.kind): result := &extensions.ThirdPartyResourceData{} if err := t.populateResource(result, mapObj, data); err != nil { - return nil, err + return nil, nil, err } - return result, nil + return result, &gvk, nil case isList && (len(t.kind) == 0 || typeMeta.Kind == t.kind+"List"): list := &extensions.ThirdPartyResourceDataList{} if err := t.populateListResource(list, mapObj); err != nil { - return nil, err + return nil, nil, err } - return list, nil + return list, &gvk, nil default: - return nil, fmt.Errorf("unexpected kind: %s, expected %s", typeMeta.Kind, t.kind) + return nil, nil, fmt.Errorf("unexpected kind: %s, expected %s", typeMeta.Kind, t.kind) } } @@ -359,11 +366,7 @@ func (t *thirdPartyResourceDataDecoder) Decode(data []byte, gvk *unversioned.Gro return t.delegate.Decode(data, gvk, into) } } - obj, err := t.populate(data) - if err != nil { - return nil, nil, err - } - return obj, gvk, nil + return t.populate(data) } switch o := into.(type) { case *extensions.ThirdPartyResourceData: @@ -377,14 +380,14 @@ func (t *thirdPartyResourceDataDecoder) Decode(data []byte, gvk *unversioned.Gro return t.delegate.Decode(data, gvk, into) } } - obj, err := t.populate(data) + obj, outGVK, err := t.populate(data) if err != nil { return nil, nil, err } o.Objects = []runtime.Object{ obj, } - return o, gvk, nil + return o, outGVK, nil default: return t.delegate.Decode(data, gvk, into) } From 24c441e96d7db090690409bebf4514e19969d5ec Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 8 Aug 2016 15:30:56 -0700 Subject: [PATCH 08/10] kubectl/resource: Remove thirdparty special casing And also remove some duplicate code while I'm at it. --- pkg/kubectl/resource/mapper.go | 17 ++--------------- pkg/kubectl/resource/visitor.go | 8 +------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/pkg/kubectl/resource/mapper.go b/pkg/kubectl/resource/mapper.go index fe0f315ab51..66f0da449f1 100644 --- a/pkg/kubectl/resource/mapper.go +++ b/pkg/kubectl/resource/mapper.go @@ -22,7 +22,6 @@ import ( "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/unversioned" - "k8s.io/kubernetes/pkg/registry/thirdpartyresourcedata" "k8s.io/kubernetes/pkg/runtime" ) @@ -54,20 +53,8 @@ func (m *Mapper) InfoForData(data []byte, source string) (*Info, error) { if err != nil { return nil, fmt.Errorf("unable to decode %q: %v", source, err) } - var obj runtime.Object - var versioned runtime.Object - if isThirdParty, gvkOut, err := thirdpartyresourcedata.IsThirdPartyObject(data, gvk); err != nil { - return nil, err - } else if isThirdParty { - obj, err = runtime.Decode(thirdpartyresourcedata.NewDecoder(nil, gvkOut.Kind), data) - versioned = obj - gvk = gvkOut - } else { - obj, versioned = versions.Last(), versions.First() - } - if err != nil { - return nil, fmt.Errorf("unable to decode %q: %v [%v]", source, err, gvk) - } + + obj, versioned := versions.Last(), versions.First() mapping, err := m.RESTMapping(gvk.GroupKind(), gvk.Version) if err != nil { return nil, fmt.Errorf("unable to recognize %q: %v", source, err) diff --git a/pkg/kubectl/resource/visitor.go b/pkg/kubectl/resource/visitor.go index cc89af65549..3d260c2d4bd 100644 --- a/pkg/kubectl/resource/visitor.go +++ b/pkg/kubectl/resource/visitor.go @@ -618,13 +618,7 @@ func RetrieveLatest(info *Info, err error) error { if info.Namespaced() && len(info.Namespace) == 0 { return fmt.Errorf("no namespace set on resource %s %q", info.Mapping.Resource, info.Name) } - obj, err := NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name, info.Export) - if err != nil { - return err - } - info.Object = obj - info.ResourceVersion, _ = info.Mapping.MetadataAccessor.ResourceVersion(obj) - return nil + return info.Get() } // RetrieveLazy updates the object if it has not been loaded yet. From 7511412ca63e44e285fde18a0e1ef1125c262802 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 8 Aug 2016 15:32:18 -0700 Subject: [PATCH 09/10] Make kubectl create use dynamic clients --- pkg/kubectl/cmd/create.go | 8 ++++++-- pkg/kubectl/cmd/create_test.go | 26 +++++++++++++++----------- pkg/kubectl/cmd/get_test.go | 21 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 0fab1339329..891a6409e50 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -27,6 +27,7 @@ import ( "k8s.io/kubernetes/pkg/kubectl" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/runtime" ) // CreateOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of @@ -107,8 +108,11 @@ func RunCreate(f *cmdutil.Factory, cmd *cobra.Command, out io.Writer, options *C return err } - mapper, typer := f.Object(cmdutil.GetIncludeThirdPartyAPIs(cmd)) - r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)). + mapper, typer, err := f.UnstructuredObject() + if err != nil { + return err + } + r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), runtime.UnstructuredJSONScheme). Schema(schema). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). diff --git a/pkg/kubectl/cmd/create_test.go b/pkg/kubectl/cmd/create_test.go index 415fd7bcda4..c2d680e24b7 100644 --- a/pkg/kubectl/cmd/create_test.go +++ b/pkg/kubectl/cmd/create_test.go @@ -21,6 +21,7 @@ import ( "net/http" "testing" + "k8s.io/kubernetes/pkg/client/typed/dynamic" "k8s.io/kubernetes/pkg/client/unversioned/fake" ) @@ -40,14 +41,15 @@ func TestCreateObject(t *testing.T) { _, _, rc := testData() rc.Items[0].Name = "redis-master-controller" - f, tf, codec, ns := NewAPIFactory() + f, tf, codec, _ := NewAPIFactory() + ns := dynamic.ContentConfig().NegotiatedSerializer tf.Printer = &testPrinter{} tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/replicationcontrollers" && m == "POST": - return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil + case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost: + return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil @@ -72,16 +74,17 @@ func TestCreateMultipleObject(t *testing.T) { initTestErrorHandler(t) _, svc, rc := testData() - f, tf, codec, ns := NewAPIFactory() + f, tf, codec, _ := NewAPIFactory() + ns := dynamic.ContentConfig().NegotiatedSerializer tf.Printer = &testPrinter{} tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/services" && m == "POST": - return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &svc.Items[0])}, nil - case p == "/namespaces/test/replicationcontrollers" && m == "POST": - return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil + case p == "/namespaces/test/services" && m == http.MethodPost: + return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &svc.Items[0])}, nil + case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost: + return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil @@ -108,14 +111,15 @@ func TestCreateDirectory(t *testing.T) { _, _, rc := testData() rc.Items[0].Name = "name" - f, tf, codec, ns := NewAPIFactory() + f, tf, codec, _ := NewAPIFactory() + ns := dynamic.ContentConfig().NegotiatedSerializer tf.Printer = &testPrinter{} tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/replicationcontrollers" && m == "POST": - return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil + case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost: + return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil diff --git a/pkg/kubectl/cmd/get_test.go b/pkg/kubectl/cmd/get_test.go index 705bdc6861c..2dc88111166 100644 --- a/pkg/kubectl/cmd/get_test.go +++ b/pkg/kubectl/cmd/get_test.go @@ -33,6 +33,7 @@ import ( apitesting "k8s.io/kubernetes/pkg/api/testing" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/client/restclient" + "k8s.io/kubernetes/pkg/client/typed/discovery" "k8s.io/kubernetes/pkg/client/unversioned/fake" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime/serializer" @@ -89,6 +90,26 @@ func testData() (*api.PodList, *api.ServiceList, *api.ReplicationControllerList) return pods, svc, rc } +func testDynamicResources() []*discovery.APIGroupResources { + return []*discovery.APIGroupResources{ + { + Group: unversioned.APIGroup{ + Versions: []unversioned.GroupVersionForDiscovery{ + {Version: "v1"}, + }, + PreferredVersion: unversioned.GroupVersionForDiscovery{Version: "v1"}, + }, + VersionedResources: map[string][]unversioned.APIResource{ + "v1": { + {Name: "pods", Namespaced: true, Kind: "Pod"}, + {Name: "services", Namespaced: true, Kind: "Service"}, + {Name: "replicationcontrollers", Namespaced: true, Kind: "ReplicationController"}, + }, + }, + }, + } +} + func testComponentStatusData() *api.ComponentStatusList { good := api.ComponentStatus{ Conditions: []api.ComponentCondition{ From b5235bccec245c80a729b72cb805b1db6e5f3944 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 15 Aug 2016 22:07:00 -0700 Subject: [PATCH 10/10] Make kubectl replace use dynamic clients --- pkg/kubectl/cmd/replace.go | 10 +++++-- pkg/kubectl/cmd/replace_test.go | 53 ++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/pkg/kubectl/cmd/replace.go b/pkg/kubectl/cmd/replace.go index e7db11a5fbd..a16d4591673 100644 --- a/pkg/kubectl/cmd/replace.go +++ b/pkg/kubectl/cmd/replace.go @@ -30,6 +30,7 @@ import ( "k8s.io/kubernetes/pkg/kubectl" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/runtime" ) // ReplaceOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of @@ -187,8 +188,11 @@ func forceReplace(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args [] } } - mapper, typer := f.Object(cmdutil.GetIncludeThirdPartyAPIs(cmd)) - r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)). + mapper, typer, err := f.UnstructuredObject() + if err != nil { + return err + } + r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), runtime.UnstructuredJSONScheme). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, options.Recursive, options.Filenames...). @@ -212,7 +216,7 @@ func forceReplace(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args [] return err } - r = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)). + r = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), runtime.UnstructuredJSONScheme). Schema(schema). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). diff --git a/pkg/kubectl/cmd/replace_test.go b/pkg/kubectl/cmd/replace_test.go index 871f475b618..992729079db 100644 --- a/pkg/kubectl/cmd/replace_test.go +++ b/pkg/kubectl/cmd/replace_test.go @@ -22,22 +22,24 @@ import ( "strings" "testing" + "k8s.io/kubernetes/pkg/client/typed/dynamic" "k8s.io/kubernetes/pkg/client/unversioned/fake" ) func TestReplaceObject(t *testing.T) { _, _, rc := testData() - f, tf, codec, ns := NewAPIFactory() + f, tf, codec, _ := NewAPIFactory() + ns := dynamic.ContentConfig().NegotiatedSerializer tf.Printer = &testPrinter{} tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/replicationcontrollers/redis-master" && (m == "GET" || m == "PUT" || m == "DELETE"): - return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil - case p == "/namespaces/test/replicationcontrollers" && m == "POST": - return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil + case p == "/namespaces/test/replicationcontrollers/redis-master" && (m == http.MethodGet || m == http.MethodPut || m == http.MethodDelete): + return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil + case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost: + return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil @@ -71,20 +73,21 @@ func TestReplaceObject(t *testing.T) { func TestReplaceMultipleObject(t *testing.T) { _, svc, rc := testData() - f, tf, codec, ns := NewAPIFactory() + f, tf, codec, _ := NewAPIFactory() + ns := dynamic.ContentConfig().NegotiatedSerializer tf.Printer = &testPrinter{} tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/replicationcontrollers/redis-master" && (m == "GET" || m == "PUT" || m == "DELETE"): - return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil - case p == "/namespaces/test/replicationcontrollers" && m == "POST": - return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil - case p == "/namespaces/test/services/frontend" && (m == "GET" || m == "PUT" || m == "DELETE"): - return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &svc.Items[0])}, nil - case p == "/namespaces/test/services" && m == "POST": - return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &svc.Items[0])}, nil + case p == "/namespaces/test/replicationcontrollers/redis-master" && (m == http.MethodGet || m == http.MethodPut || m == http.MethodDelete): + return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil + case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost: + return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil + case p == "/namespaces/test/services/frontend" && (m == http.MethodGet || m == http.MethodPut || m == http.MethodDelete): + return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &svc.Items[0])}, nil + case p == "/namespaces/test/services" && m == http.MethodPost: + return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil @@ -118,16 +121,17 @@ func TestReplaceMultipleObject(t *testing.T) { func TestReplaceDirectory(t *testing.T) { _, _, rc := testData() - f, tf, codec, ns := NewAPIFactory() + f, tf, codec, _ := NewAPIFactory() + ns := dynamic.ContentConfig().NegotiatedSerializer tf.Printer = &testPrinter{} tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers/") && (m == "GET" || m == "PUT" || m == "DELETE"): - return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil - case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers") && m == "POST": - return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil + case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers/") && (m == http.MethodGet || m == http.MethodPut || m == http.MethodDelete): + return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil + case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers") && m == http.MethodPost: + return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil @@ -161,16 +165,17 @@ func TestReplaceDirectory(t *testing.T) { func TestForceReplaceObjectNotFound(t *testing.T) { _, _, rc := testData() - f, tf, codec, ns := NewAPIFactory() + f, tf, codec, _ := NewAPIFactory() + ns := dynamic.ContentConfig().NegotiatedSerializer tf.Printer = &testPrinter{} tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE": - return &http.Response{StatusCode: 404, Header: defaultHeader(), Body: stringBody("")}, nil - case p == "/namespaces/test/replicationcontrollers" && m == "POST": - return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil + case p == "/namespaces/test/replicationcontrollers/redis-master" && m == http.MethodDelete: + return &http.Response{StatusCode: http.StatusNotFound, Header: defaultHeader(), Body: stringBody("")}, nil + case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost: + return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil