1
16
17 package explain_test
18
19 import (
20 "errors"
21 "path/filepath"
22 "regexp"
23 "testing"
24
25 "k8s.io/apimachinery/pkg/api/meta"
26 sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing"
27 "k8s.io/cli-runtime/pkg/genericiooptions"
28 "k8s.io/client-go/discovery"
29 openapiclient "k8s.io/client-go/openapi"
30 "k8s.io/client-go/rest"
31 clienttestutil "k8s.io/client-go/util/testing"
32 "k8s.io/kubectl/pkg/cmd/explain"
33 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
34 cmdutil "k8s.io/kubectl/pkg/cmd/util"
35 "k8s.io/kubectl/pkg/util/openapi"
36 )
37
38 var (
39 testDataPath = filepath.Join("..", "..", "..", "testdata")
40 fakeSchema = sptest.Fake{Path: filepath.Join(testDataPath, "openapi", "swagger.json")}
41 FakeOpenAPISchema = testOpenAPISchema{
42 OpenAPISchemaFn: func() (openapi.Resources, error) {
43 s, err := fakeSchema.OpenAPISchema()
44 if err != nil {
45 return nil, err
46 }
47 return openapi.NewOpenAPIData(s)
48 },
49 }
50 )
51
52 type testOpenAPISchema struct {
53 OpenAPISchemaFn func() (openapi.Resources, error)
54 }
55
56 func TestExplainInvalidArgs(t *testing.T) {
57 tf := cmdtesting.NewTestFactory()
58 defer tf.Cleanup()
59
60 opts := explain.NewExplainOptions("kubectl", genericiooptions.NewTestIOStreamsDiscard())
61 cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard())
62 err := opts.Complete(tf, cmd, []string{})
63 if err != nil {
64 t.Fatalf("unexpected error %v", err)
65 }
66
67 err = opts.Validate()
68 if err.Error() != "You must specify the type of resource to explain. Use \"kubectl api-resources\" for a complete list of supported resources.\n" {
69 t.Error("unexpected non-error")
70 }
71
72 err = opts.Complete(tf, cmd, []string{"resource1", "resource2"})
73 if err != nil {
74 t.Fatalf("unexpected error %v", err)
75 }
76
77 err = opts.Validate()
78 if err.Error() != "We accept only this format: explain RESOURCE\n" {
79 t.Error("unexpected non-error")
80 }
81 }
82
83 func TestExplainNotExistResource(t *testing.T) {
84 tf := cmdtesting.NewTestFactory()
85 defer tf.Cleanup()
86
87 opts := explain.NewExplainOptions("kubectl", genericiooptions.NewTestIOStreamsDiscard())
88 cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard())
89 err := opts.Complete(tf, cmd, []string{"foo"})
90 if err != nil {
91 t.Fatalf("unexpected error %v", err)
92 }
93
94 err = opts.Validate()
95 if err != nil {
96 t.Fatalf("unexpected error %v", err)
97 }
98
99 err = opts.Run()
100 if _, ok := err.(*meta.NoResourceMatchError); !ok {
101 t.Fatalf("unexpected error %v", err)
102 }
103 }
104
105 type explainTestCase struct {
106 Name string
107 Args []string
108 Flags map[string]string
109 ExpectPattern []string
110 ExpectErrorPattern string
111
112
113
114 OpenAPIV3SchemaFn func() (openapiclient.Client, error)
115 }
116
117 var explainV2Cases = []explainTestCase{
118 {
119 Name: "Basic",
120 Args: []string{"pods"},
121 ExpectPattern: []string{`\s*KIND:[\t ]*Pod\s*`},
122 },
123 {
124 Name: "Recursive",
125 Args: []string{"pods"},
126 Flags: map[string]string{"recursive": "true"},
127 ExpectPattern: []string{`\s*KIND:[\t ]*Pod\s*`},
128 },
129 {
130 Name: "DefaultAPIVersion",
131 Args: []string{"horizontalpodautoscalers"},
132 Flags: map[string]string{"api-version": "autoscaling/v1"},
133 ExpectPattern: []string{`\s*VERSION:[\t ]*(v1|autoscaling/v1)\s*`},
134 },
135 {
136 Name: "NonExistingAPIVersion",
137 Args: []string{"pods"},
138 Flags: map[string]string{"api-version": "v99"},
139 ExpectErrorPattern: `couldn't find resource for \"/v99, (Kind=Pod|Resource=pods)\"`,
140 },
141 {
142 Name: "NonExistingResource",
143 Args: []string{"foo"},
144 ExpectErrorPattern: `the server doesn't have a resource type "foo"`,
145 },
146 }
147
148 func TestExplainOpenAPIV2(t *testing.T) {
149 runExplainTestCases(t, explainV2Cases)
150 }
151
152 func TestExplainOpenAPIV3(t *testing.T) {
153
154 fallbackV3SchemaFn := func() (openapiclient.Client, error) {
155 fakeDiscoveryClient := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: "https://not.a.real.site:65543/"})
156 return fakeDiscoveryClient.OpenAPIV3(), nil
157 }
158
159 cases := []explainTestCase{
160 {
161
162
163
164
165 Name: "Fallback",
166 Args: []string{"pods"},
167 ExpectPattern: []string{`\s*KIND:[\t ]*Pod\s*`},
168 OpenAPIV3SchemaFn: fallbackV3SchemaFn,
169 },
170 {
171 Name: "NonDefaultAPIVersion",
172 Args: []string{"horizontalpodautoscalers"},
173 Flags: map[string]string{"api-version": "autoscaling/v2"},
174 ExpectPattern: []string{`\s*VERSION:[\t ]*(v2|autoscaling/v2)\s*`},
175 },
176 {
177
178
179 Name: "OutputPlaintextV2",
180 Args: []string{"pods"},
181 Flags: map[string]string{"output": "plaintext-openapiv2"},
182 ExpectPattern: []string{`\s*KIND:[\t ]*Pod\s*`},
183 OpenAPIV3SchemaFn: fallbackV3SchemaFn,
184 },
185 }
186 cases = append(cases, explainV2Cases...)
187
188 runExplainTestCases(t, cases)
189 }
190
191 func runExplainTestCases(t *testing.T, cases []explainTestCase) {
192 fakeServer, err := clienttestutil.NewFakeOpenAPIV3Server(filepath.Join(testDataPath, "openapi", "v3"))
193 if err != nil {
194 t.Fatalf("error starting fake openapi server: %v", err.Error())
195 }
196 defer fakeServer.HttpServer.Close()
197
198 openapiV3SchemaFn := func() (openapiclient.Client, error) {
199 fakeDiscoveryClient := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: fakeServer.HttpServer.URL})
200 return fakeDiscoveryClient.OpenAPIV3(), nil
201 }
202
203 tf := cmdtesting.NewTestFactory()
204 defer tf.Cleanup()
205
206 tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn
207 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
208
209 ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams()
210
211 type catchFatal error
212
213 for _, tcase := range cases {
214
215 t.Run(tcase.Name, func(t *testing.T) {
216
217
218
219
220 cmdutil.BehaviorOnFatal(func(str string, code int) {
221 panic(catchFatal(errors.New(str)))
222 })
223 defer cmdutil.DefaultBehaviorOnFatal()
224
225 var err error
226
227 func() {
228 defer func() {
229
230
231 if panicErr := recover(); panicErr != nil {
232 if e := panicErr.(catchFatal); e != nil {
233 err = e
234 } else {
235 panic(panicErr)
236 }
237 }
238 }()
239
240 if tcase.OpenAPIV3SchemaFn != nil {
241 tf.OpenAPIV3ClientFunc = tcase.OpenAPIV3SchemaFn
242 } else {
243 tf.OpenAPIV3ClientFunc = openapiV3SchemaFn
244 }
245
246 cmd := explain.NewCmdExplain("kubectl", tf, ioStreams)
247 for k, v := range tcase.Flags {
248 if err := cmd.Flags().Set(k, v); err != nil {
249 t.Fatal(err)
250 }
251 }
252 cmd.Run(cmd, tcase.Args)
253 }()
254
255 for _, rexp := range tcase.ExpectPattern {
256 if matched, err := regexp.MatchString(rexp, buf.String()); err != nil || !matched {
257 if err != nil {
258 t.Error(err)
259 } else {
260 t.Errorf("expected output to match regex:\n\t%s\ninstead got:\n\t%s", rexp, buf.String())
261 }
262 }
263 }
264
265 if err != nil {
266 if matched, regexErr := regexp.MatchString(tcase.ExpectErrorPattern, err.Error()); len(tcase.ExpectErrorPattern) == 0 || regexErr != nil || !matched {
267 t.Fatalf("unexpected error: %s did not match regex %s (%v)", err.Error(),
268 tcase.ExpectErrorPattern, regexErr)
269 }
270 } else if len(tcase.ExpectErrorPattern) > 0 {
271 t.Fatalf("did not trigger expected error: %s in output:\n%s", tcase.ExpectErrorPattern, buf.String())
272 }
273 })
274
275 buf.Reset()
276 }
277 }
278
279
280 func panicOpenAPISchemaFn() (openapi.Resources, error) {
281 panic("should never be called")
282 }
283
284
285 func TestExplainOpenAPIV3DoesNotLoadOpenAPIV2Specs(t *testing.T) {
286
287 fakeServer, err := clienttestutil.NewFakeOpenAPIV3Server(filepath.Join(testDataPath, "openapi", "v3"))
288 if err != nil {
289 t.Fatalf("error starting fake openapi server: %v", err.Error())
290 }
291 defer fakeServer.HttpServer.Close()
292 tf := cmdtesting.NewTestFactory()
293 defer tf.Cleanup()
294 tf.OpenAPIV3ClientFunc = func() (openapiclient.Client, error) {
295 fakeDiscoveryClient := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: fakeServer.HttpServer.URL})
296 return fakeDiscoveryClient.OpenAPIV3(), nil
297 }
298
299 tf.OpenAPISchemaFunc = panicOpenAPISchemaFn
300
301
302 cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard())
303 resources := []string{"pods", "services", "endpoints", "configmaps"}
304 for _, resource := range resources {
305 cmd.Run(cmd, []string{resource})
306 }
307
308 defer func() {
309 if panicErr := recover(); panicErr == nil {
310 t.Fatal("expecting panic for openapi v2 retrieval")
311 }
312 }()
313
314 if err := cmd.Flags().Set("output", "plaintext-openapiv2"); err != nil {
315 t.Fatal(err)
316 }
317 cmd.Run(cmd, []string{"pods"})
318 }
319
View as plain text