Skip to content

Commit

Permalink
make kubectl get generic with respect to objects
Browse files Browse the repository at this point in the history
  • Loading branch information
deads2k committed Nov 2, 2016
1 parent d613916 commit 90d9fa4
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 180 deletions.
15 changes: 15 additions & 0 deletions pkg/api/meta/help.go
Expand Up @@ -39,6 +39,14 @@ func GetItemsPtr(list runtime.Object) (interface{}, error) {
if err != nil {
return nil, err
}

// if we're a runtime.Unstructured, check to see if we have an `items` key
if unstructured, ok := list.(*runtime.Unstructured); ok {
if items, ok := unstructured.Object["items"]; ok {
return items, nil
}
}

items := v.FieldByName("Items")
if !items.IsValid() {
return nil, fmt.Errorf("no Items field in %#v", list)
Expand Down Expand Up @@ -117,6 +125,13 @@ func SetList(list runtime.Object, objects []runtime.Object) error {
slice := reflect.MakeSlice(items.Type(), len(objects), len(objects))
for i := range objects {
dest := slice.Index(i)

// check to see if you're directly assignable
if reflect.TypeOf(objects[i]).AssignableTo(dest.Type()) {
dest.Set(reflect.ValueOf(objects[i]))
continue
}

src, err := conversion.EnforcePtr(objects[i])
if err != nil {
return err
Expand Down
77 changes: 37 additions & 40 deletions pkg/kubectl/cmd/get.go
Expand Up @@ -149,7 +149,10 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [
selector := cmdutil.GetFlagString(cmd, "selector")
allNamespaces := cmdutil.GetFlagBool(cmd, "all-namespaces")
showKind := cmdutil.GetFlagBool(cmd, "show-kind")
mapper, typer := f.Object()
mapper, typer, err := f.UnstructuredObject()
if err != nil {
return err
}
printAll := false
filterFuncs := f.DefaultResourceFilterFunc()
filterOpts := f.DefaultResourceFilterOptions(cmd, allNamespaces)
Expand Down Expand Up @@ -196,7 +199,7 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [
// handle watch separately since we cannot watch multiple resource types
isWatch, isWatchOnly := cmdutil.GetFlagBool(cmd, "watch"), cmdutil.GetFlagBool(cmd, "watch-only")
if isWatch || isWatchOnly {
r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)).
r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), runtime.UnstructuredJSONScheme).
NamespaceParam(cmdNamespace).DefaultNamespace().AllNamespaces(allNamespaces).
FilenameParam(enforceNamespace, &options.FilenameOptions).
SelectorParam(selector).
Expand Down Expand Up @@ -281,7 +284,7 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [
return nil
}

r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)).
r := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), runtime.UnstructuredJSONScheme).
NamespaceParam(cmdNamespace).DefaultNamespace().AllNamespaces(allNamespaces).
FilenameParam(enforceNamespace, &options.FilenameOptions).
SelectorParam(selector).
Expand All @@ -302,18 +305,10 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [
}

