diff --git a/pkg/api/meta/priority.go b/pkg/api/meta/priority.go new file mode 100644 index 00000000000..24f38f78fd3 --- /dev/null +++ b/pkg/api/meta/priority.go @@ -0,0 +1,173 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package meta + +import ( + "fmt" + + "k8s.io/kubernetes/pkg/api/unversioned" +) + +const ( + AnyGroup = "*" + AnyVersion = "*" + AnyResource = "*" + AnyKind = "*" +) + +// PriorityRESTMapper is a wrapper for automatically choosing a particular Resource or Kind +// when multiple matches are possible +type PriorityRESTMapper struct { + // Delegate is the RESTMapper to use to locate all the Kind and Resource matches + Delegate RESTMapper + + // ResourcePriority is a list of priority patterns to apply to matching resources. + // The list of all matching resources is narrowed based on the patterns until only one remains. + // A pattern with no matches is skipped. A pattern with more than one match uses its + // matches as the list to continue matching against. + ResourcePriority []unversioned.GroupVersionResource + + // KindPriority is a list of priority patterns to apply to matching kinds. + // The list of all matching kinds is narrowed based on the patterns until only one remains. + // A pattern with no matches is skipped. A pattern with more than one match uses its + // matches as the list to continue matching against. + KindPriority []unversioned.GroupVersionKind +} + +func (m PriorityRESTMapper) String() string { + return fmt.Sprintf("PriorityRESTMapper{\n\t%v\n\t%v\n\t%v\n}", m.ResourcePriority, m.KindPriority, m.Delegate) +} + +// ResourceFor finds all resources, then passes them through the ResourcePriority patterns to find a single matching hit. +func (m PriorityRESTMapper) ResourceFor(partiallySpecifiedResource unversioned.GroupVersionResource) (unversioned.GroupVersionResource, error) { + originalGVRs, err := m.Delegate.ResourcesFor(partiallySpecifiedResource) + if err != nil { + return unversioned.GroupVersionResource{}, err + } + if len(originalGVRs) == 1 { + return originalGVRs[0], nil + } + + remainingGVRs := append([]unversioned.GroupVersionResource{}, originalGVRs...) + for _, pattern := range m.ResourcePriority { + matchedGVRs := []unversioned.GroupVersionResource{} + for _, gvr := range remainingGVRs { + if resourceMatches(pattern, gvr) { + matchedGVRs = append(matchedGVRs, gvr) + } + } + + switch len(matchedGVRs) { + case 0: + // if you have no matches, then nothing matched this pattern just move to the next + continue + case 1: + // one match, return + return matchedGVRs[0], nil + default: + // more than one match, use the matched hits as the list moving to the next pattern. + // this way you can have a series of selection criteria + remainingGVRs = matchedGVRs + } + } + + return unversioned.GroupVersionResource{}, &AmbiguousResourceError{PartialResource: partiallySpecifiedResource, MatchingResources: originalGVRs} +} + +// KindFor finds all kinds, then passes them through the KindPriority patterns to find a single matching hit. +func (m PriorityRESTMapper) KindFor(partiallySpecifiedResource unversioned.GroupVersionResource) (unversioned.GroupVersionKind, error) { + originalGVKs, err := m.Delegate.KindsFor(partiallySpecifiedResource) + if err != nil { + return unversioned.GroupVersionKind{}, err + } + if len(originalGVKs) == 1 { + return originalGVKs[0], nil + } + + remainingGVKs := append([]unversioned.GroupVersionKind{}, originalGVKs...) + for _, pattern := range m.KindPriority { + matchedGVKs := []unversioned.GroupVersionKind{} + for _, gvr := range remainingGVKs { + if kindMatches(pattern, gvr) { + matchedGVKs = append(matchedGVKs, gvr) + } + } + + switch len(matchedGVKs) { + case 0: + // if you have no matches, then nothing matched this pattern just move to the next + continue + case 1: + // one match, return + return matchedGVKs[0], nil + default: + // more than one match, use the matched hits as the list moving to the next pattern. + // this way you can have a series of selection criteria + remainingGVKs = matchedGVKs + } + } + + return unversioned.GroupVersionKind{}, &AmbiguousResourceError{PartialResource: partiallySpecifiedResource, MatchingKinds: originalGVKs} +} + +func resourceMatches(pattern unversioned.GroupVersionResource, resource unversioned.GroupVersionResource) bool { + if pattern.Group != AnyGroup && pattern.Group != resource.Group { + return false + } + if pattern.Version != AnyVersion && pattern.Version != resource.Version { + return false + } + if pattern.Resource != AnyResource && pattern.Resource != resource.Resource { + return false + } + + return true +} + +func kindMatches(pattern unversioned.GroupVersionKind, kind unversioned.GroupVersionKind) bool { + if pattern.Group != AnyGroup && pattern.Group != kind.Group { + return false + } + if pattern.Version != AnyVersion && pattern.Version != kind.Version { + return false + } + if pattern.Kind != AnyKind && pattern.Kind != kind.Kind { + return false + } + + return true +} + +func (m PriorityRESTMapper) RESTMapping(gk unversioned.GroupKind, versions ...string) (mapping *RESTMapping, err error) { + return m.Delegate.RESTMapping(gk, versions...) +} + +func (m PriorityRESTMapper) AliasesForResource(alias string) (aliases []string, ok bool) { + return m.Delegate.AliasesForResource(alias) +} + +func (m PriorityRESTMapper) ResourceSingularizer(resource string) (singular string, err error) { + return m.Delegate.ResourceSingularizer(resource) +} + +func (m PriorityRESTMapper) ResourcesFor(partiallySpecifiedResource unversioned.GroupVersionResource) ([]unversioned.GroupVersionResource, error) { + return m.Delegate.ResourcesFor(partiallySpecifiedResource) +} + +func (m PriorityRESTMapper) KindsFor(partiallySpecifiedResource unversioned.GroupVersionResource) (gvk []unversioned.GroupVersionKind, err error) { + return m.Delegate.KindsFor(partiallySpecifiedResource) +} diff --git a/pkg/api/meta/priority_test.go b/pkg/api/meta/priority_test.go new file mode 100644 index 00000000000..ea2d24b3725 --- /dev/null +++ b/pkg/api/meta/priority_test.go @@ -0,0 +1,206 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package meta + +import ( + "strings" + "testing" + + "k8s.io/kubernetes/pkg/api/unversioned" +) + +func TestPriorityRESTMapperResourceForErrorHandling(t *testing.T) { + tcs := []struct { + name string + + delegate RESTMapper + resourcePatterns []unversioned.GroupVersionResource + result unversioned.GroupVersionResource + err string + }{ + { + name: "single hit", + delegate: fixedRESTMapper{resourcesFor: []unversioned.GroupVersionResource{{Resource: "single-hit"}}}, + result: unversioned.GroupVersionResource{Resource: "single-hit"}, + }, + { + name: "ambiguous match", + delegate: fixedRESTMapper{resourcesFor: []unversioned.GroupVersionResource{ + {Group: "one", Version: "a", Resource: "first"}, + {Group: "two", Version: "b", Resource: "second"}, + }}, + err: "matches multiple resources", + }, + { + name: "group selection", + delegate: fixedRESTMapper{resourcesFor: []unversioned.GroupVersionResource{ + {Group: "one", Version: "a", Resource: "first"}, + {Group: "two", Version: "b", Resource: "second"}, + }}, + resourcePatterns: []unversioned.GroupVersionResource{ + {Group: "one", Version: AnyVersion, Resource: AnyResource}, + }, + result: unversioned.GroupVersionResource{Group: "one", Version: "a", Resource: "first"}, + }, + { + name: "empty match continues", + delegate: fixedRESTMapper{resourcesFor: []unversioned.GroupVersionResource{ + {Group: "one", Version: "a", Resource: "first"}, + {Group: "two", Version: "b", Resource: "second"}, + }}, + resourcePatterns: []unversioned.GroupVersionResource{ + {Group: "fail", Version: AnyVersion, Resource: AnyResource}, + {Group: "one", Version: AnyVersion, Resource: AnyResource}, + }, + result: unversioned.GroupVersionResource{Group: "one", Version: "a", Resource: "first"}, + }, + { + name: "group followed by version selection", + delegate: fixedRESTMapper{resourcesFor: []unversioned.GroupVersionResource{ + {Group: "one", Version: "a", Resource: "first"}, + {Group: "two", Version: "b", Resource: "second"}, + {Group: "one", Version: "c", Resource: "third"}, + }}, + resourcePatterns: []unversioned.GroupVersionResource{ + {Group: "one", Version: AnyVersion, Resource: AnyResource}, + {Group: AnyGroup, Version: "a", Resource: AnyResource}, + }, + result: unversioned.GroupVersionResource{Group: "one", Version: "a", Resource: "first"}, + }, + { + name: "resource selection", + delegate: fixedRESTMapper{resourcesFor: []unversioned.GroupVersionResource{ + {Group: "one", Version: "a", Resource: "first"}, + {Group: "one", Version: "a", Resource: "second"}, + }}, + resourcePatterns: []unversioned.GroupVersionResource{ + {Group: AnyGroup, Version: AnyVersion, Resource: "second"}, + }, + result: unversioned.GroupVersionResource{Group: "one", Version: "a", Resource: "second"}, + }, + } + + for _, tc := range tcs { + mapper := PriorityRESTMapper{Delegate: tc.delegate, ResourcePriority: tc.resourcePatterns} + + actualResult, actualErr := mapper.ResourceFor(unversioned.GroupVersionResource{}) + if e, a := tc.result, actualResult; e != a { + t.Errorf("%s: expected %v, got %v", tc.name, e, a) + } + if len(tc.err) == 0 && actualErr == nil { + continue + } + if len(tc.err) > 0 && actualErr == nil { + t.Errorf("%s: missing expected err: %v", tc.name, tc.err) + continue + } + if !strings.Contains(actualErr.Error(), tc.err) { + t.Errorf("%s: expected %v, got %v", tc.name, tc.err, actualErr) + } + } +} + +func TestPriorityRESTMapperKindForErrorHandling(t *testing.T) { + tcs := []struct { + name string + + delegate RESTMapper + kindPatterns []unversioned.GroupVersionKind + result unversioned.GroupVersionKind + err string + }{ + { + name: "single hit", + delegate: fixedRESTMapper{kindsFor: []unversioned.GroupVersionKind{{Kind: "single-hit"}}}, + result: unversioned.GroupVersionKind{Kind: "single-hit"}, + }, + { + name: "ambiguous match", + delegate: fixedRESTMapper{kindsFor: []unversioned.GroupVersionKind{ + {Group: "one", Version: "a", Kind: "first"}, + {Group: "two", Version: "b", Kind: "second"}, + }}, + err: "matches multiple kinds", + }, + { + name: "group selection", + delegate: fixedRESTMapper{kindsFor: []unversioned.GroupVersionKind{ + {Group: "one", Version: "a", Kind: "first"}, + {Group: "two", Version: "b", Kind: "second"}, + }}, + kindPatterns: []unversioned.GroupVersionKind{ + {Group: "one", Version: AnyVersion, Kind: AnyKind}, + }, + result: unversioned.GroupVersionKind{Group: "one", Version: "a", Kind: "first"}, + }, + { + name: "empty match continues", + delegate: fixedRESTMapper{kindsFor: []unversioned.GroupVersionKind{ + {Group: "one", Version: "a", Kind: "first"}, + {Group: "two", Version: "b", Kind: "second"}, + }}, + kindPatterns: []unversioned.GroupVersionKind{ + {Group: "fail", Version: AnyVersion, Kind: AnyKind}, + {Group: "one", Version: AnyVersion, Kind: AnyKind}, + }, + result: unversioned.GroupVersionKind{Group: "one", Version: "a", Kind: "first"}, + }, + { + name: "group followed by version selection", + delegate: fixedRESTMapper{kindsFor: []unversioned.GroupVersionKind{ + {Group: "one", Version: "a", Kind: "first"}, + {Group: "two", Version: "b", Kind: "second"}, + {Group: "one", Version: "c", Kind: "third"}, + }}, + kindPatterns: []unversioned.GroupVersionKind{ + {Group: "one", Version: AnyVersion, Kind: AnyKind}, + {Group: AnyGroup, Version: "a", Kind: AnyKind}, + }, + result: unversioned.GroupVersionKind{Group: "one", Version: "a", Kind: "first"}, + }, + { + name: "kind selection", + delegate: fixedRESTMapper{kindsFor: []unversioned.GroupVersionKind{ + {Group: "one", Version: "a", Kind: "first"}, + {Group: "one", Version: "a", Kind: "second"}, + }}, + kindPatterns: []unversioned.GroupVersionKind{ + {Group: AnyGroup, Version: AnyVersion, Kind: "second"}, + }, + result: unversioned.GroupVersionKind{Group: "one", Version: "a", Kind: "second"}, + }, + } + + for _, tc := range tcs { + mapper := PriorityRESTMapper{Delegate: tc.delegate, KindPriority: tc.kindPatterns} + + actualResult, actualErr := mapper.KindFor(unversioned.GroupVersionResource{}) + if e, a := tc.result, actualResult; e != a { + t.Errorf("%s: expected %v, got %v", tc.name, e, a) + } + if len(tc.err) == 0 && actualErr == nil { + continue + } + if len(tc.err) > 0 && actualErr == nil { + t.Errorf("%s: missing expected err: %v", tc.name, tc.err) + continue + } + if !strings.Contains(actualErr.Error(), tc.err) { + t.Errorf("%s: expected %v, got %v", tc.name, tc.err, actualErr) + } + } +} diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index 9319f19220e..9bbf3f0c3a7 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -180,7 +180,16 @@ func NewTestFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) { } return &cmdutil.Factory{ Object: func() (meta.RESTMapper, runtime.ObjectTyper) { - return t.Mapper, t.Typer + priorityRESTMapper := meta.PriorityRESTMapper{ + Delegate: t.Mapper, + ResourcePriority: []unversioned.GroupVersionResource{ + {Group: meta.AnyGroup, Version: "v1", Resource: meta.AnyResource}, + }, + KindPriority: []unversioned.GroupVersionKind{ + {Group: meta.AnyGroup, Version: "v1", Kind: meta.AnyKind}, + }, + } + return priorityRESTMapper, t.Typer }, ClientForMapping: func(*meta.RESTMapping) (resource.RESTClient, error) { return t.Client, t.Err @@ -212,7 +221,16 @@ func NewTestFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) { func NewMixedFactory(apiClient resource.RESTClient) (*cmdutil.Factory, *testFactory, runtime.Codec) { f, t, c := NewTestFactory() f.Object = func() (meta.RESTMapper, runtime.ObjectTyper) { - return meta.MultiRESTMapper{t.Mapper, testapi.Default.RESTMapper()}, runtime.MultiObjectTyper{t.Typer, api.Scheme} + priorityRESTMapper := meta.PriorityRESTMapper{ + Delegate: meta.MultiRESTMapper{t.Mapper, testapi.Default.RESTMapper()}, + ResourcePriority: []unversioned.GroupVersionResource{ + {Group: meta.AnyGroup, Version: "v1", Resource: meta.AnyResource}, + }, + KindPriority: []unversioned.GroupVersionKind{ + {Group: meta.AnyGroup, Version: "v1", Kind: meta.AnyKind}, + }, + } + return priorityRESTMapper, runtime.MultiObjectTyper{t.Typer, api.Scheme} } f.ClientForMapping = func(m *meta.RESTMapping) (resource.RESTClient, error) { if m.ObjectConvertor == api.Scheme { diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 26c78d35ed4..abbdb2880e3 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -43,6 +43,7 @@ import ( "k8s.io/kubernetes/pkg/apis/autoscaling" "k8s.io/kubernetes/pkg/apis/batch" "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/apis/metrics" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" client "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" @@ -202,7 +203,24 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { cmdApiVersion = *cfg.GroupVersion } - return kubectl.OutputVersionMapper{RESTMapper: mapper, OutputVersions: []unversioned.GroupVersion{cmdApiVersion}}, api.Scheme + outputRESTMapper := kubectl.OutputVersionMapper{RESTMapper: mapper, OutputVersions: []unversioned.GroupVersion{cmdApiVersion}} + + // eventually this should allow me choose a group priority based on the order of the discovery doc, for now hardcode a given order + priorityRESTMapper := meta.PriorityRESTMapper{ + Delegate: outputRESTMapper, + ResourcePriority: []unversioned.GroupVersionResource{ + {Group: api.GroupName, Version: meta.AnyVersion, Resource: meta.AnyResource}, + {Group: extensions.GroupName, Version: meta.AnyVersion, Resource: meta.AnyResource}, + {Group: metrics.GroupName, Version: meta.AnyVersion, Resource: meta.AnyResource}, + }, + KindPriority: []unversioned.GroupVersionKind{ + {Group: api.GroupName, Version: meta.AnyVersion, Kind: meta.AnyKind}, + {Group: extensions.GroupName, Version: meta.AnyVersion, Kind: meta.AnyKind}, + {Group: metrics.GroupName, Version: meta.AnyVersion, Kind: meta.AnyKind}, + }, + } + + return priorityRESTMapper, api.Scheme }, Client: func() (*client.Client, error) { return clients.ClientForVersion(nil)