Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4b5fd43

Browse files
author
Kubernetes Submit Queue
authored
Merge pull request #30250 from krousey/kctl_dynamic
Automatic merge from submit-queue Change kubectl create to use dynamic client #16764 #3955 This is a series of changes to allow kubectl create to use discovery-based REST mapping and dynamic clients. cc @kubernetes/sig-api-machinery **Release note**: <!-- Steps to write your release note: 1. Use the release-note-* labels to set the release note state (if you have access) 2. Enter your extended release note in the below block; leaving it blank means using the PR title as the release note. If no release note is required, just write `NONE`. --> ```release-note kubectl will no longer do client-side defaulting on create and replace. ```
2 parents 7b762fb + b5235bc commit 4b5fd43

16 files changed

Lines changed: 358 additions & 113 deletions

File tree

pkg/api/meta/unstructured.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
Copyright 2016 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package meta
18+
19+
import (
20+
"k8s.io/kubernetes/pkg/api/unversioned"
21+
"k8s.io/kubernetes/pkg/runtime"
22+
)
23+
24+
// InterfacesForUnstructured returns VersionInterfaces suitable for
25+
// dealing with runtime.Unstructured objects.
26+
func InterfacesForUnstructured(unversioned.GroupVersion) (*VersionInterfaces, error) {
27+
return &VersionInterfaces{
28+
ObjectConvertor: &runtime.UnstructuredObjectConverter{},
29+
MetadataAccessor: NewAccessor(),
30+
}, nil
31+
}

pkg/client/typed/discovery/restmapper.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ func NewRESTMapper(groupResources []*APIGroupResources, versionInterfaces meta.V
8080
// TODO only do this if it supports listing
8181
versionMapper.Add(gv.WithKind(resource.Kind+"List"), scope)
8282
}
83+
// TODO why is this type not in discovery (at least for "v1")
84+
versionMapper.Add(gv.WithKind("List"), meta.RESTScopeRoot)
8385
unionMapper = append(unionMapper, versionMapper)
8486
}
8587
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
Copyright 2016 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package discovery
18+
19+
import (
20+
"fmt"
21+
22+
"k8s.io/kubernetes/pkg/api/unversioned"
23+
"k8s.io/kubernetes/pkg/runtime"
24+
)
25+
26+
// UnstructuredObjectTyper provides a runtime.ObjectTyper implmentation for
27+
// runtime.Unstructured object based on discovery information.
28+
type UnstructuredObjectTyper struct {
29+
registered map[unversioned.GroupVersionKind]bool
30+
}
31+
32+
// NewUnstructuredObjectTyper returns a runtime.ObjectTyper for
33+
// unstructred objects based on discovery information.
34+
func NewUnstructuredObjectTyper(groupResources []*APIGroupResources) *UnstructuredObjectTyper {
35+
dot := &UnstructuredObjectTyper{registered: make(map[unversioned.GroupVersionKind]bool)}
36+
for _, group := range groupResources {
37+
for _, discoveryVersion := range group.Group.Versions {
38+
resources, ok := group.VersionedResources[discoveryVersion.Version]
39+
if !ok {
40+
continue
41+
}
42+
43+
gv := unversioned.GroupVersion{Group: group.Group.Name, Version: discoveryVersion.Version}
44+
for _, resource := range resources {
45+
dot.registered[gv.WithKind(resource.Kind)] = true
46+
}
47+
}
48+
}
49+
return dot
50+
}
51+
52+
// ObjectKind returns the group,version,kind of the provided object, or an error
53+
// if the object in not *runtime.Unstructured or has no group,version,kind
54+
// information.
55+
func (d *UnstructuredObjectTyper) ObjectKind(obj runtime.Object) (unversioned.GroupVersionKind, error) {
56+
if _, ok := obj.(*runtime.Unstructured); !ok {
57+
return unversioned.GroupVersionKind{}, fmt.Errorf("type %T is invalid for dynamic object typer", obj)
58+
}
59+
60+
return obj.GetObjectKind().GroupVersionKind(), nil
61+
}
62+
63+
// ObjectKinds returns a slice of one element with the group,version,kind of the
64+
// provided object, or an error if the object is not *runtime.Unstructured or
65+
// has no group,version,kind information. unversionedType will always be false
66+
// because runtime.Unstructured object should always have group,version,kind
67+
// information set.
68+
func (d *UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) (gvks []unversioned.GroupVersionKind, unversionedType bool, err error) {
69+
gvk, err := d.ObjectKind(obj)
70+
if err != nil {
71+
return nil, false, err
72+
}
73+
74+
return []unversioned.GroupVersionKind{gvk}, false, nil
75+
}
76+
77+
// Recognizes returns true if the provided group,version,kind was in the
78+
// discovery information.
79+
func (d *UnstructuredObjectTyper) Recognizes(gvk unversioned.GroupVersionKind) bool {
80+
return d.registered[gvk]
81+
}
82+
83+
// IsUnversioned returns false always because *runtime.Unstructured objects
84+
// should always have group,version,kind information set. ok will be true if the
85+
// object's group,version,kind is registered.
86+
func (d *UnstructuredObjectTyper) IsUnversioned(obj runtime.Object) (unversioned bool, ok bool) {
87+
gvk, err := d.ObjectKind(obj)
88+
if err != nil {
89+
return false, false
90+
}
91+
92+
return false, d.registered[gvk]
93+
}
94+
95+
var _ runtime.ObjectTyper = &UnstructuredObjectTyper{}