if generic {
clientConfig, err := f.ClientConfig()
if err != nil {
return err
}

// the outermost object will be converted to the output-version, but inner
// objects can use their mappings
version, err := cmdutil.OutputVersion(cmd, clientConfig.GroupVersion)
if err != nil {
return err
}

// we flattened the data from the builder, so we have individual items, but now we'd like to either:
// 1. if there is more than one item, combine them all into a single list
// 2. if there is a single item and that item is a list, leave it as its specific list
// 3. if there is a single item and it is not a a list, leave it as a single item
var errs []error
singular := false
infos, err := r.IntoSingular(&singular).Infos()
Expand All @@ -332,9 +327,22 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [
res = infos[0].ResourceMapping().Resource
}

obj, err := resource.AsVersionedObject(infos, !singular, version, f.JSONEncoder())
if err != nil {
return err
var obj runtime.Object
if singular {
obj = infos[0].Object
} else {
// we have more than one item, so coerce all items into a list
list := &runtime.UnstructuredList{
Object: map[string]interface{}{
"kind": "List",
"apiVersion": "v1",
"metadata": map[string]interface{}{},
},
}
for _, info := range infos {
list.Items = append(list.Items, info.Object.(*runtime.Unstructured))
}
obj = list
}

isList := meta.IsListType(obj)
Expand All @@ -343,11 +351,18 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [
if err != nil {
return err
}
filteredObj, err := cmdutil.ObjectListToVersionedObject(items, version)
if err != nil {
return err
// take the filtered items and create a new list for display
list := &runtime.UnstructuredList{
Object: map[string]interface{}{
"kind": "List",
"apiVersion": "v1",
"metadata": map[string]interface{}{},
},
}
for _, item := range items {
list.Items = append(list.Items, item.(*runtime.Unstructured))
}
if err := printer.PrintObj(filteredObj, out); err != nil {
if err := printer.PrintObj(obj, out); err != nil {
errs = append(errs, err)
}

Expand Down Expand Up @@ -390,24 +405,6 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [
}
var sorter *kubectl.RuntimeSort
if len(sorting) > 0 && len(objs) > 1 {
clientConfig, err := f.ClientConfig()
if err != nil {
return err
}

version, err := cmdutil.OutputVersion(cmd, clientConfig.GroupVersion)
if err != nil {
return err
}

for ix := range infos {
objs[ix], err = infos[ix].Mapping.ConvertToVersion(infos[ix].Object, version)
if err != nil {
allErrs = append(allErrs, err)
continue
}
}

// TODO: questionable
if sorter, err = kubectl.SortObjects(f.Decoder(true), objs, sorting); err != nil {
return err
Expand Down
3 changes: 2 additions & 1 deletion pkg/kubectl/cmd/get_test.go
Expand Up @@ -120,7 +120,8 @@ func testComponentStatusData() *api.ComponentStatusList {

// Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get.
func TestGetUnknownSchemaObject(t *testing.T) {
f, tf, codec, ns := cmdtesting.NewTestFactory()
f, tf, _, ns := cmdtesting.NewAPIFactory()
_, _, codec, _ := cmdtesting.NewTestFactory()
tf.Printer = &testPrinter{}
tf.Client = &fake.RESTClient{
NegotiatedSerializer: ns,
Expand Down
6 changes: 5 additions & 1 deletion pkg/kubectl/cmd/testing/fake.go
Expand Up @@ -182,7 +182,11 @@ func (f *FakeFactory) Object() (meta.RESTMapper, runtime.ObjectTyper) {
}

func (f *FakeFactory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper, error) {
return nil, nil, nil
groupResources := testDynamicResources()
mapper := discovery.NewRESTMapper(groupResources, meta.InterfacesForUnstructured)
typer := discovery.NewUnstructuredObjectTyper(groupResources)

return cmdutil.NewShortcutExpander(mapper, nil), typer, nil
}

func (f *FakeFactory) Decoder(bool) runtime.Decoder {
Expand Down
68 changes: 1 addition & 67 deletions pkg/kubectl/cmd/util/factory.go
Expand Up @@ -33,23 +33,19 @@ import (
"time"

"github.com/emicklei/go-restful/swagger"
"github.com/golang/glog"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

"k8s.io/kubernetes/federation/apis/federation"
"k8s.io/kubernetes/pkg/api"
apierrors "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/meta"
"k8s.io/kubernetes/pkg/api/service"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/api/validation"
"k8s.io/kubernetes/pkg/apimachinery"
"k8s.io/kubernetes/pkg/apimachinery/registered"
"k8s.io/kubernetes/pkg/apis/apps"
"k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/kubernetes/pkg/apis/extensions"
extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
"k8s.io/kubernetes/pkg/client/restclient"
Expand Down Expand Up @@ -339,21 +335,10 @@ func (f *factory) Object() (meta.RESTMapper, runtime.ObjectTyper) {
mapper := registered.RESTMapper()
discoveryClient, err := discovery.NewDiscoveryClientForConfig(cfg)
if err == nil {
// register third party resources with the api machinery groups. This probably should be done, but
// its consistent with old code, so we'll start with it.
if err := registerThirdPartyResources(discoveryClient); err != nil {
glog.V(1).Infof("Unable to register third party resources: %v", err)
}
// ThirdPartyResourceData is special. It's not discoverable, but needed for thirdparty resource listing
// TODO eliminate this once we're truly generic.
thirdPartyResourceDataMapper := meta.NewDefaultRESTMapper([]unversioned.GroupVersion{extensionsv1beta1.SchemeGroupVersion}, registered.InterfacesFor)
thirdPartyResourceDataMapper.Add(extensionsv1beta1.SchemeGroupVersion.WithKind("ThirdPartyResourceData"), meta.RESTScopeNamespace)

mapper = meta.FirstHitRESTMapper{
MultiRESTMapper: meta.MultiRESTMapper{
discovery.NewDeferredDiscoveryRESTMapper(discoveryClient, registered.InterfacesFor),
thirdPartyResourceDataMapper, // needed for TPR printing
registered.RESTMapper(), // hardcoded fall back
registered.RESTMapper(), // hardcoded fall back
},
}
}
Expand Down Expand Up @@ -1331,54 +1316,3 @@ func (f *factory) SuggestedPodTemplateResources() []unversioned.GroupResource {
{Resource: "replicaset"},
}
}

// registerThirdPartyResources inspects the discovery endpoint to find thirdpartyresources in the discovery doc
// and then registers them with the apimachinery code. I think this is done so that scheme/codec stuff works,
// but I really don't know. Feels like this code should go away once kubectl is completely generic for generic
// CRUD
func registerThirdPartyResources(discoveryClient discovery.DiscoveryInterface) error {
var versions []unversioned.GroupVersion
var gvks []unversioned.GroupVersionKind
var err error
retries := 3
for i := 0; i < retries; i++ {
versions, gvks, err = GetThirdPartyGroupVersions(discoveryClient)
// Retry if we got a NotFound error, because user may delete
// a thirdparty group when the GetThirdPartyGroupVersions is
// running.
if err == nil || !apierrors.IsNotFound(err) {
break
}
}
if err != nil {
return err
}

groupsMap := map[string][]unversioned.GroupVersion{}
for _, version := range versions {
groupsMap[version.Group] = append(groupsMap[version.Group], version)
}
for group, versionList := range groupsMap {
preferredExternalVersion := versionList[0]

thirdPartyMapper, err := kubectl.NewThirdPartyResourceMapper(versionList, getGroupVersionKinds(gvks, group))
if err != nil {
return err
}

accessor := meta.NewAccessor()
groupMeta := apimachinery.GroupMeta{
GroupVersion: preferredExternalVersion,
GroupVersions: versionList,
RESTMapper: thirdPartyMapper,
SelfLinker: runtime.SelfLinker(accessor),
InterfacesFor: makeInterfacesFor(versionList),
}
if err := registered.RegisterGroup(groupMeta); err != nil {
return err
}
registered.AddThirdPartyAPIGroupVersions(versionList...)
}

return nil
}
44 changes: 0 additions & 44 deletions pkg/kubectl/cmd/util/helpers.go
Expand Up @@ -34,7 +34,6 @@ import (
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apimachinery/registered"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/client/typed/discovery"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
"k8s.io/kubernetes/pkg/kubectl"
"k8s.io/kubernetes/pkg/kubectl/resource"
Expand Down Expand Up @@ -551,49 +550,6 @@ func ShouldRecord(cmd *cobra.Command, info *resource.Info) bool {
return GetRecordFlag(cmd) || (ContainsChangeCause(info) && !cmd.Flags().Changed("record"))
}

// GetThirdPartyGroupVersions returns the thirdparty "group/versions"s and
// resources supported by the server. A user may delete a thirdparty resource
// when this function is running, so this function may return a "NotFound" error
// due to the race.
func GetThirdPartyGroupVersions(discovery discovery.DiscoveryInterface) ([]unversioned.GroupVersion, []unversioned.GroupVersionKind, error) {
result := []unversioned.GroupVersion{}
gvks := []unversioned.GroupVersionKind{}

groupList, err := discovery.ServerGroups()
if err != nil {
// On forbidden or not found, just return empty lists.
if kerrors.IsForbidden(err) || kerrors.IsNotFound(err) {
return result, gvks, nil
}

return nil, nil, err
}

for ix := range groupList.Groups {
group := &groupList.Groups[ix]
for jx := range group.Versions {
gv, err2 := unversioned.ParseGroupVersion(group.Versions[jx].GroupVersion)
if err2 != nil {
return nil, nil, err
}
// Skip GroupVersionKinds that have been statically registered.
if registered.IsRegisteredVersion(gv) {
continue
}
result = append(result, gv)

resourceList, err := discovery.ServerResourcesForGroupVersion(group.Versions[jx].GroupVersion)
if err != nil {
return nil, nil, err
}
for kx := range resourceList.APIResources {
gvks = append(gvks, gv.WithKind(resourceList.APIResources[kx].Kind))
}
}
}
return result, gvks, nil
}

func AddInclude3rdPartyFlags(cmd *cobra.Command) {
cmd.Flags().Bool("include-extended-apis", true, "If true, include definitions of new APIs via calls to the API server. [default true]")
cmd.Flags().MarkDeprecated("include-extended-apis", "No longer required.")
Expand Down
11 changes: 10 additions & 1 deletion pkg/kubectl/custom_column_printer.go
Expand Up @@ -206,9 +206,18 @@ func (s *CustomColumnsPrinter) printOneObject(obj runtime.Object, parsers []*jso
}
}
}

for ix := range parsers {
parser := parsers[ix]
values, err := parser.FindResults(reflect.ValueOf(obj).Elem().Interface())

var values [][]reflect.Value
var err error
if unstructured, ok := obj.(*runtime.Unstructured); ok {
values, err = parser.FindResults(unstructured.Object)
} else {
values, err = parser.FindResults(reflect.ValueOf(obj).Elem().Interface())
}

if err != nil {
return err
}
Expand Down
22 changes: 0 additions & 22 deletions pkg/kubectl/kubectl.go
Expand Up @@ -48,28 +48,6 @@ func makeImageList(spec *api.PodSpec) string {
return strings.Join(listOfImages(spec), ",")
}

func NewThirdPartyResourceMapper(gvs []unversioned.GroupVersion, gvks []unversioned.GroupVersionKind) (meta.RESTMapper, error) {
mapper := meta.NewDefaultRESTMapper(gvs, func(gv unversioned.GroupVersion) (*meta.VersionInterfaces, error) {
for ix := range gvs {
if gvs[ix].Group == gv.Group && gvs[ix].Version == gv.Version {
return &meta.VersionInterfaces{
ObjectConvertor: api.Scheme,
MetadataAccessor: meta.NewAccessor(),
}, nil
}
}
groupVersions := make([]string, 0, len(gvs))
for ix := range gvs {
groupVersions = append(groupVersions, gvs[ix].String())
}
return nil, fmt.Errorf("unsupported storage version: %s (valid: %s)", gv.String(), strings.Join(groupVersions, ", "))
})
for ix := range gvks {
mapper.Add(gvks[ix], meta.RESTScopeNamespace)
}
return mapper, nil
}

// OutputVersionMapper is a RESTMapper that will prefer mappings that
// correspond to a preferred output version (if feasible)
type OutputVersionMapper struct {
Expand Down

0 comments on commit 90d9fa4

Please sign in to comment.