Extend all to more resources

Added more things from the list here:
https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/cmd.go#L159

Update the devel/kubectl-conventions.md with the rules mentioned by
a few folks on which resources could be added to the special 'all' alias

Related to a suggestion in issue #22337
pull/6/head
Davanum Srinivas 2016-07-14 07:48:32 -04:00
parent 4fdde68f78
commit ccf4e4d61e
8 changed files with 151 additions and 46 deletions

View File

@ -43,6 +43,7 @@ Updated: 8/27/2015
- [Principles](#principles) - [Principles](#principles)
- [Command conventions](#command-conventions) - [Command conventions](#command-conventions)
- [Create commands](#create-commands) - [Create commands](#create-commands)
- [Rules for extending special resource alias - "all"](#rules-for-extending-special-resource-alias---all)
- [Flag conventions](#flag-conventions) - [Flag conventions](#flag-conventions)
- [Output conventions](#output-conventions) - [Output conventions](#output-conventions)
- [Documentation conventions](#documentation-conventions) - [Documentation conventions](#documentation-conventions)
@ -118,6 +119,21 @@ creating tls secrets. You create these as separate commands to get distinct
flags and separate help that is tailored for the particular usage. flags and separate help that is tailored for the particular usage.
### Rules for extending special resource alias - "all"
Here are the rules to add a new resource to the `kubectl get all` output.
* No cluster scoped resources
* No namespace admin level resources (limits, quota, policy, authorization
rules)
* No resources that are potentially unrecoverable (secrets and pvc)
* Resources that are considered "similar" to #3 should be grouped
the same (configmaps)
## Flag conventions ## Flag conventions
* Flags are all lowercase, with words separated by hyphens * Flags are all lowercase, with words separated by hyphens

View File

@ -1014,6 +1014,18 @@ __EOF__
exit 1 exit 1
fi fi
### Test kubectl get all
output_message=$(kubectl --v=6 --namespace default get all 2>&1 "${kube_flags[@]}")
# Post-condition: Check if we get 200 OK from all the url(s)
kube::test::if_has_string "${output_message}" "/api 200 OK"
kube::test::if_has_string "${output_message}" "/apis 200 OK"
kube::test::if_has_string "${output_message}" "/apis/apps/v1alpha1/namespaces/default/petsets 200 OK"
kube::test::if_has_string "${output_message}" "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers 200 OK"
kube::test::if_has_string "${output_message}" "/apis/extensions/v1beta1/namespaces/default/jobs 200 OK"
kube::test::if_has_string "${output_message}" "/apis/extensions/v1beta1/namespaces/default/deployments 200 OK"
kube::test::if_has_string "${output_message}" "/apis/extensions/v1beta1/namespaces/default/deployments 200 OK"
##################################### #####################################
# Third Party Resources # # Third Party Resources #
##################################### #####################################

View File

@ -88,9 +88,6 @@ func enableVersions(externalVersions []unversioned.GroupVersion) error {
return nil return nil
} }
// userResources is a group of resources mostly used by a kubectl user
var userResources = []string{"rc", "svc", "pods", "pvc"}
func newRESTMapper(externalVersions []unversioned.GroupVersion) meta.RESTMapper { func newRESTMapper(externalVersions []unversioned.GroupVersion) meta.RESTMapper {
// the list of kinds that are scoped at the root of the api hierarchy // the list of kinds that are scoped at the root of the api hierarchy
// if a kind is not enumerated here, it is assumed to have a namespace scope // if a kind is not enumerated here, it is assumed to have a namespace scope
@ -116,11 +113,7 @@ func newRESTMapper(externalVersions []unversioned.GroupVersion) meta.RESTMapper
"ThirdPartyResourceData", "ThirdPartyResourceData",
"ThirdPartyResourceList") "ThirdPartyResourceList")
mapper := api.NewDefaultRESTMapper(externalVersions, interfacesFor, importPrefix, ignoredKinds, rootScoped) return api.NewDefaultRESTMapper(externalVersions, interfacesFor, importPrefix, ignoredKinds, rootScoped)
// setup aliases for groups of resources
mapper.AddResourceAlias("all", userResources...)
return mapper
} }
// InterfacesFor returns the default Codec and ResourceVersioner for a given version // InterfacesFor returns the default Codec and ResourceVersioner for a given version

View File

@ -230,12 +230,53 @@ func makeInterfacesFor(versionList []unversioned.GroupVersion) func(version unve
} }
} }
func DiscoveryRESTMapper(clients *ClientCache, delegate meta.RESTMapper) kubectl.ShortcutExpander {
defaultMapper := kubectl.NewShortcutExpander(delegate)
if clients == nil {
return defaultMapper
}
client, err := clients.ClientForVersion(&unversioned.GroupVersion{Version: "v1"})
if err != nil {
return defaultMapper
}
// Check if we have access to server resources
apiResources, err := client.Discovery().ServerResources()
if err != nil {
return defaultMapper
}
availableResources := []unversioned.GroupVersionResource{}
for groupVersionString, resourceList := range apiResources {
currVersion, err := unversioned.ParseGroupVersion(groupVersionString)
if err != nil {
return defaultMapper
}
for _, resource := range resourceList.APIResources {
availableResources = append(availableResources, currVersion.WithResource(resource.Name))
}
}
availableAll := []unversioned.GroupResource{}
for _, requestedResource := range defaultMapper.All {
for _, availableResource := range availableResources {
if requestedResource.Group == availableResource.Group &&
requestedResource.Resource == availableResource.Resource {
availableAll = append(availableAll, requestedResource)
break
}
}
}
return kubectl.ShortcutExpander{All: availableAll, RESTMapper: delegate}
}
// NewFactory creates a factory with the default Kubernetes resources defined // NewFactory creates a factory with the default Kubernetes resources defined
// if optionalClientConfig is nil, then flags will be bound to a new clientcmd.ClientConfig. // if optionalClientConfig is nil, then flags will be bound to a new clientcmd.ClientConfig.
// if optionalClientConfig is not nil, then this factory will make use of it. // if optionalClientConfig is not nil, then this factory will make use of it.
func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory {
mapper := kubectl.ShortcutExpander{RESTMapper: registered.RESTMapper()}
flags := pflag.NewFlagSet("", pflag.ContinueOnError) flags := pflag.NewFlagSet("", pflag.ContinueOnError)
flags.SetNormalizeFunc(utilflag.WarnWordSepNormalizeFunc) // Warn for "_" flags flags.SetNormalizeFunc(utilflag.WarnWordSepNormalizeFunc) // Warn for "_" flags
@ -245,6 +286,7 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory {
} }
clients := NewClientCache(clientConfig) clients := NewClientCache(clientConfig)
mapper := DiscoveryRESTMapper(clients, registered.RESTMapper())
return &Factory{ return &Factory{
clients: clients, clients: clients,

View File

@ -32,6 +32,7 @@ import (
"time" "time"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/meta"
"k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/api/validation"
@ -42,6 +43,7 @@ import (
"k8s.io/kubernetes/pkg/client/unversioned/testclient" "k8s.io/kubernetes/pkg/client/unversioned/testclient"
"k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/kubectl" "k8s.io/kubernetes/pkg/kubectl"
"k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/labels"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/flag" "k8s.io/kubernetes/pkg/util/flag"
@ -714,3 +716,43 @@ func TestMakePortsString(t *testing.T) {
} }
} }
} }
func fakeClient() resource.ClientMapper {
return resource.ClientMapperFunc(func(*meta.RESTMapping) (resource.RESTClient, error) {
return &fake.RESTClient{}, nil
})
}
func TestReplaceAliases(t *testing.T) {
tests := []struct {
name string
arg string
expected string
}{
{
name: "no-replacement",
arg: "service",
expected: "service",
},
{
name: "all-replacement",
arg: "all",
expected: "pods,replicationcontrollers,services,petsets,horizontalpodautoscalers,jobs,deployments,replicasets",
},
{
name: "alias-in-comma-separated-arg",
arg: "all,secrets",
expected: "pods,replicationcontrollers,services,petsets,horizontalpodautoscalers,jobs,deployments,replicasets,secrets",
},
}
mapper := DiscoveryRESTMapper(nil, testapi.Default.RESTMapper())
b := resource.NewBuilder(mapper, api.Scheme, fakeClient(), testapi.Default.Codec())
for _, test := range tests {
replaced := b.ReplaceAliases(test.arg)
if replaced != test.expected {
t.Errorf("%s: unexpected argument: expected %s, got %s", test.name, test.expected, replaced)
}
}
}

