1/* 2Copyright 2018 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package apiresources 18 19import ( 20 "bytes" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "os" 25 "sort" 26 "strings" 27 28 "github.com/spf13/cobra" 29 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 "k8s.io/apimachinery/pkg/util/errors" 33 "k8s.io/apimachinery/pkg/util/sets" 34 "k8s.io/cli-runtime/pkg/genericclioptions" 35 "k8s.io/cli-runtime/pkg/printers" 36 cmdutil "k8s.io/kubectl/pkg/cmd/util" 37 "k8s.io/kubectl/pkg/util/i18n" 38 "k8s.io/kubectl/pkg/util/templates" 39) 40 41var ( 42 apiresourcesExample = templates.Examples(` 43 # Print the supported API resources 44 kubectl api-resources 45 46 # Print the supported API resources with more information 47 kubectl api-resources -o wide 48 49 # Print the supported API resources sorted by a column 50 kubectl api-resources --sort-by=name 51 52 # Print the supported namespaced resources 53 kubectl api-resources --namespaced=true 54 55 # Print the supported non-namespaced resources 56 kubectl api-resources --namespaced=false 57 58 # Print the supported API resources with a specific APIGroup 59 kubectl api-resources --api-group=extensions`) 60) 61 62// APIResourceOptions is the start of the data required to perform the operation. 63// As new fields are added, add them here instead of referencing the cmd.Flags() 64type APIResourceOptions struct { 65 Output string 66 SortBy string 67 APIGroup string 68 Namespaced bool 69 Verbs []string 70 NoHeaders bool 71 Cached bool 72 73 genericclioptions.IOStreams 74} 75 76// groupResource contains the APIGroup and APIResource 77type groupResource struct { 78 APIGroup string 79 APIGroupVersion string 80 APIResource metav1.APIResource 81} 82 83// NewAPIResourceOptions creates the options for APIResource 84func NewAPIResourceOptions(ioStreams genericclioptions.IOStreams) *APIResourceOptions { 85 return &APIResourceOptions{ 86 IOStreams: ioStreams, 87 Namespaced: true, 88 } 89} 90 91// NewCmdAPIResources creates the `api-resources` command 92func NewCmdAPIResources(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { 93 o := NewAPIResourceOptions(ioStreams) 94 95 cmd := &cobra.Command{ 96 Use: "api-resources", 97 Short: i18n.T("Print the supported API resources on the server"), 98 Long: i18n.T("Print the supported API resources on the server."), 99 Example: apiresourcesExample, 100 Run: func(cmd *cobra.Command, args []string) { 101 cmdutil.CheckErr(o.Complete(cmd, args)) 102 cmdutil.CheckErr(o.Validate()) 103 cmdutil.CheckErr(o.RunAPIResources(cmd, f)) 104 }, 105 } 106 107 cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "When using the default or custom-column output format, don't print headers (default print headers).") 108 cmd.Flags().StringVarP(&o.Output, "output", "o", o.Output, "Output format. One of: wide|name.") 109 110 cmd.Flags().StringVar(&o.APIGroup, "api-group", o.APIGroup, "Limit to resources in the specified API group.") 111 cmd.Flags().BoolVar(&o.Namespaced, "namespaced", o.Namespaced, "If false, non-namespaced resources will be returned, otherwise returning namespaced resources by default.") 112 cmd.Flags().StringSliceVar(&o.Verbs, "verbs", o.Verbs, "Limit to resources that support the specified verbs.") 113 cmd.Flags().StringVar(&o.SortBy, "sort-by", o.SortBy, "If non-empty, sort list of resources using specified field. The field can be either 'name' or 'kind'.") 114 cmd.Flags().BoolVar(&o.Cached, "cached", o.Cached, "Use the cached list of resources if available.") 115 return cmd 116} 117 118// Validate checks to the APIResourceOptions to see if there is sufficient information run the command 119func (o *APIResourceOptions) Validate() error { 120 supportedOutputTypes := sets.NewString("", "wide", "name") 121 if !supportedOutputTypes.Has(o.Output) { 122 return fmt.Errorf("--output %v is not available", o.Output) 123 } 124 supportedSortTypes := sets.NewString("", "name", "kind") 125 if len(o.SortBy) > 0 { 126 if !supportedSortTypes.Has(o.SortBy) { 127 return fmt.Errorf("--sort-by accepts only name or kind") 128 } 129 } 130 return nil 131} 132 133// Complete adapts from the command line args and validates them 134func (o *APIResourceOptions) Complete(cmd *cobra.Command, args []string) error { 135 if len(args) != 0 { 136 return cmdutil.UsageErrorf(cmd, "unexpected arguments: %v", args) 137 } 138 return nil 139} 140 141// RunAPIResources does the work 142func (o *APIResourceOptions) RunAPIResources(cmd *cobra.Command, f cmdutil.Factory) error { 143 w := printers.GetNewTabWriter(o.Out) 144 defer w.Flush() 145 146 discoveryclient, err := f.ToDiscoveryClient() 147 if err != nil { 148 return err 149 } 150 151 if !o.Cached { 152 // Always request fresh data from the server 153 discoveryclient.Invalidate() 154 } 155 156 errs := []error{} 157 lists, err := discoveryclient.ServerPreferredResources() 158 if err != nil { 159 errs = append(errs, err) 160 } 161 162 resources := []groupResource{} 163 164 groupChanged := cmd.Flags().Changed("api-group") 165 nsChanged := cmd.Flags().Changed("namespaced") 166 167 for _, list := range lists { 168 if len(list.APIResources) == 0 { 169 continue 170 } 171 gv, err := schema.ParseGroupVersion(list.GroupVersion) 172 if err != nil { 173 continue 174 } 175 for _, resource := range list.APIResources { 176 if len(resource.Verbs) == 0 { 177 continue 178 } 179 // filter apiGroup 180 if groupChanged && o.APIGroup != gv.Group { 181 continue 182 } 183 // filter namespaced 184 if nsChanged && o.Namespaced != resource.Namespaced { 185 continue 186 } 187 // filter to resources that support the specified verbs 188 if len(o.Verbs) > 0 && !sets.NewString(resource.Verbs...).HasAll(o.Verbs...) { 189 continue 190 } 191 resources = append(resources, groupResource{ 192 APIGroup: gv.Group, 193 APIGroupVersion: gv.String(), 194 APIResource: resource, 195 }) 196 } 197 } 198 199 if o.NoHeaders == false && o.Output != "name" { 200 if err = printContextHeaders(w, o.Output); err != nil { 201 return err 202 } 203 } 204 205 sort.Stable(sortableResource{resources, o.SortBy}) 206 for _, r := range resources { 207 switch o.Output { 208 case "name": 209 name := r.APIResource.Name 210 if len(r.APIGroup) > 0 { 211 name += "." + r.APIGroup 212 } 213 if _, err := fmt.Fprintf(w, "%s\n", name); err != nil { 214 errs = append(errs, err) 215 } 216 case "wide": 217 if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\t%v\n", 218 r.APIResource.Name, 219 strings.Join(r.APIResource.ShortNames, ","), 220 r.APIGroupVersion, 221 r.APIResource.Namespaced, 222 r.APIResource.Kind, 223 r.APIResource.Verbs); err != nil { 224 errs = append(errs, err) 225 } 226 case "": 227 if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\n", 228 r.APIResource.Name, 229 strings.Join(r.APIResource.ShortNames, ","), 230 r.APIGroupVersion, 231 r.APIResource.Namespaced, 232 r.APIResource.Kind); err != nil { 233 errs = append(errs, err) 234 } 235 } 236 } 237 238 if len(errs) > 0 { 239 return errors.NewAggregate(errs) 240 } 241 return nil 242} 243 244func printContextHeaders(out io.Writer, output string) error { 245 columnNames := []string{"NAME", "SHORTNAMES", "APIVERSION", "NAMESPACED", "KIND"} 246 if output == "wide" { 247 columnNames = append(columnNames, "VERBS") 248 } 249 _, err := fmt.Fprintf(out, "%s\n", strings.Join(columnNames, "\t")) 250 return err 251} 252 253type sortableResource struct { 254 resources []groupResource 255 sortBy string 256} 257 258func (s sortableResource) Len() int { return len(s.resources) } 259func (s sortableResource) Swap(i, j int) { 260 s.resources[i], s.resources[j] = s.resources[j], s.resources[i] 261} 262func (s sortableResource) Less(i, j int) bool { 263 ret := strings.Compare(s.compareValues(i, j)) 264 if ret > 0 { 265 return false 266 } else if ret == 0 { 267 return strings.Compare(s.resources[i].APIResource.Name, s.resources[j].APIResource.Name) < 0 268 } 269 return true 270} 271 272func (s sortableResource) compareValues(i, j int) (string, string) { 273 switch s.sortBy { 274 case "name": 275 return s.resources[i].APIResource.Name, s.resources[j].APIResource.Name 276 case "kind": 277 return s.resources[i].APIResource.Kind, s.resources[j].APIResource.Kind 278 } 279 return s.resources[i].APIGroup, s.resources[j].APIGroup 280} 281 282// CompGetResourceList returns the list of api resources which begin with `toComplete`. 283func CompGetResourceList(f cmdutil.Factory, cmd *cobra.Command, toComplete string) []string { 284 buf := new(bytes.Buffer) 285 streams := genericclioptions.IOStreams{In: os.Stdin, Out: buf, ErrOut: ioutil.Discard} 286 o := NewAPIResourceOptions(streams) 287 288 // Get the list of resources 289 o.Output = "name" 290 o.Cached = true 291 o.Verbs = []string{"get"} 292 // TODO:Should set --request-timeout=5s 293 294 // Ignore errors as the output may still be valid 295 o.RunAPIResources(cmd, f) 296 297 // Resources can be a comma-separated list. The last element is then 298 // the one we should complete. For example if toComplete=="pods,secre" 299 // we should return "pods,secrets" 300 prefix := "" 301 suffix := toComplete 302 lastIdx := strings.LastIndex(toComplete, ",") 303 if lastIdx != -1 { 304 prefix = toComplete[0 : lastIdx+1] 305 suffix = toComplete[lastIdx+1:] 306 } 307 var comps []string 308 resources := strings.Split(buf.String(), "\n") 309 for _, res := range resources { 310 if res != "" && strings.HasPrefix(res, suffix) { 311 comps = append(comps, fmt.Sprintf("%s%s", prefix, res)) 312 } 313 } 314 return comps 315} 316