pkg/client/typed/dynamic/client.go

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,7 @@ import (
4040
// Client is a Kubernetes client that allows you to access metadata
4141
// and manipulate metadata of a Kubernetes API group.
4242
type Client struct {
43-
cl *restclient.RESTClient
44-
}
45-
46-
type ClientWithParameterCodec struct {
47-
client *Client
43+
cl *restclient.RESTClient
4844
parameterCodec runtime.ParameterCodec
4945
}
5046

@@ -55,9 +51,12 @@ func NewClient(conf *restclient.Config) (*Client, error) {
5551
confCopy := *conf
5652
conf = &confCopy
5753

58-
// TODO: it's questionable that this should be using anything other than unstructured schema and JSON
59-
conf.ContentType = runtime.ContentTypeJSON
60-
conf.AcceptContentTypes = runtime.ContentTypeJSON
54+
contentConfig := ContentConfig()
55+
contentConfig.GroupVersion = conf.GroupVersion
56+
if conf.NegotiatedSerializer != nil {
57+
contentConfig.NegotiatedSerializer = conf.NegotiatedSerializer
58+
}
59+
conf.ContentConfig = contentConfig
6160

6261
if conf.APIPath == "" {
6362
conf.APIPath = "/api"
@@ -66,10 +65,6 @@ func NewClient(conf *restclient.Config) (*Client, error) {
6665
if len(conf.UserAgent) == 0 {
6766
conf.UserAgent = restclient.DefaultKubernetesUserAgent()
6867
}
69-
if conf.NegotiatedSerializer == nil {
70-
streamingInfo, _ := api.Codecs.StreamingSerializerForMediaType("application/json;stream=watch", nil)
71-
conf.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: dynamicCodec{}}, streamingInfo)
72-
}
7368

7469
cl, err := restclient.RESTClientFor(conf)
7570
if err != nil {
@@ -86,35 +81,24 @@ func (c *Client) GetRateLimiter() flowcontrol.RateLimiter {
8681

8782
// Resource returns an API interface to the specified resource for this client's
8883
// group and version. If resource is not a namespaced resource, then namespace
89-
// is ignored.
84+
// is ignored. The ResourceClient inherits the parameter codec of c.
9085
func (c *Client) Resource(resource *unversioned.APIResource, namespace string) *ResourceClient {
9186
return &ResourceClient{
92-
cl: c.cl,
93-
resource: resource,
94-
ns: namespace,
87+
cl: c.cl,
88+
resource: resource,
89+
ns: namespace,
90+
parameterCodec: c.parameterCodec,
9591
}
9692
}
9793

98-
// ParameterCodec wraps a parameterCodec around the Client.
99-
func (c *Client) ParameterCodec(parameterCodec runtime.ParameterCodec) *ClientWithParameterCodec {
100-
return &ClientWithParameterCodec{
101-
client: c,
94+
// ParameterCodec returns a client with the provided parameter codec.
95+
func (c *Client) ParameterCodec(parameterCodec runtime.ParameterCodec) *Client {
96+
return &Client{
97+
cl: c.cl,
10298
parameterCodec: parameterCodec,
10399
}
104100
}
105101

106-
// Resource returns an API interface to the specified resource for this client's
107-
// group and version. If resource is not a namespaced resource, then namespace
108-
// is ignored. The ResourceClient inherits the parameter codec of c.
109-
func (c *ClientWithParameterCodec) Resource(resource *unversioned.APIResource, namespace string) *ResourceClient {
110-
return &ResourceClient{
111-
cl: c.client.cl,
112-
resource: resource,
113-
ns: namespace,
114-
parameterCodec: c.parameterCodec,
115-
}
116-
}
117-
118102
// ResourceClient is an API interface to a specific resource under a
119103
// dynamic client.
120104
type ResourceClient struct {
@@ -255,6 +239,18 @@ func (dynamicCodec) Encode(obj runtime.Object, w io.Writer) error {
255239
return runtime.UnstructuredJSONScheme.Encode(obj, w)
256240
}
257241

242+
// ContentConfig returns a restclient.ContentConfig for dynamic types.
243+
func ContentConfig() restclient.ContentConfig {
244+
// TODO: it's questionable that this should be using anything other than unstructured schema and JSON
245+
codec := dynamicCodec{}
246+
streamingInfo, _ := api.Codecs.StreamingSerializerForMediaType("application/json;stream=watch", nil)
247+
return restclient.ContentConfig{
248+
AcceptContentTypes: runtime.ContentTypeJSON,
249+
ContentType: runtime.ContentTypeJSON,
250+
NegotiatedSerializer: serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec}, streamingInfo),
251+
}
252+
}
253+
258254
// paramaterCodec is a codec converts an API object to query
259255
// parameters without trying to convert to the target version.
260256
type parameterCodec struct{}

pkg/kubectl/cmd/cmd_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"k8s.io/kubernetes/pkg/api/unversioned"
3535
"k8s.io/kubernetes/pkg/api/validation"
3636
"k8s.io/kubernetes/pkg/client/restclient"
37+
"k8s.io/kubernetes/pkg/client/typed/discovery"
3738
client "k8s.io/kubernetes/pkg/client/unversioned"
3839
"k8s.io/kubernetes/pkg/client/unversioned/fake"
3940
"k8s.io/kubernetes/pkg/kubectl"
@@ -279,6 +280,13 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec, runtime.Neg
279280
Object: func(discovery bool) (meta.RESTMapper, runtime.ObjectTyper) {
280281
return testapi.Default.RESTMapper(), api.Scheme
281282
},
283+
UnstructuredObject: func() (meta.RESTMapper, runtime.ObjectTyper, error) {
284+
groupResources := testDynamicResources()
285+
mapper := discovery.NewRESTMapper(groupResources, meta.InterfacesForUnstructured)
286+
typer := discovery.NewUnstructuredObjectTyper(groupResources)
287+
288+
return kubectl.ShortcutExpander{RESTMapper: mapper}, typer, nil
289+
},
282290
Client: func() (*client.Client, error) {
283291
// Swap out the HTTP client out of the client with the fake's version.
284292
fakeClient := t.Client.(*fake.RESTClient)
@@ -290,6 +298,9 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec, runtime.Neg
290298
ClientForMapping: func(*meta.RESTMapping) (resource.RESTClient, error) {
291299
return t.Client, t.Err
292300
},
301+
UnstructuredClientForMapping: func(*meta.RESTMapping) (resource.RESTClient, error) {
302+
return t.Client, t.Err
303+
},
293304
Decoder: func(bool) runtime.Decoder {
294305
return testapi.Default.Codec()
295306
},

pkg/kubectl/cmd/create.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"k8s.io/kubernetes/pkg/kubectl"
2828
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
2929
"k8s.io/kubernetes/pkg/kubectl/resource"
30+
"k8s.io/kubernetes/pkg/runtime"
3031
)
3132

3233
// 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
107108
return err
108109
}
109110

110-
mapper, typer := f.Object(cmdutil.GetIncludeThirdPartyAPIs(cmd))
111-
r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)).
111+
mapper, typer, err := f.UnstructuredObject()
112+
if err != nil {
113+
return err
114+
}
115+
r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), runtime.UnstructuredJSONScheme).
112116
Schema(schema).
113117
ContinueOnError().
114118
NamespaceParam(cmdNamespace).DefaultNamespace().

