diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/explain/explain.go b/staging/src/k8s.io/kubectl/pkg/cmd/explain/explain.go index 3da1ad54d48..874cc876b63 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/explain/explain.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/explain/explain.go @@ -18,14 +18,17 @@ package explain import ( "fmt" + "os" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/discovery" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/explain" + explainv2 "k8s.io/kubectl/pkg/explain/v2" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/openapi" "k8s.io/kubectl/pkg/util/templates" @@ -62,12 +65,25 @@ type ExplainOptions struct { Mapper meta.RESTMapper Schema openapi.Resources + + // Toggles whether the OpenAPI v3 template-based renderer should be used to show + // output. + EnableOpenAPIV3 bool + + // Name of the template to use with the openapiv3 template renderer. If + // `EnableOpenAPIV3` is disabled, this does nothing + OutputFormat string + + // Client capable of fetching openapi documents from the user's cluster + DiscoveryClient discovery.DiscoveryInterface } func NewExplainOptions(parent string, streams genericclioptions.IOStreams) *ExplainOptions { return &ExplainOptions{ - IOStreams: streams, - CmdParent: parent, + IOStreams: streams, + CmdParent: parent, + EnableOpenAPIV3: os.Getenv("KUBECTL_EXPLAIN_OPENAPIV3") == "true", + OutputFormat: "plaintext", } } @@ -89,6 +105,12 @@ func NewCmdExplain(parent string, f cmdutil.Factory, streams genericclioptions.I } cmd.Flags().BoolVar(&o.Recursive, "recursive", o.Recursive, "Print the fields of fields (Currently only 1 level deep)") cmd.Flags().StringVar(&o.APIVersion, "api-version", o.APIVersion, "Get different explanations for particular API version (API group/version)") + + // Only enable --output as a valid flag if the feature is enabled + if o.EnableOpenAPIV3 { + cmd.Flags().StringVar(&o.OutputFormat, "output", o.OutputFormat, "Format in which to render the schema") + } + return cmd } @@ -104,6 +126,15 @@ func (o *ExplainOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args [] return err } + // Only openapi v3 needs the discovery client. + if o.EnableOpenAPIV3 { + clientset, err := f.KubernetesClientSet() + if err != nil { + return err + } + o.DiscoveryClient = clientset.DiscoveryClient + } + o.args = args return nil } @@ -142,6 +173,17 @@ func (o *ExplainOptions) Run() error { } } + if o.EnableOpenAPIV3 { + return explainv2.PrintModelDescription( + fieldsPath, + o.Out, + o.DiscoveryClient.OpenAPIV3(), + fullySpecifiedGVR, + recursive, + o.OutputFormat, + ) + } + gvk, _ := o.Mapper.KindFor(fullySpecifiedGVR) if gvk.Empty() { gvk, err = o.Mapper.KindFor(fullySpecifiedGVR.GroupResource().WithVersion("")) diff --git a/staging/src/k8s.io/kubectl/pkg/explain/v2/explain.go b/staging/src/k8s.io/kubectl/pkg/explain/v2/explain.go new file mode 100644 index 00000000000..0dc0350d045 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/explain/v2/explain.go @@ -0,0 +1,84 @@ +/* +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 v2 + +import ( + "encoding/json" + "fmt" + "io" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/openapi" +) + +// PrintModelDescription prints the description of a specific model or dot path. +// If recursive, all components nested within the fields of the schema will be +// printed. +func PrintModelDescription( + fieldsPath []string, + w io.Writer, + client openapi.Client, + gvr schema.GroupVersionResource, + recursive bool, + outputFormat string, +) error { + return printModelDescriptionWithGenerator( + NewGenerator(), fieldsPath, w, client, gvr, recursive, outputFormat) +} + +// Factored out for testability +func printModelDescriptionWithGenerator( + generator *generator, + fieldsPath []string, + w io.Writer, + client openapi.Client, + gvr schema.GroupVersionResource, + recursive bool, + outputFormat string, +) error { + paths, err := client.Paths() + + if err != nil { + return fmt.Errorf("failed to fetch list of groupVersions: %w", err) + } + + var resourcePath string + if len(gvr.Group) == 0 { + resourcePath = fmt.Sprintf("api/%s", gvr.Version) + } else { + resourcePath = fmt.Sprintf("apis/%s/%s", gvr.Group, gvr.Version) + } + + gv, exists := paths[resourcePath] + + if !exists { + return fmt.Errorf("could not locate schema for %s", resourcePath) + } + + openAPISchemaBytes, err := gv.Schema(runtime.ContentTypeJSON) + if err != nil { + return fmt.Errorf("failed to fetch openapi schema for %s: %w", resourcePath, err) + } + + var parsedV3Schema map[string]interface{} + if err := json.Unmarshal(openAPISchemaBytes, &parsedV3Schema); err != nil { + return fmt.Errorf("failed to parse openapi schema for %s: %w", resourcePath, err) + } + + return generator.Render(outputFormat, parsedV3Schema, gvr, fieldsPath, recursive, w) +}