mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
Merge pull request #30250 from krousey/kctl_dynamic
Automatic merge from submit-queue Change kubectl create to use dynamic client https://github.com/kubernetes/kubernetes/issues/16764 https://github.com/kubernetes/kubernetes/issues/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. ```
This commit is contained in:
commit
4b5fd43e24
31
pkg/api/meta/unstructured.go
Normal file
31
pkg/api/meta/unstructured.go
Normal file
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
95
pkg/client/typed/discovery/unstructured.go
Normal file
95
pkg/client/typed/discovery/unstructured.go
Normal file
@ -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{}
|
@ -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
|
||||
}
|
||||
|
||||
@ -55,9 +51,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 +65,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 {
|
||||
@ -86,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 {
|
||||
@ -255,6 +239,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{}
|
||||
|
@ -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"
|
||||
@ -279,6 +280,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)
|
||||
@ -290,6 +298,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()
|
||||
},
|
||||
|
@ -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().
|
||||
|
@ -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
|
||||
|
@ -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{
|
||||
|
@ -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().
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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{})
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user