319 lines
9.8 KiB
Go
319 lines
9.8 KiB
Go
/*
|
|
Copyright 2022 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package explain_test
|
|
|
|
import (
|
|
"errors"
|
|
"path/filepath"
|
|
"regexp"
|
|
"testing"
|
|
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing"
|
|
"k8s.io/cli-runtime/pkg/genericiooptions"
|
|
"k8s.io/client-go/discovery"
|
|
openapiclient "k8s.io/client-go/openapi"
|
|
"k8s.io/client-go/rest"
|
|
clienttestutil "k8s.io/client-go/util/testing"
|
|
"k8s.io/kubectl/pkg/cmd/explain"
|
|
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
|
"k8s.io/kubectl/pkg/util/openapi"
|
|
)
|
|
|
|
var (
|
|
testDataPath = filepath.Join("..", "..", "..", "testdata")
|
|
fakeSchema = sptest.Fake{Path: filepath.Join(testDataPath, "openapi", "swagger.json")}
|
|
FakeOpenAPISchema = testOpenAPISchema{
|
|
OpenAPISchemaFn: func() (openapi.Resources, error) {
|
|
s, err := fakeSchema.OpenAPISchema()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return openapi.NewOpenAPIData(s)
|
|
},
|
|
}
|
|
)
|
|
|
|
type testOpenAPISchema struct {
|
|
OpenAPISchemaFn func() (openapi.Resources, error)
|
|
}
|
|
|
|
func TestExplainInvalidArgs(t *testing.T) {
|
|
tf := cmdtesting.NewTestFactory()
|
|
defer tf.Cleanup()
|
|
|
|
flags := explain.NewExplainFlags(genericclioptions.NewTestIOStreamsDiscard())
|
|
|
|
opts, err := flags.ToOptions(tf, "kubectl", []string{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %v", err)
|
|
}
|
|
|
|
err = opts.Validate()
|
|
if err.Error() != "You must specify the type of resource to explain. Use \"kubectl api-resources\" for a complete list of supported resources.\n" {
|
|
t.Error("unexpected non-error")
|
|
}
|
|
|
|
opts, err = flags.ToOptions(tf, "kubectl", []string{"resource1", "resource2"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %v", err)
|
|
}
|
|
|
|
err = opts.Validate()
|
|
if err.Error() != "We accept only this format: explain RESOURCE\n" {
|
|
t.Error("unexpected non-error")
|
|
}
|
|
}
|
|
|
|
func TestExplainNotExistResource(t *testing.T) {
|
|
tf := cmdtesting.NewTestFactory()
|
|
defer tf.Cleanup()
|
|
|
|
flags := explain.NewExplainFlags(genericclioptions.NewTestIOStreamsDiscard())
|
|
|
|
opts, err := flags.ToOptions(tf, "kubectl", []string{"foo"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %v", err)
|
|
}
|
|
|
|
err = opts.Validate()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %v", err)
|
|
}
|
|
|
|
err = opts.Run()
|
|
if _, ok := err.(*meta.NoResourceMatchError); !ok {
|
|
t.Fatalf("unexpected error %v", err)
|
|
}
|
|
}
|
|
|
|
type explainTestCase struct {
|
|
Name string
|
|
Args []string
|
|
Flags map[string]string
|
|
ExpectPattern []string
|
|
ExpectErrorPattern string
|
|
|
|
// Custom OpenAPI V3 client to use for the test. If nil, a default one will
|
|
// be provided
|
|
OpenAPIV3SchemaFn func() (openapiclient.Client, error)
|
|
}
|
|
|
|
var explainV2Cases = []explainTestCase{
|
|
{
|
|
Name: "Basic",
|
|
Args: []string{"pods"},
|
|
ExpectPattern: []string{`\s*KIND:[\t ]*Pod\s*`},
|
|
},
|
|
{
|
|
Name: "Recursive",
|
|
Args: []string{"pods"},
|
|
Flags: map[string]string{"recursive": "true"},
|
|
ExpectPattern: []string{`\s*KIND:[\t ]*Pod\s*`},
|
|
},
|
|
{
|
|
Name: "DefaultAPIVersion",
|
|
Args: []string{"horizontalpodautoscalers"},
|
|
Flags: map[string]string{"api-version": "autoscaling/v1"},
|
|
ExpectPattern: []string{`\s*VERSION:[\t ]*(v1|autoscaling/v1)\s*`},
|
|
},
|
|
{
|
|
Name: "NonExistingAPIVersion",
|
|
Args: []string{"pods"},
|
|
Flags: map[string]string{"api-version": "v99"},
|
|
ExpectErrorPattern: `couldn't find resource for \"/v99, (Kind=Pod|Resource=pods)\"`,
|
|
},
|
|
{
|
|
Name: "NonExistingResource",
|
|
Args: []string{"foo"},
|
|
ExpectErrorPattern: `the server doesn't have a resource type "foo"`,
|
|
},
|
|
}
|
|
|
|
func TestExplainOpenAPIV2(t *testing.T) {
|
|
runExplainTestCases(t, explainV2Cases)
|
|
}
|
|
|
|
func TestExplainOpenAPIV3(t *testing.T) {
|
|
|
|
fallbackV3SchemaFn := func() (openapiclient.Client, error) {
|
|
fakeDiscoveryClient := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: "https://not.a.real.site:65543/"})
|
|
return fakeDiscoveryClient.OpenAPIV3(), nil
|
|
}
|
|
// Returns a client that causes fallback to v2 implementation
|
|
cases := []explainTestCase{
|
|
{
|
|
// No --output, but OpenAPIV3 enabled should fall back to v2 if
|
|
// v2 is not available. Shows this by making openapiv3 client
|
|
// point to a bad URL. So the fact the proper data renders is
|
|
// indication v2 was used instead.
|
|
Name: "Fallback",
|
|
Args: []string{"pods"},
|
|
ExpectPattern: []string{`\s*KIND:[\t ]*Pod\s*`},
|
|
OpenAPIV3SchemaFn: fallbackV3SchemaFn,
|
|
},
|
|
{
|
|
Name: "NonDefaultAPIVersion",
|
|
Args: []string{"horizontalpodautoscalers"},
|
|
Flags: map[string]string{"api-version": "autoscaling/v2"},
|
|
ExpectPattern: []string{`\s*VERSION:[\t ]*(v2|autoscaling/v2)\s*`},
|
|
},
|
|
{
|
|
// Show that explicitly specifying --output plaintext-openapiv2 causes
|
|
// old implementation to be used even though OpenAPIV3 is enabled
|
|
Name: "OutputPlaintextV2",
|
|
Args: []string{"pods"},
|
|
Flags: map[string]string{"output": "plaintext-openapiv2"},
|
|
ExpectPattern: []string{`\s*KIND:[\t ]*Pod\s*`},
|
|
OpenAPIV3SchemaFn: fallbackV3SchemaFn,
|
|
},
|
|
}
|
|
cases = append(cases, explainV2Cases...)
|
|
|
|
runExplainTestCases(t, cases)
|
|
}
|
|
|
|
func runExplainTestCases(t *testing.T, cases []explainTestCase) {
|
|
fakeServer, err := clienttestutil.NewFakeOpenAPIV3Server(filepath.Join(testDataPath, "openapi", "v3"))
|
|
if err != nil {
|
|
t.Fatalf("error starting fake openapi server: %v", err.Error())
|
|
}
|
|
defer fakeServer.HttpServer.Close()
|
|
|
|
openapiV3SchemaFn := func() (openapiclient.Client, error) {
|
|
fakeDiscoveryClient := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: fakeServer.HttpServer.URL})
|
|
return fakeDiscoveryClient.OpenAPIV3(), nil
|
|
}
|
|
|
|
tf := cmdtesting.NewTestFactory()
|
|
defer tf.Cleanup()
|
|
|
|
tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn
|
|
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
|
|
|
ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams()
|
|
|
|
type catchFatal error
|
|
|
|
for _, tcase := range cases {
|
|
|
|
t.Run(tcase.Name, func(t *testing.T) {
|
|
|
|
// Catch os.Exit calls for tests which expect them
|
|
// and replace them with panics that we catch in each test
|
|
// to check if it is expected.
|
|
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
|
panic(catchFatal(errors.New(str)))
|
|
})
|
|
defer cmdutil.DefaultBehaviorOnFatal()
|
|
|
|
var err error
|
|
|
|
func() {
|
|
defer func() {
|
|
// Catch panic and check at end of test if it is
|
|
// expected.
|
|
if panicErr := recover(); panicErr != nil {
|
|
if e := panicErr.(catchFatal); e != nil {
|
|
err = e
|
|
} else {
|
|
panic(panicErr)
|
|
}
|
|
}
|
|
}()
|
|
|
|
if tcase.OpenAPIV3SchemaFn != nil {
|
|
tf.OpenAPIV3ClientFunc = tcase.OpenAPIV3SchemaFn
|
|
} else {
|
|
tf.OpenAPIV3ClientFunc = openapiV3SchemaFn
|
|
}
|
|
|
|
cmd := explain.NewCmdExplain("kubectl", tf, ioStreams)
|
|
for k, v := range tcase.Flags {
|
|
if err := cmd.Flags().Set(k, v); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
cmd.Run(cmd, tcase.Args)
|
|
}()
|
|
|
|
for _, rexp := range tcase.ExpectPattern {
|
|
if matched, err := regexp.MatchString(rexp, buf.String()); err != nil || !matched {
|
|
if err != nil {
|
|
t.Error(err)
|
|
} else {
|
|
t.Errorf("expected output to match regex:\n\t%s\ninstead got:\n\t%s", rexp, buf.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
if matched, regexErr := regexp.MatchString(tcase.ExpectErrorPattern, err.Error()); len(tcase.ExpectErrorPattern) == 0 || regexErr != nil || !matched {
|
|
t.Fatalf("unexpected error: %s did not match regex %s (%v)", err.Error(),
|
|
tcase.ExpectErrorPattern, regexErr)
|
|
}
|
|
} else if len(tcase.ExpectErrorPattern) > 0 {
|
|
t.Fatalf("did not trigger expected error: %s in output:\n%s", tcase.ExpectErrorPattern, buf.String())
|
|
}
|
|
})
|
|
|
|
buf.Reset()
|
|
}
|
|
}
|
|
|
|
// OpenAPI V2 specifications retrieval -- should never be called.
|
|
func panicOpenAPISchemaFn() (openapi.Resources, error) {
|
|
panic("should never be called")
|
|
}
|
|
|
|
// OpenAPI V3 specifications retrieval does *not* retrieve V2 specifications.
|
|
func TestExplainOpenAPIV3DoesNotLoadOpenAPIV2Specs(t *testing.T) {
|
|
// Set up OpenAPI V3 specifications endpoint for explain.
|
|
fakeServer, err := clienttestutil.NewFakeOpenAPIV3Server(filepath.Join(testDataPath, "openapi", "v3"))
|
|
if err != nil {
|
|
t.Fatalf("error starting fake openapi server: %v", err.Error())
|
|
}
|
|
defer fakeServer.HttpServer.Close()
|
|
tf := cmdtesting.NewTestFactory()
|
|
defer tf.Cleanup()
|
|
tf.OpenAPIV3ClientFunc = func() (openapiclient.Client, error) {
|
|
fakeDiscoveryClient := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: fakeServer.HttpServer.URL})
|
|
return fakeDiscoveryClient.OpenAPIV3(), nil
|
|
}
|
|
// OpenAPI V2 specifications retrieval will panic if called.
|
|
tf.OpenAPISchemaFunc = panicOpenAPISchemaFn
|
|
|
|
// Explain the following resources, validating the command does not panic.
|
|
cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard())
|
|
resources := []string{"pods", "services", "endpoints", "configmaps"}
|
|
for _, resource := range resources {
|
|
cmd.Run(cmd, []string{resource})
|
|
}
|
|
// Verify retrieving OpenAPI V2 specifications will panic.
|
|
defer func() {
|
|
if panicErr := recover(); panicErr == nil {
|
|
t.Fatal("expecting panic for openapi v2 retrieval")
|
|
}
|
|
}()
|
|
// Set OpenAPI V2 output flag for explain.
|
|
if err := cmd.Flags().Set("output", "plaintext-openapiv2"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmd.Run(cmd, []string{"pods"})
|
|
}
|