diff --git a/pkg/kubectl/cmd/auth/BUILD b/pkg/kubectl/cmd/auth/BUILD index 8824bf0f2f7..3f9a44e6cfb 100644 --- a/pkg/kubectl/cmd/auth/BUILD +++ b/pkg/kubectl/cmd/auth/BUILD @@ -34,6 +34,7 @@ go_library( "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/printers:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/resource:go_default_library", + "//staging/src/k8s.io/client-go/discovery:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/typed/rbac/v1:go_default_library", diff --git a/pkg/kubectl/cmd/auth/cani.go b/pkg/kubectl/cmd/auth/cani.go index 4aa28c18c45..52a66d9b7e8 100644 --- a/pkg/kubectl/cmd/auth/cani.go +++ b/pkg/kubectl/cmd/auth/cani.go @@ -33,6 +33,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" + discovery "k8s.io/client-go/discovery" authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" describeutil "k8s.io/kubernetes/pkg/kubectl/describe/versioned" @@ -44,11 +45,12 @@ import ( // CanIOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags() type CanIOptions struct { - AllNamespaces bool - Quiet bool - NoHeaders bool - Namespace string - AuthClient authorizationv1client.AuthorizationV1Interface + AllNamespaces bool + Quiet bool + NoHeaders bool + Namespace string + AuthClient authorizationv1client.AuthorizationV1Interface + DiscoveryClient discovery.DiscoveryInterface Verb string Resource schema.GroupVersionResource @@ -169,6 +171,7 @@ func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error { return err } o.AuthClient = client.AuthorizationV1() + o.DiscoveryClient = client.Discovery() o.Namespace = "" if !o.AllNamespaces { o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() @@ -196,6 +199,14 @@ func (o *CanIOptions) Validate() error { if o.Resource != (schema.GroupVersionResource{}) || o.ResourceName != "" { return fmt.Errorf("NonResourceURL and ResourceName can not specified together") } + } else if !o.Resource.Empty() && !o.AllNamespaces && o.DiscoveryClient != nil { + if namespaced, err := isNamespaced(o.Resource, o.DiscoveryClient); err == nil && !namespaced { + if len(o.Resource.Group) == 0 { + fmt.Fprintf(o.ErrOut, "Warning: resource '%s' is not namespace scoped\n", o.Resource.Resource) + } else { + fmt.Fprintf(o.ErrOut, "Warning: resource '%s' is not namespace scoped in group '%s'\n", o.Resource.Resource, o.Resource.Group) + } + } } if o.NoHeaders { @@ -360,3 +371,23 @@ func printAccess(out io.Writer, rules []rbacv1.PolicyRule) error { } return nil } + +func isNamespaced(gvr schema.GroupVersionResource, discoveryClient discovery.DiscoveryInterface) (bool, error) { + if gvr.Resource == "*" { + return true, nil + } + apiResourceList, err := discoveryClient.ServerResourcesForGroupVersion(schema.GroupVersion{ + Group: gvr.Group, Version: gvr.Version, + }.String()) + if err != nil { + return true, err + } + + for _, resource := range apiResourceList.APIResources { + if resource.Name == gvr.Resource { + return resource.Namespaced, nil + } + } + + return false, fmt.Errorf("the server doesn't have a resource type '%s' in group '%s'", gvr.Resource, gvr.Group) +} diff --git a/test/cmd/legacy-script.sh b/test/cmd/legacy-script.sh index a30a3bb4e4e..83d8658e4d2 100755 --- a/test/cmd/legacy-script.sh +++ b/test/cmd/legacy-script.sh @@ -761,6 +761,27 @@ runTests() { output_message=$(kubectl auth can-i get pods --subresource=log --quiet 2>&1 "${kube_flags[@]}"; echo $?) kube::test::if_has_string "${output_message}" '0' + + # kubectl auth can-i get '*' does not warn about namespaced scope or print an error + output_message=$(kubectl auth can-i get '*' 2>&1 "${kube_flags[@]}") + kube::test::if_has_not_string "${output_message}" "Warning" + + # kubectl auth can-i get foo does not print a namespaced warning message, and only prints a single lookup error + output_message=$(kubectl auth can-i get foo 2>&1 "${kube_flags[@]}") + kube::test::if_has_string "${output_message}" "Warning: the server doesn't have a resource type 'foo'" + kube::test::if_has_not_string "${output_message}" "Warning: resource 'foo' is not namespace scoped" + + # kubectl auth can-i get pods does not print a namespaced warning message or a lookup error + output_message=$(kubectl auth can-i get pods 2>&1 "${kube_flags[@]}") + kube::test::if_has_not_string "${output_message}" "Warning" + + # kubectl auth can-i get nodes prints a namespaced warning message + output_message=$(kubectl auth can-i get nodes 2>&1 "${kube_flags[@]}") + kube::test::if_has_string "${output_message}" "Warning: resource 'nodes' is not namespace scoped" + + # kubectl auth can-i get nodes --all-namespaces does not print a namespaced warning message + output_message=$(kubectl auth can-i get nodes --all-namespaces 2>&1 "${kube_flags[@]}") + kube::test::if_has_not_string "${output_message}" "Warning: resource 'nodes' is not namespace scoped" fi # kubectl auth reconcile