View File

@ -38,6 +38,18 @@ serviceaccounts (aka 'sa'), ingresses (aka 'ing'), horizontalpodautoscalers (aka
componentstatuses (aka 'cs), endpoints (aka 'ep'), petsets (alpha feature, may be unstable) and secrets.` componentstatuses (aka 'cs), endpoints (aka 'ep'), petsets (alpha feature, may be unstable) and secrets.`
) )
// userResources is a group of resources mostly used by a kubectl user
var userResources = []unversioned.GroupResource{
{Group: "", Resource: "pods"},
{Group: "", Resource: "replicationcontrollers"},
{Group: "", Resource: "services"},
{Group: "apps", Resource: "petsets"},
{Group: "autoscaling", Resource: "horizontalpodautoscalers"},
{Group: "extensions", Resource: "jobs"},
{Group: "extensions", Resource: "deployments"},
{Group: "extensions", Resource: "replicasets"},
}
type NamespaceInfo struct { type NamespaceInfo struct {
Namespace string Namespace string
} }
@ -107,6 +119,15 @@ func (m OutputVersionMapper) RESTMapping(gk unversioned.GroupKind, versions ...s
// resources. It expands the resource first, then invokes the wrapped RESTMapper // resources. It expands the resource first, then invokes the wrapped RESTMapper
type ShortcutExpander struct { type ShortcutExpander struct {
RESTMapper meta.RESTMapper RESTMapper meta.RESTMapper
All []unversioned.GroupResource
}
func NewShortcutExpander(delegate meta.RESTMapper) ShortcutExpander {
return ShortcutExpander{
All: userResources,
RESTMapper: delegate,
}
} }
var _ meta.RESTMapper = &ShortcutExpander{} var _ meta.RESTMapper = &ShortcutExpander{}
@ -139,7 +160,19 @@ func (e ShortcutExpander) ResourceSingularizer(resource string) (string, error)
return e.RESTMapper.ResourceSingularizer(expandResourceShortcut(unversioned.GroupVersionResource{Resource: resource}).Resource) return e.RESTMapper.ResourceSingularizer(expandResourceShortcut(unversioned.GroupVersionResource{Resource: resource}).Resource)
} }
// AliasesForResource returns whether a resource has an alias or not
func (e ShortcutExpander) AliasesForResource(resource string) ([]string, bool) { func (e ShortcutExpander) AliasesForResource(resource string) ([]string, bool) {
if strings.ToLower(resource) == "all" {
var resources []unversioned.GroupResource
if resources = e.All; len(e.All) == 0 {
resources = userResources
}
aliases := []string{}
for _, r := range resources {
aliases = append(aliases, r.Resource)
}
return aliases, true
}
return e.RESTMapper.AliasesForResource(expandResourceShortcut(unversioned.GroupVersionResource{Resource: resource}).Resource) return e.RESTMapper.AliasesForResource(expandResourceShortcut(unversioned.GroupVersionResource{Resource: resource}).Resource)
} }

View File

@ -317,7 +317,7 @@ func (b *Builder) ResourceTypeOrNameArgs(allowEmptySelector bool, args ...string
} }
if len(args) > 0 { if len(args) > 0 {
// Try replacing aliases only in types // Try replacing aliases only in types
args[0] = b.replaceAliases(args[0]) args[0] = b.ReplaceAliases(args[0])
} }
switch { switch {
case len(args) > 2: case len(args) > 2:
@ -338,9 +338,9 @@ func (b *Builder) ResourceTypeOrNameArgs(allowEmptySelector bool, args ...string
return b return b
} }
// replaceAliases accepts an argument and tries to expand any existing // ReplaceAliases accepts an argument and tries to expand any existing
// aliases found in it // aliases found in it
func (b *Builder) replaceAliases(input string) string { func (b *Builder) ReplaceAliases(input string) string {
replaced := []string{} replaced := []string{}
for _, arg := range strings.Split(input, ",") { for _, arg := range strings.Split(input, ",") {
if aliases, ok := b.mapper.AliasesForResource(arg); ok { if aliases, ok := b.mapper.AliasesForResource(arg); ok {

View File

@ -1154,39 +1154,6 @@ func TestReceiveMultipleErrors(t *testing.T) {
} }
} }
func TestReplaceAliases(t *testing.T) {
tests := []struct {
name string
arg string
expected string
}{
{
name: "no-replacement",
arg: "service",
expected: "service",
},
{
name: "all-replacement",
arg: "all",
expected: "rc,svc,pods,pvc",
},
{
name: "alias-in-comma-separated-arg",
arg: "all,secrets",
expected: "rc,svc,pods,pvc,secrets",
},
}
b := NewBuilder(testapi.Default.RESTMapper(), api.Scheme, fakeClient(), testapi.Default.Codec())
for _, test := range tests {
replaced := b.replaceAliases(test.arg)
if replaced != test.expected {
t.Errorf("%s: unexpected argument: expected %s, got %s", test.name, test.expected, replaced)
}
}
}
func TestHasNames(t *testing.T) { func TestHasNames(t *testing.T) {
tests := []struct { tests := []struct {
args []string args []string