pkg/kubectl/cmd/create_test.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"net/http"
2222
"testing"
2323

24+
"k8s.io/kubernetes/pkg/client/typed/dynamic"
2425
"k8s.io/kubernetes/pkg/client/unversioned/fake"
2526
)
2627

@@ -40,14 +41,15 @@ func TestCreateObject(t *testing.T) {
4041
_, _, rc := testData()
4142
rc.Items[0].Name = "redis-master-controller"
4243

43-
f, tf, codec, ns := NewAPIFactory()
44+
f, tf, codec, _ := NewAPIFactory()
45+
ns := dynamic.ContentConfig().NegotiatedSerializer
4446
tf.Printer = &testPrinter{}
4547
tf.Client = &fake.RESTClient{
4648
NegotiatedSerializer: ns,
4749
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
4850
switch p, m := req.URL.Path, req.Method; {
49-
case p == "/namespaces/test/replicationcontrollers" && m == "POST":
50-
return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil
51+
case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost:
52+
return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil
5153
default:
5254
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
5355
return nil, nil
@@ -72,16 +74,17 @@ func TestCreateMultipleObject(t *testing.T) {
7274
initTestErrorHandler(t)
7375
_, svc, rc := testData()
7476

75-
f, tf, codec, ns := NewAPIFactory()
77+
f, tf, codec, _ := NewAPIFactory()
78+
ns := dynamic.ContentConfig().NegotiatedSerializer
7679
tf.Printer = &testPrinter{}
7780
tf.Client = &fake.RESTClient{
7881
NegotiatedSerializer: ns,
7982
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
8083
switch p, m := req.URL.Path, req.Method; {
81-
case p == "/namespaces/test/services" && m == "POST":
82-
return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &svc.Items[0])}, nil
83-
case p == "/namespaces/test/replicationcontrollers" && m == "POST":
84-
return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil
84+
case p == "/namespaces/test/services" && m == http.MethodPost:
85+
return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &svc.Items[0])}, nil
86+
case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost:
87+
return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil
8588
default:
8689
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
8790
return nil, nil
@@ -108,14 +111,15 @@ func TestCreateDirectory(t *testing.T) {
108111
_, _, rc := testData()
109112
rc.Items[0].Name = "name"
110113

111-
f, tf, codec, ns := NewAPIFactory()
114+
f, tf, codec, _ := NewAPIFactory()
115+
ns := dynamic.ContentConfig().NegotiatedSerializer
112116
tf.Printer = &testPrinter{}
113117
tf.Client = &fake.RESTClient{
114118
NegotiatedSerializer: ns,
115119
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
116120
switch p, m := req.URL.Path, req.Method; {
117-
case p == "/namespaces/test/replicationcontrollers" && m == "POST":
118-
return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil
121+
case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost:
122+
return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil
119123
default:
120124
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
121125
return nil, nil

pkg/kubectl/cmd/get_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
apitesting "k8s.io/kubernetes/pkg/api/testing"
3434
"k8s.io/kubernetes/pkg/api/unversioned"
3535
"k8s.io/kubernetes/pkg/client/restclient"
36+
"k8s.io/kubernetes/pkg/client/typed/discovery"
3637
"k8s.io/kubernetes/pkg/client/unversioned/fake"
3738
"k8s.io/kubernetes/pkg/runtime"
3839
"k8s.io/kubernetes/pkg/runtime/serializer"
@@ -89,6 +90,26 @@ func testData() (*api.PodList, *api.ServiceList, *api.ReplicationControllerList)
8990
return pods, svc, rc
9091
}
9192

93+
func testDynamicResources() []*discovery.APIGroupResources {
94+
return []*discovery.APIGroupResources{
95+
{
96+
Group: unversioned.APIGroup{
97+
Versions: []unversioned.GroupVersionForDiscovery{
98+
{Version: "v1"},
99+
},
100+
PreferredVersion: unversioned.GroupVersionForDiscovery{Version: "v1"},
101+
},
102+
VersionedResources: map[string][]unversioned.APIResource{
103+
"v1": {
104+
{Name: "pods", Namespaced: true, Kind: "Pod"},
105+
{Name: "services", Namespaced: true, Kind: "Service"},
106+
{Name: "replicationcontrollers", Namespaced: true, Kind: "ReplicationController"},
107+
},
108+
},
109+
},
110+
}
111+
}
112+
92113
func testComponentStatusData() *api.ComponentStatusList {
93114
good := api.ComponentStatus{
94115
Conditions: []api.ComponentCondition{

0 commit comments

Comments
 (0)