1
16
17 package cmd
18
19 import (
20 "fmt"
21 "os"
22 "reflect"
23 "testing"
24
25 "github.com/google/go-cmp/cmp"
26 "github.com/google/go-cmp/cmp/cmpopts"
27 "github.com/spf13/cobra"
28
29 "k8s.io/cli-runtime/pkg/genericclioptions"
30 "k8s.io/cli-runtime/pkg/genericiooptions"
31 "k8s.io/kubectl/pkg/cmd/plugin"
32 )
33
34 func TestNormalizationFuncGlobalExistence(t *testing.T) {
35
36 root := NewKubectlCommand(KubectlOptions{IOStreams: genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}})
37
38 if root.Parent() != nil {
39 t.Fatal("We expect the root command to be returned")
40 }
41 if root.GlobalNormalizationFunc() == nil {
42 t.Fatal("We expect that root command has a global normalization function")
43 }
44
45 if reflect.ValueOf(root.GlobalNormalizationFunc()).Pointer() != reflect.ValueOf(root.Flags().GetNormalizeFunc()).Pointer() {
46 t.Fatal("root command seems to have a wrong normalization function")
47 }
48
49 sub := root
50 for sub.HasSubCommands() {
51 sub = sub.Commands()[0]
52 }
53
54
55 if reflect.ValueOf(sub.Flags().GetNormalizeFunc()).Pointer() != reflect.ValueOf(root.Flags().GetNormalizeFunc()).Pointer() {
56 t.Fatal("child and root commands should have the same normalization functions")
57 }
58 }
59
60 func TestKubectlSubcommandShadowPlugin(t *testing.T) {
61 tests := []struct {
62 name string
63 args []string
64 expectPlugin string
65 expectPluginArgs []string
66 expectLookupError string
67 }{
68 {
69 name: "test that a plugin executable is found based on command args when builtin subcommand does not exist",
70 args: []string{"kubectl", "create", "foo", "--bar", "--bar2", "--namespace", "test-namespace"},
71 expectPlugin: "plugin/testdata/kubectl-create-foo",
72 expectPluginArgs: []string{"--bar", "--bar2", "--namespace", "test-namespace"},
73 },
74 {
75 name: "test that a plugin executable is not found based on command args when also builtin subcommand does not exist",
76 args: []string{"kubectl", "create", "foo2", "--bar", "--bar2", "--namespace", "test-namespace"},
77 expectLookupError: "unable to find a plugin executable \"kubectl-create-foo2\"",
78 },
79 {
80 name: "test that normal commands are able to be executed, when builtin subcommand exists",
81 args: []string{"kubectl", "create", "job", "foo", "--image=busybox", "--dry-run=client", "--namespace", "test-namespace"},
82 expectPlugin: "",
83 expectPluginArgs: []string{},
84 },
85
86
87 {
88 name: "test that normal commands are able to be executed, when no plugin overshadows them",
89 args: []string{"kubectl", "config", "get-clusters"},
90 expectPlugin: "",
91 expectPluginArgs: []string{},
92 },
93 {
94 name: "test that a plugin executable is found based on command args",
95 args: []string{"kubectl", "foo", "--bar"},
96 expectPlugin: "plugin/testdata/kubectl-foo",
97 expectPluginArgs: []string{"--bar"},
98 },
99 {
100 name: "test that a plugin does not execute over an existing command by the same name",
101 args: []string{"kubectl", "version", "--client=true"},
102 },
103 {
104 name: "test that a plugin does not execute over Cobra's help command",
105 args: []string{"kubectl", "help"},
106 },
107 {
108 name: "test that a plugin does not execute over Cobra's __complete command",
109 args: []string{"kubectl", cobra.ShellCompRequestCmd, "de"},
110 },
111 {
112 name: "test that a plugin does not execute over Cobra's __completeNoDesc command",
113 args: []string{"kubectl", cobra.ShellCompNoDescRequestCmd, "de"},
114 },
115 {
116 name: "test that a flag does not break Cobra's help command",
117 args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", "help"},
118 },
119 {
120 name: "test that a flag does not break Cobra's __complete command",
121 args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompRequestCmd},
122 },
123 {
124 name: "test that a flag does not break Cobra's __completeNoDesc command",
125 args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompNoDescRequestCmd},
126 },
127 }
128
129 for _, test := range tests {
130 t.Run(test.name, func(t *testing.T) {
131 pluginsHandler := &testPluginHandler{
132 pluginsDirectory: "plugin/testdata",
133 validPrefixes: plugin.ValidPluginFilenamePrefixes,
134 }
135 ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
136 root := NewDefaultKubectlCommandWithArgs(KubectlOptions{PluginHandler: pluginsHandler, Arguments: test.args, IOStreams: ioStreams})
137
138 if !pluginsHandler.lookedup && !pluginsHandler.executed {
139
140
141 root.SetArgs(test.args[1:])
142
143 if err := root.Execute(); err != nil {
144 t.Fatalf("unexpected error: %v", err)
145 }
146 }
147
148 if (pluginsHandler.lookupErr != nil && pluginsHandler.lookupErr.Error() != test.expectLookupError) ||
149 (pluginsHandler.lookupErr == nil && len(test.expectLookupError) > 0) {
150 t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectLookupError, pluginsHandler.lookupErr)
151 }
152
153 if pluginsHandler.lookedup && !pluginsHandler.executed && len(test.expectLookupError) == 0 {
154
155 t.Fatalf("expected plugin execution, but did not occur")
156 }
157
158 if pluginsHandler.executedPlugin != test.expectPlugin {
159 t.Fatalf("unexpected plugin execution: expected %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin)
160 }
161
162 if pluginsHandler.executed && len(test.expectPlugin) == 0 {
163 t.Fatalf("unexpected plugin execution: expected no plugin, got %q", pluginsHandler.executedPlugin)
164 }
165
166 if !cmp.Equal(pluginsHandler.withArgs, test.expectPluginArgs, cmpopts.EquateEmpty()) {
167 t.Fatalf("unexpected plugin execution args: expected %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs)
168 }
169 })
170 }
171 }
172
173 func TestKubectlCommandHandlesPlugins(t *testing.T) {
174 tests := []struct {
175 name string
176 args []string
177 expectPlugin string
178 expectPluginArgs []string
179 expectLookupError string
180 }{
181 {
182 name: "test that normal commands are able to be executed, when no plugin overshadows them",
183 args: []string{"kubectl", "config", "get-clusters"},
184 expectPlugin: "",
185 expectPluginArgs: []string{},
186 },
187 {
188 name: "test that normal commands are able to be executed, when no plugin overshadows them and shadowing feature is not enabled",
189 args: []string{"kubectl", "create", "job", "foo", "--image=busybox", "--dry-run=client"},
190 expectPlugin: "",
191 expectPluginArgs: []string{},
192 },
193 {
194 name: "test that a plugin executable is found based on command args",
195 args: []string{"kubectl", "foo", "--bar"},
196 expectPlugin: "plugin/testdata/kubectl-foo",
197 expectPluginArgs: []string{"--bar"},
198 },
199 {
200 name: "test that a plugin does not execute over an existing command by the same name",
201 args: []string{"kubectl", "version", "--client=true"},
202 },
203
204
205 {
206 name: "test that a plugin does not execute over Cobra's help command",
207 args: []string{"kubectl", "help"},
208 },
209 {
210 name: "test that a plugin does not execute over Cobra's __complete command",
211 args: []string{"kubectl", cobra.ShellCompRequestCmd, "de"},
212 },
213 {
214 name: "test that a plugin does not execute over Cobra's __completeNoDesc command",
215 args: []string{"kubectl", cobra.ShellCompNoDescRequestCmd, "de"},
216 },
217
218
219
220 {
221 name: "test that a flag does not break Cobra's help command",
222 args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", "help"},
223 },
224 {
225 name: "test that a flag does not break Cobra's __complete command",
226 args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompRequestCmd},
227 },
228 {
229 name: "test that a flag does not break Cobra's __completeNoDesc command",
230 args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompNoDescRequestCmd},
231 },
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247 }
248
249 for _, test := range tests {
250 t.Run(test.name, func(t *testing.T) {
251 pluginsHandler := &testPluginHandler{
252 pluginsDirectory: "plugin/testdata",
253 validPrefixes: plugin.ValidPluginFilenamePrefixes,
254 }
255 ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
256 root := NewDefaultKubectlCommandWithArgs(KubectlOptions{PluginHandler: pluginsHandler, Arguments: test.args, IOStreams: ioStreams})
257
258 if !pluginsHandler.lookedup && !pluginsHandler.executed {
259
260
261 root.SetArgs(test.args[1:])
262
263 if err := root.Execute(); err != nil {
264 t.Fatalf("unexpected error: %v", err)
265 }
266 }
267
268 if (pluginsHandler.lookupErr != nil && pluginsHandler.lookupErr.Error() != test.expectLookupError) ||
269 (pluginsHandler.lookupErr == nil && len(test.expectLookupError) > 0) {
270 t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectLookupError, pluginsHandler.lookupErr)
271 }
272
273 if pluginsHandler.lookedup && !pluginsHandler.executed && len(test.expectLookupError) == 0 {
274
275 t.Fatalf("expected plugin execution, but did not occur")
276 }
277
278 if pluginsHandler.executedPlugin != test.expectPlugin {
279 t.Fatalf("unexpected plugin execution: expected %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin)
280 }
281
282 if pluginsHandler.executed && len(test.expectPlugin) == 0 {
283 t.Fatalf("unexpected plugin execution: expected no plugin, got %q", pluginsHandler.executedPlugin)
284 }
285
286 if !cmp.Equal(pluginsHandler.withArgs, test.expectPluginArgs, cmpopts.EquateEmpty()) {
287 t.Fatalf("unexpected plugin execution args: expected %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs)
288 }
289 })
290 }
291 }
292
293 type testPluginHandler struct {
294 pluginsDirectory string
295 validPrefixes []string
296
297
298 lookedup bool
299 lookupErr error
300
301
302 executed bool
303 executedPlugin string
304 withArgs []string
305 withEnv []string
306 }
307
308 func (h *testPluginHandler) Lookup(filename string) (string, bool) {
309 h.lookedup = true
310
311 dir, err := os.Stat(h.pluginsDirectory)
312 if err != nil {
313 h.lookupErr = err
314 return "", false
315 }
316
317 if !dir.IsDir() {
318 h.lookupErr = fmt.Errorf("expected %q to be a directory", h.pluginsDirectory)
319 return "", false
320 }
321
322 plugins, err := os.ReadDir(h.pluginsDirectory)
323 if err != nil {
324 h.lookupErr = err
325 return "", false
326 }
327
328 filenameWithSuportedPrefix := ""
329 for _, prefix := range h.validPrefixes {
330 for _, p := range plugins {
331 filenameWithSuportedPrefix = fmt.Sprintf("%s-%s", prefix, filename)
332 if p.Name() == filenameWithSuportedPrefix {
333 return fmt.Sprintf("%s/%s", h.pluginsDirectory, p.Name()), true
334 }
335 }
336 }
337
338 h.lookupErr = fmt.Errorf("unable to find a plugin executable %q", filenameWithSuportedPrefix)
339 return "", false
340 }
341
342 func (h *testPluginHandler) Execute(executablePath string, cmdArgs, env []string) error {
343 h.executed = true
344 h.executedPlugin = executablePath
345 h.withArgs = cmdArgs
346 h.withEnv = env
347 return nil
348 }
349
350 func TestKubectlCommandHeadersHooks(t *testing.T) {
351 tests := map[string]struct {
352 envVar string
353 addsHooks bool
354 }{
355 "empty environment variable; hooks added": {
356 envVar: "",
357 addsHooks: true,
358 },
359 "random env var value; hooks added": {
360 envVar: "foo",
361 addsHooks: true,
362 },
363 "true env var value; hooks added": {
364 envVar: "true",
365 addsHooks: true,
366 },
367 "false env var value; hooks NOT added": {
368 envVar: "false",
369 addsHooks: false,
370 },
371 "zero env var value; hooks NOT added": {
372 envVar: "0",
373 addsHooks: false,
374 },
375 }
376
377 for name, testCase := range tests {
378 t.Run(name, func(t *testing.T) {
379 cmds := &cobra.Command{}
380 kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag()
381 if kubeConfigFlags.WrapConfigFn != nil {
382 t.Fatal("expected initial nil WrapConfigFn")
383 }
384 t.Setenv(kubectlCmdHeaders, testCase.envVar)
385 addCmdHeaderHooks(cmds, kubeConfigFlags)
386
387 if testCase.addsHooks && kubeConfigFlags.WrapConfigFn == nil {
388 t.Error("after adding kubectl command header, expecting non-nil WrapConfigFn")
389 }
390 if !testCase.addsHooks && kubeConfigFlags.WrapConfigFn != nil {
391 t.Error("env var feature gate should have blocked setting WrapConfigFn")
392 }
393 })
394 }
395 }
396
View as plain text