...

Source file src/k8s.io/kubectl/pkg/cmd/explain/explain_test.go

Documentation: k8s.io/kubectl/pkg/cmd/explain

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    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  	// Custom OpenAPI V3 client to use for the test. If nil, a default one will
   113  	// be provided
   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  	// Returns a client that causes fallback to v2 implementation
   159  	cases := []explainTestCase{
   160  		{
   161  			// No --output, but OpenAPIV3 enabled should fall back to v2 if
   162  			// v2 is not available. Shows this by making openapiv3 client
   163  			// point to a bad URL. So the fact the proper data renders is
   164  			// indication v2 was used instead.
   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  			// Show that explicitly specifying --output plaintext-openapiv2 causes
   178  			// old implementation to be used even though OpenAPIV3 is enabled
   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  			// Catch os.Exit calls for tests which expect them
   218  			// and replace them with panics that we catch in each test
   219  			// to check if it is expected.
   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  					// Catch panic and check at end of test if it is
   230  					// expected.
   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  // OpenAPI V2 specifications retrieval -- should never be called.
   280  func panicOpenAPISchemaFn() (openapi.Resources, error) {
   281  	panic("should never be called")
   282  }
   283  
   284  // OpenAPI V3 specifications retrieval does *not* retrieve V2 specifications.
   285  func TestExplainOpenAPIV3DoesNotLoadOpenAPIV2Specs(t *testing.T) {
   286  	// Set up OpenAPI V3 specifications endpoint for explain.
   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  	// OpenAPI V2 specifications retrieval will panic if called.
   299  	tf.OpenAPISchemaFunc = panicOpenAPISchemaFn
   300  
   301  	// Explain the following resources, validating the command does not panic.
   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  	// Verify retrieving OpenAPI V2 specifications will panic.
   308  	defer func() {
   309  		if panicErr := recover(); panicErr == nil {
   310  			t.Fatal("expecting panic for openapi v2 retrieval")
   311  		}
   312  	}()
   313  	// Set OpenAPI V2 output flag for explain.
   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