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