mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-28 14:07:14 +00:00
Merge pull request #115480 from alexzielenski/kubectl/explain/openapiv3/alias-legacy
kubectl-explain: add --output plaintext-openapiv2 fallback
This commit is contained in:
commit
beacb8d7af
@ -24,10 +24,10 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
"k8s.io/client-go/discovery"
|
openapiclient "k8s.io/client-go/openapi"
|
||||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
"k8s.io/kubectl/pkg/explain"
|
"k8s.io/kubectl/pkg/explain"
|
||||||
explainv2 "k8s.io/kubectl/pkg/explain/v2"
|
openapiv3explain "k8s.io/kubectl/pkg/explain/v2"
|
||||||
"k8s.io/kubectl/pkg/util/i18n"
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
"k8s.io/kubectl/pkg/util/openapi"
|
"k8s.io/kubectl/pkg/util/openapi"
|
||||||
"k8s.io/kubectl/pkg/util/templates"
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
@ -51,6 +51,9 @@ var (
|
|||||||
|
|
||||||
# Get the documentation of a specific field of a resource
|
# Get the documentation of a specific field of a resource
|
||||||
kubectl explain pods.spec.containers`))
|
kubectl explain pods.spec.containers`))
|
||||||
|
|
||||||
|
plaintextTemplateName = "plaintext"
|
||||||
|
plaintextOpenAPIV2TemplateName = "plaintext-openapiv2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExplainOptions struct {
|
type ExplainOptions struct {
|
||||||
@ -74,7 +77,7 @@ type ExplainOptions struct {
|
|||||||
OutputFormat string
|
OutputFormat string
|
||||||
|
|
||||||
// Client capable of fetching openapi documents from the user's cluster
|
// Client capable of fetching openapi documents from the user's cluster
|
||||||
DiscoveryClient discovery.DiscoveryInterface
|
OpenAPIV3Client openapiclient.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExplainOptions(parent string, streams genericclioptions.IOStreams) *ExplainOptions {
|
func NewExplainOptions(parent string, streams genericclioptions.IOStreams) *ExplainOptions {
|
||||||
@ -82,7 +85,7 @@ func NewExplainOptions(parent string, streams genericclioptions.IOStreams) *Expl
|
|||||||
IOStreams: streams,
|
IOStreams: streams,
|
||||||
CmdParent: parent,
|
CmdParent: parent,
|
||||||
EnableOpenAPIV3: cmdutil.ExplainOpenapiV3.IsEnabled(),
|
EnableOpenAPIV3: cmdutil.ExplainOpenapiV3.IsEnabled(),
|
||||||
OutputFormat: "plaintext",
|
OutputFormat: plaintextTemplateName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +110,7 @@ func NewCmdExplain(parent string, f cmdutil.Factory, streams genericclioptions.I
|
|||||||
|
|
||||||
// Only enable --output as a valid flag if the feature is enabled
|
// Only enable --output as a valid flag if the feature is enabled
|
||||||
if o.EnableOpenAPIV3 {
|
if o.EnableOpenAPIV3 {
|
||||||
cmd.Flags().StringVar(&o.OutputFormat, "output", o.OutputFormat, "Format in which to render the schema")
|
cmd.Flags().StringVar(&o.OutputFormat, "output", plaintextTemplateName, "Format in which to render the schema (plaintext, plaintext-openapiv2)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
@ -125,13 +128,12 @@ func (o *ExplainOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only openapi v3 needs the discovery client.
|
// Only openapi v3 needs the openapiv3client
|
||||||
if o.EnableOpenAPIV3 {
|
if o.EnableOpenAPIV3 {
|
||||||
discoveryClient, err := f.ToDiscoveryClient()
|
o.OpenAPIV3Client, err = f.OpenAPIV3Client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
o.DiscoveryClient = discoveryClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
o.args = args
|
o.args = args
|
||||||
@ -151,13 +153,10 @@ func (o *ExplainOptions) Validate() error {
|
|||||||
|
|
||||||
// Run executes the appropriate steps to print a model's documentation
|
// Run executes the appropriate steps to print a model's documentation
|
||||||
func (o *ExplainOptions) Run() error {
|
func (o *ExplainOptions) Run() error {
|
||||||
recursive := o.Recursive
|
|
||||||
apiVersionString := o.APIVersion
|
|
||||||
|
|
||||||
var fullySpecifiedGVR schema.GroupVersionResource
|
var fullySpecifiedGVR schema.GroupVersionResource
|
||||||
var fieldsPath []string
|
var fieldsPath []string
|
||||||
var err error
|
var err error
|
||||||
if len(apiVersionString) == 0 {
|
if len(o.APIVersion) == 0 {
|
||||||
fullySpecifiedGVR, fieldsPath, err = explain.SplitAndParseResourceRequestWithMatchingPrefix(o.args[0], o.Mapper)
|
fullySpecifiedGVR, fieldsPath, err = explain.SplitAndParseResourceRequestWithMatchingPrefix(o.args[0], o.Mapper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -172,16 +171,47 @@ func (o *ExplainOptions) Run() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to openapiv2 implementation using special template name
|
||||||
if o.EnableOpenAPIV3 {
|
if o.EnableOpenAPIV3 {
|
||||||
return explainv2.PrintModelDescription(
|
switch o.OutputFormat {
|
||||||
fieldsPath,
|
case plaintextOpenAPIV2TemplateName:
|
||||||
o.Out,
|
return o.renderOpenAPIV2(fullySpecifiedGVR, fieldsPath)
|
||||||
o.DiscoveryClient.OpenAPIV3(),
|
case plaintextTemplateName:
|
||||||
fullySpecifiedGVR,
|
// Check whether the server reponds to OpenAPIV3.
|
||||||
recursive,
|
if _, err := o.OpenAPIV3Client.Paths(); err != nil {
|
||||||
o.OutputFormat,
|
// Use v2 renderer if server does not support v3
|
||||||
)
|
return o.renderOpenAPIV2(fullySpecifiedGVR, fieldsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
if len(o.APIVersion) > 0 {
|
||||||
|
apiVersion, err := schema.ParseGroupVersion(o.APIVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fullySpecifiedGVR.Group = apiVersion.Group
|
||||||
|
fullySpecifiedGVR.Version = apiVersion.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
return openapiv3explain.PrintModelDescription(
|
||||||
|
fieldsPath,
|
||||||
|
o.Out,
|
||||||
|
o.OpenAPIV3Client,
|
||||||
|
fullySpecifiedGVR,
|
||||||
|
o.Recursive,
|
||||||
|
o.OutputFormat,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return o.renderOpenAPIV2(fullySpecifiedGVR, fieldsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ExplainOptions) renderOpenAPIV2(
|
||||||
|
fullySpecifiedGVR schema.GroupVersionResource,
|
||||||
|
fieldsPath []string,
|
||||||
|
) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
gvk, _ := o.Mapper.KindFor(fullySpecifiedGVR)
|
gvk, _ := o.Mapper.KindFor(fullySpecifiedGVR)
|
||||||
if gvk.Empty() {
|
if gvk.Empty() {
|
||||||
@ -191,8 +221,8 @@ func (o *ExplainOptions) Run() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(apiVersionString) != 0 {
|
if len(o.APIVersion) != 0 {
|
||||||
apiVersion, err := schema.ParseGroupVersion(apiVersionString)
|
apiVersion, err := schema.ParseGroupVersion(o.APIVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -204,5 +234,5 @@ func (o *ExplainOptions) Run() error {
|
|||||||
return fmt.Errorf("couldn't find resource for %q", gvk)
|
return fmt.Errorf("couldn't find resource for %q", gvk)
|
||||||
}
|
}
|
||||||
|
|
||||||
return explain.PrintModelDescription(fieldsPath, o.Out, schema, gvk, recursive)
|
return explain.PrintModelDescription(fieldsPath, o.Out, schema, gvk, o.Recursive)
|
||||||
}
|
}
|
||||||
|
@ -14,24 +14,31 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package explain
|
package explain_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing"
|
sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing"
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"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"
|
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
"k8s.io/kubectl/pkg/util/openapi"
|
"k8s.io/kubectl/pkg/util/openapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fakeSchema = sptest.Fake{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "swagger.json")}
|
testDataPath = filepath.Join("..", "..", "..", "testdata")
|
||||||
|
fakeSchema = sptest.Fake{Path: filepath.Join(testDataPath, "openapi", "swagger.json")}
|
||||||
FakeOpenAPISchema = testOpenAPISchema{
|
FakeOpenAPISchema = testOpenAPISchema{
|
||||||
OpenAPISchemaFn: func() (openapi.Resources, error) {
|
OpenAPISchemaFn: func() (openapi.Resources, error) {
|
||||||
s, err := fakeSchema.OpenAPISchema()
|
s, err := fakeSchema.OpenAPISchema()
|
||||||
@ -51,8 +58,8 @@ func TestExplainInvalidArgs(t *testing.T) {
|
|||||||
tf := cmdtesting.NewTestFactory()
|
tf := cmdtesting.NewTestFactory()
|
||||||
defer tf.Cleanup()
|
defer tf.Cleanup()
|
||||||
|
|
||||||
opts := NewExplainOptions("kubectl", genericclioptions.NewTestIOStreamsDiscard())
|
opts := explain.NewExplainOptions("kubectl", genericclioptions.NewTestIOStreamsDiscard())
|
||||||
cmd := NewCmdExplain("kubectl", tf, genericclioptions.NewTestIOStreamsDiscard())
|
cmd := explain.NewCmdExplain("kubectl", tf, genericclioptions.NewTestIOStreamsDiscard())
|
||||||
err := opts.Complete(tf, cmd, []string{})
|
err := opts.Complete(tf, cmd, []string{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error %v", err)
|
t.Fatalf("unexpected error %v", err)
|
||||||
@ -78,8 +85,8 @@ func TestExplainNotExistResource(t *testing.T) {
|
|||||||
tf := cmdtesting.NewTestFactory()
|
tf := cmdtesting.NewTestFactory()
|
||||||
defer tf.Cleanup()
|
defer tf.Cleanup()
|
||||||
|
|
||||||
opts := NewExplainOptions("kubectl", genericclioptions.NewTestIOStreamsDiscard())
|
opts := explain.NewExplainOptions("kubectl", genericclioptions.NewTestIOStreamsDiscard())
|
||||||
cmd := NewCmdExplain("kubectl", tf, genericclioptions.NewTestIOStreamsDiscard())
|
cmd := explain.NewCmdExplain("kubectl", tf, genericclioptions.NewTestIOStreamsDiscard())
|
||||||
err := opts.Complete(tf, cmd, []string{"foo"})
|
err := opts.Complete(tf, cmd, []string{"foo"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error %v", err)
|
t.Fatalf("unexpected error %v", err)
|
||||||
@ -96,30 +103,106 @@ func TestExplainNotExistResource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExplainNotExistVersion(t *testing.T) {
|
type explainTestCase struct {
|
||||||
tf := cmdtesting.NewTestFactory()
|
Name string
|
||||||
defer tf.Cleanup()
|
Args []string
|
||||||
|
Flags map[string]string
|
||||||
|
ExpectPattern []string
|
||||||
|
ExpectErrorPattern string
|
||||||
|
|
||||||
opts := NewExplainOptions("kubectl", genericclioptions.NewTestIOStreamsDiscard())
|
// Custom OpenAPI V3 client to use for the test. If nil, a default one will
|
||||||
cmd := NewCmdExplain("kubectl", tf, genericclioptions.NewTestIOStreamsDiscard())
|
// be provided
|
||||||
err := opts.Complete(tf, cmd, []string{"pods"})
|
OpenAPIV3SchemaFn func() (openapiclient.Client, error)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
opts.APIVersion = "v99"
|
|
||||||
|
|
||||||
err = opts.Validate()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = opts.Run()
|
|
||||||
if err.Error() != "couldn't find resource for \"/v99, Kind=Pod\"" {
|
|
||||||
t.Errorf("unexpected non-error")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExplain(t *testing.T) {
|
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...)
|
||||||
|
|
||||||
|
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ExplainOpenapiV3}, t, func(t *testing.T) {
|
||||||
|
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()
|
tf := cmdtesting.NewTestFactory()
|
||||||
defer tf.Cleanup()
|
defer tf.Cleanup()
|
||||||
|
|
||||||
@ -127,29 +210,72 @@ func TestExplain(t *testing.T) {
|
|||||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||||
|
|
||||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||||
cmd := NewCmdExplain("kubectl", tf, ioStreams)
|
|
||||||
cmd.Run(cmd, []string{"pods"})
|
|
||||||
if !strings.Contains(buf.String(), "KIND: Pod") {
|
|
||||||
t.Fatalf("expected output should include pod kind")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Flags().Set("recursive", "true")
|
type catchFatal error
|
||||||
cmd.Run(cmd, []string{"pods"})
|
|
||||||
if !strings.Contains(buf.String(), "KIND: Pod") ||
|
|
||||||
!strings.Contains(buf.String(), "annotations\t<map[string]string>") {
|
|
||||||
t.Fatalf("expected output should include pod kind")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Flags().Set("api-version", "batch/v1")
|
for _, tcase := range cases {
|
||||||
cmd.Run(cmd, []string{"cronjobs"})
|
|
||||||
if !strings.Contains(buf.String(), "VERSION: batch/v1") {
|
|
||||||
t.Fatalf("expected output should include pod batch/v1")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Flags().Set("api-version", "batch/v1beta1")
|
t.Run(tcase.Name, func(t *testing.T) {
|
||||||
cmd.Run(cmd, []string{"cronjobs"})
|
|
||||||
if !strings.Contains(buf.String(), "VERSION: batch/v1beta1") {
|
// Catch os.Exit calls for tests which expect them
|
||||||
t.Fatalf("expected output should include pod batch/v1beta1")
|
// 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,11 +287,11 @@ func TestAlphaEnablement(t *testing.T) {
|
|||||||
f := cmdtesting.NewTestFactory()
|
f := cmdtesting.NewTestFactory()
|
||||||
defer f.Cleanup()
|
defer f.Cleanup()
|
||||||
|
|
||||||
cmd := NewCmdExplain("kubectl", f, genericclioptions.NewTestIOStreamsDiscard())
|
cmd := explain.NewCmdExplain("kubectl", f, genericclioptions.NewTestIOStreamsDiscard())
|
||||||
require.Nil(t, cmd.Flags().Lookup(flag), "flag %q should not be registered without the %q feature enabled", flag, feature)
|
require.Nil(t, cmd.Flags().Lookup(flag), "flag %q should not be registered without the %q feature enabled", flag, feature)
|
||||||
|
|
||||||
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{feature}, t, func(t *testing.T) {
|
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{feature}, t, func(t *testing.T) {
|
||||||
cmd := NewCmdExplain("kubectl", f, genericclioptions.NewTestIOStreamsDiscard())
|
cmd := explain.NewCmdExplain("kubectl", f, genericclioptions.NewTestIOStreamsDiscard())
|
||||||
require.NotNil(t, cmd.Flags().Lookup(flag), "flag %q should be registered with the %q feature enabled", flag, feature)
|
require.NotNil(t, cmd.Flags().Lookup(flag), "flag %q should be registered with the %q feature enabled", flag, feature)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,8 @@ import (
|
|||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
fakedynamic "k8s.io/client-go/dynamic/fake"
|
fakedynamic "k8s.io/client-go/dynamic/fake"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
|
openapiclient "k8s.io/client-go/openapi"
|
||||||
|
"k8s.io/client-go/openapi/openapitest"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/rest/fake"
|
"k8s.io/client-go/rest/fake"
|
||||||
"k8s.io/client-go/restmapper"
|
"k8s.io/client-go/restmapper"
|
||||||
@ -418,6 +420,7 @@ type TestFactory struct {
|
|||||||
|
|
||||||
UnstructuredClientForMappingFunc resource.FakeClientFunc
|
UnstructuredClientForMappingFunc resource.FakeClientFunc
|
||||||
OpenAPISchemaFunc func() (openapi.Resources, error)
|
OpenAPISchemaFunc func() (openapi.Resources, error)
|
||||||
|
OpenAPIV3ClientFunc func() (openapiclient.Client, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTestFactory returns an initialized TestFactory instance
|
// NewTestFactory returns an initialized TestFactory instance
|
||||||
@ -533,6 +536,13 @@ func (f *TestFactory) OpenAPISchema() (openapi.Resources, error) {
|
|||||||
return openapitesting.EmptyResources{}, nil
|
return openapitesting.EmptyResources{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *TestFactory) OpenAPIV3Client() (openapiclient.Client, error) {
|
||||||
|
if f.OpenAPIV3ClientFunc != nil {
|
||||||
|
return f.OpenAPIV3ClientFunc()
|
||||||
|
}
|
||||||
|
return openapitest.NewFakeClient(), nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewBuilder returns an initialized resource.Builder instance
|
// NewBuilder returns an initialized resource.Builder instance
|
||||||
func (f *TestFactory) NewBuilder() *resource.Builder {
|
func (f *TestFactory) NewBuilder() *resource.Builder {
|
||||||
return resource.NewFakeBuilder(
|
return resource.NewFakeBuilder(
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"k8s.io/cli-runtime/pkg/resource"
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
|
openapiclient "k8s.io/client-go/openapi"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
"k8s.io/kubectl/pkg/util/openapi"
|
"k8s.io/kubectl/pkg/util/openapi"
|
||||||
"k8s.io/kubectl/pkg/validation"
|
"k8s.io/kubectl/pkg/validation"
|
||||||
@ -63,4 +64,7 @@ type Factory interface {
|
|||||||
Validator(validationDirective string) (validation.Schema, error)
|
Validator(validationDirective string) (validation.Schema, error)
|
||||||
// OpenAPISchema returns the parsed openapi schema definition
|
// OpenAPISchema returns the parsed openapi schema definition
|
||||||
OpenAPISchema() (openapi.Resources, error)
|
OpenAPISchema() (openapi.Resources, error)
|
||||||
|
// OpenAPIV3Schema returns a client for fetching parsed schemas for
|
||||||
|
// any group version
|
||||||
|
OpenAPIV3Client() (openapiclient.Client, error)
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import (
|
|||||||
"k8s.io/client-go/discovery"
|
"k8s.io/client-go/discovery"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
|
openapiclient "k8s.io/client-go/openapi"
|
||||||
"k8s.io/client-go/openapi/cached"
|
"k8s.io/client-go/openapi/cached"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
@ -211,3 +212,12 @@ func (f *factoryImpl) openAPIGetter() discovery.OpenAPISchemaInterface {
|
|||||||
|
|
||||||
return f.oapi
|
return f.oapi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *factoryImpl) OpenAPIV3Client() (openapiclient.Client, error) {
|
||||||
|
discovery, err := f.clientGetter.ToDiscoveryClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return discovery.OpenAPIV3(), nil
|
||||||
|
}
|
||||||
|
@ -72,7 +72,7 @@ func printModelDescriptionWithGenerator(
|
|||||||
gv, exists := paths[resourcePath]
|
gv, exists := paths[resourcePath]
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("could not locate schema for %s", resourcePath)
|
return fmt.Errorf("couldn't find resource for \"%v\"", gvr)
|
||||||
}
|
}
|
||||||
|
|
||||||
openAPISchemaBytes, err := gv.Schema(runtime.ContentTypeJSON)
|
openAPISchemaBytes, err := gv.Schema(runtime.ContentTypeJSON)
|
||||||
|
@ -45,7 +45,7 @@ func TestExplainErrors(t *testing.T) {
|
|||||||
Version: "v1",
|
Version: "v1",
|
||||||
Resource: "doesntmatter",
|
Resource: "doesntmatter",
|
||||||
}, false, "unknown-format")
|
}, false, "unknown-format")
|
||||||
require.ErrorContains(t, err, "could not locate schema")
|
require.ErrorContains(t, err, "couldn't find resource for \"test0.example.com/v1, Resource=doesntmatter\"")
|
||||||
|
|
||||||
// Validate error when openapi client returns error.
|
// Validate error when openapi client returns error.
|
||||||
fakeClient.ForcedErr = fmt.Errorf("Always fails")
|
fakeClient.ForcedErr = fmt.Errorf("Always fails")
|
||||||
|
28772
staging/src/k8s.io/kubectl/testdata/openapi/v3/api/v1.json
vendored
Normal file
28772
staging/src/k8s.io/kubectl/testdata/openapi/v3/api/v1.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
staging/src/k8s.io/kubectl/testdata/openapi/v3/apis/autoscaling/v1.json
vendored
Normal file
1
staging/src/k8s.io/kubectl/testdata/openapi/v3/apis/autoscaling/v1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
staging/src/k8s.io/kubectl/testdata/openapi/v3/apis/autoscaling/v2.json
vendored
Normal file
1
staging/src/k8s.io/kubectl/testdata/openapi/v3/apis/autoscaling/v2.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@ -1836,6 +1836,7 @@ k8s.io/client-go/metadata/metadatainformer
|
|||||||
k8s.io/client-go/metadata/metadatalister
|
k8s.io/client-go/metadata/metadatalister
|
||||||
k8s.io/client-go/openapi
|
k8s.io/client-go/openapi
|
||||||
k8s.io/client-go/openapi/cached
|
k8s.io/client-go/openapi/cached
|
||||||
|
k8s.io/client-go/openapi/openapitest
|
||||||
k8s.io/client-go/openapi3
|
k8s.io/client-go/openapi3
|
||||||
k8s.io/client-go/pkg/apis/clientauthentication
|
k8s.io/client-go/pkg/apis/clientauthentication
|
||||||
k8s.io/client-go/pkg/apis/clientauthentication/install
|
k8s.io/client-go/pkg/apis/clientauthentication/install
|
||||||
|
Loading…
Reference in New Issue
Block a user