add new command explain
Signed-off-by: hulizhe <pompeii.hu@gmail.com>
This commit is contained in:
parent
e13518de1e
commit
216b465e98
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
Copyright 2024 The Karmada 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
kubectlexplain "k8s.io/kubectl/pkg/cmd/explain"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
|
||||
"github.com/karmada-io/karmada/pkg/karmadactl/options"
|
||||
"github.com/karmada-io/karmada/pkg/karmadactl/util"
|
||||
)
|
||||
|
||||
var (
|
||||
explainLong = templates.LongDesc(`
|
||||
Describe fields and structure of various resources in Karmada control plane or a member cluster.
|
||||
|
||||
This command describes the fields associated with each supported API resource.
|
||||
Fields are identified via a simple JSONPath identifier:
|
||||
|
||||
<type>.<fieldName>[.<fieldName>]
|
||||
|
||||
Information about each field is retrieved from the server in OpenAPI format.`)
|
||||
|
||||
explainExamples = templates.Examples(`
|
||||
# Get the documentation of the resource and its fields in Karmada control plane
|
||||
%[1]s explain propagationpolicies
|
||||
|
||||
# Get all the fields in the resource in member cluster member1
|
||||
%[1]s explain pods --recursive --operation-scope=members --cluster=member1
|
||||
|
||||
# Get the explanation for resourcebindings in supported api versions in Karmada control plane
|
||||
%[1]s explain resourcebindings --api-version=work.karmada.io/v1alpha1
|
||||
|
||||
# Get the documentation of a specific field of a resource in member cluster member1
|
||||
%[1]s explain pods.spec.containers --operation-scope=members --cluster=member1
|
||||
|
||||
# Get the documentation of resources in different format in Karmada control plane
|
||||
%[1]s explain clusterpropagationpolicies --output=plaintext-openapiv2`)
|
||||
plaintextTemplateName = "plaintext"
|
||||
)
|
||||
|
||||
// NewCmdExplain new explain command.
|
||||
func NewCmdExplain(f util.Factory, parentCommand string, streams genericiooptions.IOStreams) *cobra.Command {
|
||||
var o CommandExplainOptions
|
||||
o.ExplainOptions = kubectlexplain.NewExplainOptions(parentCommand, streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "explain TYPE [--recursive=FALSE|TRUE] [--api-version=api-version-group] [--output=plaintext|plaintext-openapiv2] ",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Get documentation for a resource",
|
||||
Long: fmt.Sprintf(explainLong, parentCommand),
|
||||
Example: fmt.Sprintf(explainExamples, parentCommand),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
o.OperationScope = options.KarmadaControlPlane
|
||||
flags.Var(&o.OperationScope, "operation-scope", "Used to control the operation scope of the command. The optional values are karmada and members. Defaults to karmada.")
|
||||
flags.BoolVar(&o.Recursive, "recursive", o.Recursive, "When true, print the name of all the fields recursively. Otherwise, print the available fields with their description.")
|
||||
flags.StringVar(&o.APIVersion, "api-version", o.APIVersion, "Use given api-version (group/version) of the resource.")
|
||||
|
||||
// Only enable --output as a valid flag if the feature is enabled
|
||||
flags.StringVar(&o.OutputFormat, "output", plaintextTemplateName, "Format in which to render the schema. Valid values are: (plaintext, plaintext-openapiv2).")
|
||||
|
||||
flags.StringVarP(options.DefaultConfigFlags.Namespace, "namespace", "n", *options.DefaultConfigFlags.Namespace, "If present, the namespace scope for this CLI request")
|
||||
flags.StringVar(&o.Cluster, "cluster", "", "Used to specify a target member cluster and only takes effect when the command's operation scope is member clusters, for example: --operation-scope=all --cluster=member1")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// CommandExplainOptions contains the input to the explain command.
|
||||
type CommandExplainOptions struct {
|
||||
// flags specific to explain
|
||||
*kubectlexplain.ExplainOptions
|
||||
Cluster string
|
||||
OperationScope options.OperationScope
|
||||
}
|
||||
|
||||
// Complete ensures that options are valid and marshals them if necessary
|
||||
func (o *CommandExplainOptions) Complete(f util.Factory, cmd *cobra.Command, args []string) error {
|
||||
var explainFactory cmdutil.Factory = f
|
||||
if o.OperationScope == options.Members && len(o.Cluster) != 0 {
|
||||
memberFactory, err := f.FactoryForMemberCluster(o.Cluster)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
explainFactory = memberFactory
|
||||
}
|
||||
|
||||
return o.ExplainOptions.Complete(explainFactory, cmd, args)
|
||||
}
|
||||
|
||||
// Validate checks that the provided explain options are specified
|
||||
func (o *CommandExplainOptions) Validate() error {
|
||||
err := options.VerifyOperationScopeFlags(o.OperationScope, options.KarmadaControlPlane, options.Members)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if o.OperationScope == options.Members && len(o.Cluster) == 0 {
|
||||
return fmt.Errorf("must specify a member cluster")
|
||||
}
|
||||
return o.ExplainOptions.Validate()
|
||||
}
|
||||
|
||||
// Run executes the appropriate steps to print a model's documentation
|
||||
func (o *CommandExplainOptions) Run() error {
|
||||
return o.ExplainOptions.Run()
|
||||
}
|
|
@ -38,6 +38,7 @@ import (
|
|||
"github.com/karmada-io/karmada/pkg/karmadactl/deinit"
|
||||
"github.com/karmada-io/karmada/pkg/karmadactl/describe"
|
||||
"github.com/karmada-io/karmada/pkg/karmadactl/exec"
|
||||
"github.com/karmada-io/karmada/pkg/karmadactl/explain"
|
||||
"github.com/karmada-io/karmada/pkg/karmadactl/get"
|
||||
"github.com/karmada-io/karmada/pkg/karmadactl/interpret"
|
||||
"github.com/karmada-io/karmada/pkg/karmadactl/join"
|
||||
|
@ -89,6 +90,7 @@ func NewKarmadaCtlCommand(cmdUse, parentCommand string) *cobra.Command {
|
|||
{
|
||||
Message: "Basic Commands:",
|
||||
Commands: []*cobra.Command{
|
||||
explain.NewCmdExplain(f, parentCommand, ioStreams),
|
||||
get.NewCmdGet(f, parentCommand, ioStreams),
|
||||
create.NewCmdCreate(f, parentCommand, ioStreams),
|
||||
},
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
/*
|
||||
Copyright 2014 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
openapiclient "k8s.io/client-go/openapi"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/explain"
|
||||
openapiv3explain "k8s.io/kubectl/pkg/explain/v2"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
explainLong = templates.LongDesc(i18n.T(`
|
||||
Describe fields and structure of various resources.
|
||||
|
||||
This command describes the fields associated with each supported API resource.
|
||||
Fields are identified via a simple JSONPath identifier:
|
||||
|
||||
<type>.<fieldName>[.<fieldName>]
|
||||
|
||||
Information about each field is retrieved from the server in OpenAPI format.`))
|
||||
|
||||
explainExamples = templates.Examples(i18n.T(`
|
||||
# Get the documentation of the resource and its fields
|
||||
kubectl explain pods
|
||||
|
||||
# Get all the fields in the resource
|
||||
kubectl explain pods --recursive
|
||||
|
||||
# Get the explanation for deployment in supported api versions
|
||||
kubectl explain deployments --api-version=apps/v1
|
||||
|
||||
# Get the documentation of a specific field of a resource
|
||||
kubectl explain pods.spec.containers
|
||||
|
||||
# Get the documentation of resources in different format
|
||||
kubectl explain deployment --output=plaintext-openapiv2`))
|
||||
|
||||
plaintextTemplateName = "plaintext"
|
||||
plaintextOpenAPIV2TemplateName = "plaintext-openapiv2"
|
||||
)
|
||||
|
||||
type ExplainOptions struct {
|
||||
genericiooptions.IOStreams
|
||||
|
||||
CmdParent string
|
||||
APIVersion string
|
||||
Recursive bool
|
||||
|
||||
args []string
|
||||
|
||||
Mapper meta.RESTMapper
|
||||
openAPIGetter openapi.OpenAPIResourcesGetter
|
||||
|
||||
// Name of the template to use with the openapiv3 template renderer.
|
||||
OutputFormat string
|
||||
|
||||
// Client capable of fetching openapi documents from the user's cluster
|
||||
OpenAPIV3Client openapiclient.Client
|
||||
}
|
||||
|
||||
func NewExplainOptions(parent string, streams genericiooptions.IOStreams) *ExplainOptions {
|
||||
return &ExplainOptions{
|
||||
IOStreams: streams,
|
||||
CmdParent: parent,
|
||||
OutputFormat: plaintextTemplateName,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdExplain returns a cobra command for swagger docs
|
||||
func NewCmdExplain(parent string, f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
|
||||
o := NewExplainOptions(parent, streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "explain TYPE [--recursive=FALSE|TRUE] [--api-version=api-version-group] [--output=plaintext|plaintext-openapiv2]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Get documentation for a resource"),
|
||||
Long: explainLong + "\n\n" + cmdutil.SuggestAPIResources(parent),
|
||||
Example: explainExamples,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&o.Recursive, "recursive", o.Recursive, "When true, print the name of all the fields recursively. Otherwise, print the available fields with their description.")
|
||||
cmd.Flags().StringVar(&o.APIVersion, "api-version", o.APIVersion, "Use given api-version (group/version) of the resource.")
|
||||
|
||||
// Only enable --output as a valid flag if the feature is enabled
|
||||
cmd.Flags().StringVar(&o.OutputFormat, "output", plaintextTemplateName, "Format in which to render the schema. Valid values are: (plaintext, plaintext-openapiv2).")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *ExplainOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
o.Mapper, err = f.ToRESTMapper()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only openapi v3 needs the discovery client.
|
||||
o.OpenAPIV3Client, err = f.OpenAPIV3Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Lazy-load the OpenAPI V2 Resources, so they're not loaded when using OpenAPI V3.
|
||||
o.openAPIGetter = f
|
||||
o.args = args
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *ExplainOptions) Validate() error {
|
||||
if len(o.args) == 0 {
|
||||
return fmt.Errorf("You must specify the type of resource to explain. %s\n", cmdutil.SuggestAPIResources(o.CmdParent))
|
||||
}
|
||||
if len(o.args) > 1 {
|
||||
return fmt.Errorf("We accept only this format: explain RESOURCE\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run executes the appropriate steps to print a model's documentation
|
||||
func (o *ExplainOptions) Run() error {
|
||||
var fullySpecifiedGVR schema.GroupVersionResource
|
||||
var fieldsPath []string
|
||||
var err error
|
||||
if len(o.APIVersion) == 0 {
|
||||
fullySpecifiedGVR, fieldsPath, err = explain.SplitAndParseResourceRequestWithMatchingPrefix(o.args[0], o.Mapper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// TODO: After we figured out the new syntax to separate group and resource, allow
|
||||
// the users to use it in explain (kubectl explain <group><syntax><resource>).
|
||||
// Refer to issue #16039 for why we do this. Refer to PR #15808 that used "/" syntax.
|
||||
fullySpecifiedGVR, fieldsPath, err = explain.SplitAndParseResourceRequest(o.args[0], o.Mapper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to openapiv2 implementation using special template name
|
||||
switch o.OutputFormat {
|
||||
case plaintextOpenAPIV2TemplateName:
|
||||
return o.renderOpenAPIV2(fullySpecifiedGVR, fieldsPath)
|
||||
case plaintextTemplateName:
|
||||
// Check whether the server reponds to OpenAPIV3.
|
||||
if _, err := o.OpenAPIV3Client.Paths(); err != nil {
|
||||
// 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ExplainOptions) renderOpenAPIV2(
|
||||
fullySpecifiedGVR schema.GroupVersionResource,
|
||||
fieldsPath []string,
|
||||
) error {
|
||||
var err error
|
||||
|
||||
gvk, _ := o.Mapper.KindFor(fullySpecifiedGVR)
|
||||
if gvk.Empty() {
|
||||
gvk, err = o.Mapper.KindFor(fullySpecifiedGVR.GroupResource().WithVersion(""))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(o.APIVersion) != 0 {
|
||||
apiVersion, err := schema.ParseGroupVersion(o.APIVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gvk = apiVersion.WithKind(gvk.Kind)
|
||||
}
|
||||
|
||||
resources, err := o.openAPIGetter.OpenAPISchema()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
schema := resources.LookupResource(gvk)
|
||||
if schema == nil {
|
||||
return fmt.Errorf("couldn't find resource for %q", gvk)
|
||||
}
|
||||
|
||||
return explain.PrintModelDescription(fieldsPath, o.Out, schema, gvk, o.Recursive)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- apelisse
|
||||
reviewers:
|
||||
- apelisse
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
Copyright 2017 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/util/jsonpath"
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
)
|
||||
|
||||
type fieldsPrinter interface {
|
||||
PrintFields(proto.Schema) error
|
||||
}
|
||||
|
||||
// jsonPathParse gets back the inner list of nodes we want to work with
|
||||
func jsonPathParse(in string) ([]jsonpath.Node, error) {
|
||||
// Remove trailing period just in case
|
||||
in = strings.TrimSuffix(in, ".")
|
||||
|
||||
// Define initial jsonpath Parser
|
||||
jpp, err := jsonpath.Parse("user", "{."+in+"}")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Because of the way the jsonpath library works, the schema of the parser is [][]NodeList
|
||||
// meaning we need to get the outer node list, make sure it's only length 1, then get the inner node
|
||||
// list, and only then can we look at the individual nodes themselves.
|
||||
outerNodeList := jpp.Root.Nodes
|
||||
if len(outerNodeList) != 1 {
|
||||
return nil, fmt.Errorf("must pass in 1 jsonpath string")
|
||||
}
|
||||
|
||||
// The root node is always a list node so this type assertion is safe
|
||||
return outerNodeList[0].(*jsonpath.ListNode).Nodes, nil
|
||||
}
|
||||
|
||||
// SplitAndParseResourceRequest separates the users input into a model and fields
|
||||
func SplitAndParseResourceRequest(inResource string, mapper meta.RESTMapper) (schema.GroupVersionResource, []string, error) {
|
||||
inResourceNodeList, err := jsonPathParse(inResource)
|
||||
if err != nil {
|
||||
return schema.GroupVersionResource{}, nil, err
|
||||
}
|
||||
|
||||
if inResourceNodeList[0].Type() != jsonpath.NodeField {
|
||||
return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, first node must be field node")
|
||||
}
|
||||
resource := inResourceNodeList[0].(*jsonpath.FieldNode).Value
|
||||
gvr, err := mapper.ResourceFor(schema.GroupVersionResource{Resource: resource})
|
||||
if err != nil {
|
||||
return schema.GroupVersionResource{}, nil, err
|
||||
}
|
||||
|
||||
var fieldsPath []string
|
||||
for _, node := range inResourceNodeList[1:] {
|
||||
if node.Type() != jsonpath.NodeField {
|
||||
return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, all nodes must be field nodes")
|
||||
}
|
||||
fieldsPath = append(fieldsPath, node.(*jsonpath.FieldNode).Value)
|
||||
}
|
||||
|
||||
return gvr, fieldsPath, nil
|
||||
}
|
||||
|
||||
// SplitAndParseResourceRequestWithMatchingPrefix separates the users input into a model and fields
|
||||
// while selecting gvr whose (resource, group) prefix matches the resource
|
||||
func SplitAndParseResourceRequestWithMatchingPrefix(inResource string, mapper meta.RESTMapper) (gvr schema.GroupVersionResource, fieldsPath []string, err error) {
|
||||
inResourceNodeList, err := jsonPathParse(inResource)
|
||||
if err != nil {
|
||||
return schema.GroupVersionResource{}, nil, err
|
||||
}
|
||||
|
||||
// Get resource from first node of jsonpath
|
||||
if inResourceNodeList[0].Type() != jsonpath.NodeField {
|
||||
return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, first node must be field node")
|
||||
}
|
||||
resource := inResourceNodeList[0].(*jsonpath.FieldNode).Value
|
||||
|
||||
gvrs, err := mapper.ResourcesFor(schema.GroupVersionResource{Resource: resource})
|
||||
if err != nil {
|
||||
return schema.GroupVersionResource{}, nil, err
|
||||
}
|
||||
|
||||
for _, gvrItem := range gvrs {
|
||||
// Find first gvr whose gr prefixes requested resource
|
||||
groupResource := gvrItem.GroupResource().String()
|
||||
if strings.HasPrefix(inResource, groupResource) {
|
||||
resourceSuffix := inResource[len(groupResource):]
|
||||
var fieldsPath []string
|
||||
if len(resourceSuffix) > 0 {
|
||||
// Define another jsonpath Parser for the resource suffix
|
||||
resourceSuffixNodeList, err := jsonPathParse(resourceSuffix)
|
||||
if err != nil {
|
||||
return schema.GroupVersionResource{}, nil, err
|
||||
}
|
||||
|
||||
if len(resourceSuffixNodeList) > 0 {
|
||||
nodeList := resourceSuffixNodeList[1:]
|
||||
for _, node := range nodeList {
|
||||
if node.Type() != jsonpath.NodeField {
|
||||
return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, first node must be field node")
|
||||
}
|
||||
fieldsPath = append(fieldsPath, node.(*jsonpath.FieldNode).Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return gvrItem, fieldsPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If no match, take the first (the highest priority) gvr
|
||||
fieldsPath = []string{}
|
||||
if len(gvrs) > 0 {
|
||||
gvr = gvrs[0]
|
||||
|
||||
fieldsPathNodeList, err := jsonPathParse(inResource)
|
||||
if err != nil {
|
||||
return schema.GroupVersionResource{}, nil, err
|
||||
}
|
||||
|
||||
for _, node := range fieldsPathNodeList[1:] {
|
||||
if node.Type() != jsonpath.NodeField {
|
||||
return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, first node must be field node")
|
||||
}
|
||||
fieldsPath = append(fieldsPath, node.(*jsonpath.FieldNode).Value)
|
||||
}
|
||||
}
|
||||
|
||||
return gvr, fieldsPath, nil
|
||||
}
|
||||
|
||||
// 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, schema proto.Schema, gvk schema.GroupVersionKind, recursive bool) error {
|
||||
fieldName := ""
|
||||
if len(fieldsPath) != 0 {
|
||||
fieldName = fieldsPath[len(fieldsPath)-1]
|
||||
}
|
||||
|
||||
// Go down the fieldsPath to find what we're trying to explain
|
||||
schema, err := LookupSchemaForField(schema, fieldsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := fieldsPrinterBuilder{Recursive: recursive}
|
||||
f := &Formatter{Writer: w, Wrap: 80}
|
||||
return PrintModel(fieldName, f, b, schema, gvk)
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2017 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
)
|
||||
|
||||
// fieldLookup walks through a schema by following a path, and returns
|
||||
// the final schema.
|
||||
type fieldLookup struct {
|
||||
// Path to walk
|
||||
Path []string
|
||||
|
||||
// Return information: Schema found, or error.
|
||||
Schema proto.Schema
|
||||
Error error
|
||||
}
|
||||
|
||||
// SaveLeafSchema is used to detect if we are done walking the path, and
|
||||
// saves the schema as a match.
|
||||
func (f *fieldLookup) SaveLeafSchema(schema proto.Schema) bool {
|
||||
if len(f.Path) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
f.Schema = schema
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// VisitArray is mostly a passthrough.
|
||||
func (f *fieldLookup) VisitArray(a *proto.Array) {
|
||||
if f.SaveLeafSchema(a) {
|
||||
return
|
||||
}
|
||||
|
||||
// Passthrough arrays.
|
||||
a.SubType.Accept(f)
|
||||
}
|
||||
|
||||
// VisitMap is mostly a passthrough.
|
||||
func (f *fieldLookup) VisitMap(m *proto.Map) {
|
||||
if f.SaveLeafSchema(m) {
|
||||
return
|
||||
}
|
||||
|
||||
// Passthrough maps.
|
||||
m.SubType.Accept(f)
|
||||
}
|
||||
|
||||
// VisitPrimitive stops the operation and returns itself as the found
|
||||
// schema, even if it had more path to walk.
|
||||
func (f *fieldLookup) VisitPrimitive(p *proto.Primitive) {
|
||||
// Even if Path is not empty (we're not expecting a leaf),
|
||||
// return that primitive.
|
||||
f.Schema = p
|
||||
}
|
||||
|
||||
// VisitKind unstacks fields as it finds them.
|
||||
func (f *fieldLookup) VisitKind(k *proto.Kind) {
|
||||
if f.SaveLeafSchema(k) {
|
||||
return
|
||||
}
|
||||
|
||||
subSchema, ok := k.Fields[f.Path[0]]
|
||||
if !ok {
|
||||
f.Error = fmt.Errorf("field %q does not exist", f.Path[0])
|
||||
return
|
||||
}
|
||||
|
||||
f.Path = f.Path[1:]
|
||||
subSchema.Accept(f)
|
||||
}
|
||||
|
||||
func (f *fieldLookup) VisitArbitrary(a *proto.Arbitrary) {
|
||||
f.Schema = a
|
||||
}
|
||||
|
||||
// VisitReference is mostly a passthrough.
|
||||
func (f *fieldLookup) VisitReference(r proto.Reference) {
|
||||
if f.SaveLeafSchema(r) {
|
||||
return
|
||||
}
|
||||
|
||||
// Passthrough references.
|
||||
r.SubSchema().Accept(f)
|
||||
}
|
||||
|
||||
// LookupSchemaForField looks for the schema of a given path in a base schema.
|
||||
func LookupSchemaForField(schema proto.Schema, path []string) (proto.Schema, error) {
|
||||
f := &fieldLookup{Path: path}
|
||||
schema.Accept(f)
|
||||
return f.Schema, f.Error
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Copyright 2017 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
|
||||
|
||||
import "k8s.io/kube-openapi/pkg/util/proto"
|
||||
|
||||
// indentDesc is the level of indentation for descriptions.
|
||||
const indentDesc = 2
|
||||
|
||||
// regularFieldsPrinter prints fields with their type and description.
|
||||
type regularFieldsPrinter struct {
|
||||
Writer *Formatter
|
||||
Error error
|
||||
}
|
||||
|
||||
var _ proto.SchemaVisitor = ®ularFieldsPrinter{}
|
||||
var _ fieldsPrinter = ®ularFieldsPrinter{}
|
||||
|
||||
// VisitArray prints a Array type. It is just a passthrough.
|
||||
func (f *regularFieldsPrinter) VisitArray(a *proto.Array) {
|
||||
a.SubType.Accept(f)
|
||||
}
|
||||
|
||||
// VisitKind prints a Kind type. It prints each key in the kind, with
|
||||
// the type, the required flag, and the description.
|
||||
func (f *regularFieldsPrinter) VisitKind(k *proto.Kind) {
|
||||
for _, key := range k.Keys() {
|
||||
v := k.Fields[key]
|
||||
required := ""
|
||||
if k.IsRequired(key) {
|
||||
required = " -required-"
|
||||
}
|
||||
|
||||
if err := f.Writer.Write("%s\t<%s>%s", key, GetTypeName(v), required); err != nil {
|
||||
f.Error = err
|
||||
return
|
||||
}
|
||||
if err := f.Writer.Indent(indentDesc).WriteWrapped("%s", v.GetDescription()); err != nil {
|
||||
f.Error = err
|
||||
return
|
||||
}
|
||||
if err := f.Writer.Write(""); err != nil {
|
||||
f.Error = err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VisitMap prints a Map type. It is just a passthrough.
|
||||
func (f *regularFieldsPrinter) VisitMap(m *proto.Map) {
|
||||
m.SubType.Accept(f)
|
||||
}
|
||||
|
||||
// VisitPrimitive prints a Primitive type. It stops the recursion.
|
||||
func (f *regularFieldsPrinter) VisitPrimitive(p *proto.Primitive) {
|
||||
// Nothing to do. Shouldn't really happen.
|
||||
}
|
||||
|
||||
// VisitReference prints a Reference type. It is just a passthrough.
|
||||
func (f *regularFieldsPrinter) VisitReference(r proto.Reference) {
|
||||
r.SubSchema().Accept(f)
|
||||
}
|
||||
|
||||
// PrintFields will write the types from schema.
|
||||
func (f *regularFieldsPrinter) PrintFields(schema proto.Schema) error {
|
||||
schema.Accept(f)
|
||||
return f.Error
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Copyright 2017 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
|
||||
|
||||
// fieldsPrinterBuilder builds either a regularFieldsPrinter or a
|
||||
// recursiveFieldsPrinter based on the argument.
|
||||
type fieldsPrinterBuilder struct {
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
// BuildFieldsPrinter builds the appropriate fieldsPrinter.
|
||||
func (f fieldsPrinterBuilder) BuildFieldsPrinter(writer *Formatter) fieldsPrinter {
|
||||
if f.Recursive {
|
||||
return &recursiveFieldsPrinter{
|
||||
Writer: writer,
|
||||
}
|
||||
}
|
||||
|
||||
return ®ularFieldsPrinter{
|
||||
Writer: writer,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
Copyright 2017 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Formatter helps you write with indentation, and can wrap text as needed.
|
||||
type Formatter struct {
|
||||
IndentLevel int
|
||||
Wrap int
|
||||
Writer io.Writer
|
||||
}
|
||||
|
||||
// Indent creates a new Formatter that will indent the code by that much more.
|
||||
func (f Formatter) Indent(indent int) *Formatter {
|
||||
f.IndentLevel = f.IndentLevel + indent
|
||||
return &f
|
||||
}
|
||||
|
||||
// Write writes a string with the indentation set for the
|
||||
// Formatter. This is not wrapping text.
|
||||
func (f *Formatter) Write(str string, a ...interface{}) error {
|
||||
// Don't indent empty lines
|
||||
if str == "" {
|
||||
_, err := io.WriteString(f.Writer, "\n")
|
||||
return err
|
||||
}
|
||||
|
||||
indent := ""
|
||||
for i := 0; i < f.IndentLevel; i++ {
|
||||
indent = indent + " "
|
||||
}
|
||||
|
||||
if len(a) > 0 {
|
||||
str = fmt.Sprintf(str, a...)
|
||||
}
|
||||
_, err := io.WriteString(f.Writer, indent+str+"\n")
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteWrapped writes a string with the indentation set for the
|
||||
// Formatter, and wraps as needed.
|
||||
func (f *Formatter) WriteWrapped(str string, a ...interface{}) error {
|
||||
if f.Wrap == 0 {
|
||||
return f.Write(str, a...)
|
||||
}
|
||||
text := fmt.Sprintf(str, a...)
|
||||
strs := wrapString(text, f.Wrap-f.IndentLevel)
|
||||
for _, substr := range strs {
|
||||
if err := f.Write(substr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type line struct {
|
||||
wrap int
|
||||
words []string
|
||||
}
|
||||
|
||||
func (l *line) String() string {
|
||||
return strings.Join(l.words, " ")
|
||||
}
|
||||
|
||||
func (l *line) Empty() bool {
|
||||
return len(l.words) == 0
|
||||
}
|
||||
|
||||
func (l *line) Len() int {
|
||||
return len(l.String())
|
||||
}
|
||||
|
||||
// Add adds the word to the line, returns true if we could, false if we
|
||||
// didn't have enough room. It's always possible to add to an empty line.
|
||||
func (l *line) Add(word string) bool {
|
||||
newLine := line{
|
||||
wrap: l.wrap,
|
||||
words: append(l.words, word),
|
||||
}
|
||||
if newLine.Len() <= l.wrap || len(l.words) == 0 {
|
||||
l.words = newLine.words
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var bullet = regexp.MustCompile(`^(\d+\.?|-|\*)\s`)
|
||||
|
||||
func shouldStartNewLine(lastWord, str string) bool {
|
||||
// preserve line breaks ending in :
|
||||
if strings.HasSuffix(lastWord, ":") {
|
||||
return true
|
||||
}
|
||||
|
||||
// preserve code blocks
|
||||
if strings.HasPrefix(str, " ") {
|
||||
return true
|
||||
}
|
||||
str = strings.TrimSpace(str)
|
||||
// preserve empty lines
|
||||
if len(str) == 0 {
|
||||
return true
|
||||
}
|
||||
// preserve lines that look like they're starting lists
|
||||
if bullet.MatchString(str) {
|
||||
return true
|
||||
}
|
||||
// otherwise combine
|
||||
return false
|
||||
}
|
||||
|
||||
func wrapString(str string, wrap int) []string {
|
||||
wrapped := []string{}
|
||||
l := line{wrap: wrap}
|
||||
// track the last word added to the current line
|
||||
lastWord := ""
|
||||
flush := func() {
|
||||
if !l.Empty() {
|
||||
lastWord = ""
|
||||
wrapped = append(wrapped, l.String())
|
||||
l = line{wrap: wrap}
|
||||
}
|
||||
}
|
||||
|
||||
// iterate over the lines in the original description
|
||||
for _, str := range strings.Split(str, "\n") {
|
||||
// preserve code blocks and blockquotes as-is
|
||||
if strings.HasPrefix(str, " ") {
|
||||
flush()
|
||||
wrapped = append(wrapped, str)
|
||||
continue
|
||||
}
|
||||
|
||||
// preserve empty lines after the first line, since they can separate logical sections
|
||||
if len(wrapped) > 0 && len(strings.TrimSpace(str)) == 0 {
|
||||
flush()
|
||||
wrapped = append(wrapped, "")
|
||||
continue
|
||||
}
|
||||
|
||||
// flush if we should start a new line
|
||||
if shouldStartNewLine(lastWord, str) {
|
||||
flush()
|
||||
}
|
||||
words := strings.Fields(str)
|
||||
for _, word := range words {
|
||||
lastWord = word
|
||||
if !l.Add(word) {
|
||||
flush()
|
||||
if !l.Add(word) {
|
||||
panic("Couldn't add to empty line.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
flush()
|
||||
return wrapped
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
Copyright 2017 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
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
// fieldIndentLevel is the level of indentation for fields.
|
||||
fieldIndentLevel = 3
|
||||
// descriptionIndentLevel is the level of indentation for the
|
||||
// description.
|
||||
descriptionIndentLevel = 5
|
||||
)
|
||||
|
||||
// modelPrinter prints a schema in Writer. Its "Builder" will decide if
|
||||
// it's recursive or not.
|
||||
type modelPrinter struct {
|
||||
Name string
|
||||
Type string
|
||||
Descriptions []string
|
||||
Writer *Formatter
|
||||
Builder fieldsPrinterBuilder
|
||||
GVK schema.GroupVersionKind
|
||||
Error error
|
||||
}
|
||||
|
||||
var _ proto.SchemaVisitor = &modelPrinter{}
|
||||
|
||||
func (m *modelPrinter) PrintKindAndVersion() error {
|
||||
if err := m.Writer.Write("KIND: %s", m.GVK.Kind); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.Writer.Write("VERSION: %s\n", m.GVK.GroupVersion())
|
||||
}
|
||||
|
||||
// PrintDescription prints the description for a given schema. There
|
||||
// might be multiple description, since we collect descriptions when we
|
||||
// go through references, arrays and maps.
|
||||
func (m *modelPrinter) PrintDescription(schema proto.Schema) error {
|
||||
if err := m.Writer.Write("DESCRIPTION:"); err != nil {
|
||||
return err
|
||||
}
|
||||
empty := true
|
||||
for i, desc := range append(m.Descriptions, schema.GetDescription()) {
|
||||
if desc == "" {
|
||||
continue
|
||||
}
|
||||
empty = false
|
||||
if i != 0 {
|
||||
if err := m.Writer.Write(""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := m.Writer.Indent(descriptionIndentLevel).WriteWrapped("%s", desc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if empty {
|
||||
return m.Writer.Indent(descriptionIndentLevel).WriteWrapped("<empty>")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitArray recurses inside the subtype, while collecting the type if
|
||||
// not done yet, and the description.
|
||||
func (m *modelPrinter) VisitArray(a *proto.Array) {
|
||||
m.Descriptions = append(m.Descriptions, a.GetDescription())
|
||||
if m.Type == "" {
|
||||
m.Type = GetTypeName(a)
|
||||
}
|
||||
a.SubType.Accept(m)
|
||||
}
|
||||
|
||||
// VisitKind prints a full resource with its fields.
|
||||
func (m *modelPrinter) VisitKind(k *proto.Kind) {
|
||||
if err := m.PrintKindAndVersion(); err != nil {
|
||||
m.Error = err
|
||||
return
|
||||
}
|
||||
|
||||
if m.Type == "" {
|
||||
m.Type = GetTypeName(k)
|
||||
}
|
||||
if m.Name != "" {
|
||||
m.Writer.Write("RESOURCE: %s <%s>\n", m.Name, m.Type)
|
||||
}
|
||||
|
||||
if err := m.PrintDescription(k); err != nil {
|
||||
m.Error = err
|
||||
return
|
||||
}
|
||||
if err := m.Writer.Write("\nFIELDS:"); err != nil {
|
||||
m.Error = err
|
||||
return
|
||||
}
|
||||
m.Error = m.Builder.BuildFieldsPrinter(m.Writer.Indent(fieldIndentLevel)).PrintFields(k)
|
||||
}
|
||||
|
||||
// VisitMap recurses inside the subtype, while collecting the type if
|
||||
// not done yet, and the description.
|
||||
func (m *modelPrinter) VisitMap(om *proto.Map) {
|
||||
m.Descriptions = append(m.Descriptions, om.GetDescription())
|
||||
if m.Type == "" {
|
||||
m.Type = GetTypeName(om)
|
||||
}
|
||||
om.SubType.Accept(m)
|
||||
}
|
||||
|
||||
// VisitPrimitive prints a field type and its description.
|
||||
func (m *modelPrinter) VisitPrimitive(p *proto.Primitive) {
|
||||
if err := m.PrintKindAndVersion(); err != nil {
|
||||
m.Error = err
|
||||
return
|
||||
}
|
||||
|
||||
if m.Type == "" {
|
||||
m.Type = GetTypeName(p)
|
||||
}
|
||||
if err := m.Writer.Write("FIELD: %s <%s>\n", m.Name, m.Type); err != nil {
|
||||
m.Error = err
|
||||
return
|
||||
}
|
||||
m.Error = m.PrintDescription(p)
|
||||
}
|
||||
|
||||
func (m *modelPrinter) VisitArbitrary(a *proto.Arbitrary) {
|
||||
if err := m.PrintKindAndVersion(); err != nil {
|
||||
m.Error = err
|
||||
return
|
||||
}
|
||||
|
||||
m.Error = m.PrintDescription(a)
|
||||
}
|
||||
|
||||
// VisitReference recurses inside the subtype, while collecting the description.
|
||||
func (m *modelPrinter) VisitReference(r proto.Reference) {
|
||||
m.Descriptions = append(m.Descriptions, r.GetDescription())
|
||||
r.SubSchema().Accept(m)
|
||||
}
|
||||
|
||||
// PrintModel prints the description of a schema in writer.
|
||||
func PrintModel(name string, writer *Formatter, builder fieldsPrinterBuilder, schema proto.Schema, gvk schema.GroupVersionKind) error {
|
||||
m := &modelPrinter{Name: name, Writer: writer, Builder: builder, GVK: gvk}
|
||||
schema.Accept(m)
|
||||
return m.Error
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Copyright 2017 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
|
||||
|
||||
import "k8s.io/kube-openapi/pkg/util/proto"
|
||||
|
||||
// indentPerLevel is the level of indentation for each field recursion.
|
||||
const indentPerLevel = 3
|
||||
|
||||
// recursiveFieldsPrinter recursively prints all the fields for a given
|
||||
// schema.
|
||||
type recursiveFieldsPrinter struct {
|
||||
Writer *Formatter
|
||||
Error error
|
||||
}
|
||||
|
||||
var _ proto.SchemaVisitor = &recursiveFieldsPrinter{}
|
||||
var _ fieldsPrinter = &recursiveFieldsPrinter{}
|
||||
var visitedReferences = map[string]struct{}{}
|
||||
|
||||
// VisitArray is just a passthrough.
|
||||
func (f *recursiveFieldsPrinter) VisitArray(a *proto.Array) {
|
||||
a.SubType.Accept(f)
|
||||
}
|
||||
|
||||
// VisitKind prints all its fields with their type, and then recurses
|
||||
// inside each of these (pre-order).
|
||||
func (f *recursiveFieldsPrinter) VisitKind(k *proto.Kind) {
|
||||
for _, key := range k.Keys() {
|
||||
v := k.Fields[key]
|
||||
f.Writer.Write("%s\t<%s>", key, GetTypeName(v))
|
||||
subFields := &recursiveFieldsPrinter{
|
||||
Writer: f.Writer.Indent(indentPerLevel),
|
||||
}
|
||||
if err := subFields.PrintFields(v); err != nil {
|
||||
f.Error = err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VisitMap is just a passthrough.
|
||||
func (f *recursiveFieldsPrinter) VisitMap(m *proto.Map) {
|
||||
m.SubType.Accept(f)
|
||||
}
|
||||
|
||||
// VisitPrimitive does nothing, since it doesn't have sub-fields.
|
||||
func (f *recursiveFieldsPrinter) VisitPrimitive(p *proto.Primitive) {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
// VisitReference is just a passthrough.
|
||||
func (f *recursiveFieldsPrinter) VisitReference(r proto.Reference) {
|
||||
if _, ok := visitedReferences[r.Reference()]; ok {
|
||||
return
|
||||
}
|
||||
visitedReferences[r.Reference()] = struct{}{}
|
||||
r.SubSchema().Accept(f)
|
||||
delete(visitedReferences, r.Reference())
|
||||
}
|
||||
|
||||
// PrintFields will recursively print all the fields for the given
|
||||
// schema.
|
||||
func (f *recursiveFieldsPrinter) PrintFields(schema proto.Schema) error {
|
||||
schema.Accept(f)
|
||||
return f.Error
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "Kubernetes",
|
||||
"version": "v1.9.0"
|
||||
},
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"OneKind": {
|
||||
"description": "OneKind has a short description",
|
||||
"required": [
|
||||
"field1"
|
||||
],
|
||||
"properties": {
|
||||
"field1": {
|
||||
"description": "This is first reference field",
|
||||
"$ref": "#/definitions/ReferenceKind"
|
||||
},
|
||||
"field2": {
|
||||
"description": "This is other kind field with string and reference",
|
||||
"$ref": "#/definitions/OtherKind"
|
||||
}
|
||||
},
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "",
|
||||
"kind": "OneKind",
|
||||
"version": "v2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ReferenceKind": {
|
||||
"description": "This is reference Kind",
|
||||
"properties": {
|
||||
"referencefield": {
|
||||
"description": "This is reference to itself.",
|
||||
"$ref": "#/definitions/ReferenceKind"
|
||||
},
|
||||
"referencesarray": {
|
||||
"description": "This is an array of references",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"description": "This is reference object",
|
||||
"$ref": "#/definitions/ReferenceKind"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OtherKind": {
|
||||
"description": "This is other kind with string and reference fields",
|
||||
"properties": {
|
||||
"string": {
|
||||
"description": "This string must be a string",
|
||||
"type": "string"
|
||||
},
|
||||
"reference": {
|
||||
"description": "This is reference field.",
|
||||
"$ref": "#/definitions/ReferenceKind"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "Kubernetes",
|
||||
"version": "v1.9.0"
|
||||
},
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"PrimitiveDef": {
|
||||
"type": "string"
|
||||
},
|
||||
"OneKind": {
|
||||
"description": "OneKind has a short description",
|
||||
"required": [
|
||||
"field1"
|
||||
],
|
||||
"properties": {
|
||||
"field1": {
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut lacus ac enim vulputate imperdiet ac accumsan risus. Integer vel accumsan lectus. Praesent tempus nulla id tortor luctus, quis varius nulla laoreet. Ut orci nisi, suscipit id velit sed, blandit eleifend turpis. Curabitur tempus ante at lectus viverra, a mattis augue euismod. Morbi quam ligula, porttitor sit amet lacus non, interdum pulvinar tortor. Praesent accumsan risus et ipsum dictum, vel ullamcorper lorem egestas.",
|
||||
"$ref": "#/definitions/OtherKind"
|
||||
},
|
||||
"field2": {
|
||||
"description": "This is an array of object of PrimitiveDef",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"description": "This is an object of PrimitiveDef",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/PrimitiveDef"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "",
|
||||
"kind": "OneKind",
|
||||
"version": "v1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ControlCharacterKind": {
|
||||
"description": "Control character %",
|
||||
"properties": {
|
||||
"field1": {
|
||||
"description": "Control character %",
|
||||
}
|
||||
},
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "",
|
||||
"kind": "ControlCharacterKind",
|
||||
"version": "v1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"OtherKind": {
|
||||
"description": "This is another kind of Kind",
|
||||
"required": [
|
||||
"string"
|
||||
],
|
||||
"properties": {
|
||||
"string": {
|
||||
"description": "This string must be a string",
|
||||
"type": "string"
|
||||
},
|
||||
"int": {
|
||||
"description": "This int must be an int",
|
||||
"type": "integer"
|
||||
},
|
||||
"array": {
|
||||
"description": "This array must be an array of int",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"description": "This is an int in an array",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"object": {
|
||||
"description": "This is an object of string",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"description": "this is a string in an object",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primitive": {
|
||||
"$ref": "#/definitions/PrimitiveDef"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CrdKind": {
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "",
|
||||
"kind": "CrdKind",
|
||||
"version": "v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
Copyright 2017 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
)
|
||||
|
||||
// typeName finds the name of a schema
|
||||
type typeName struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
var _ proto.SchemaVisitor = &typeName{}
|
||||
|
||||
// VisitArray adds the [] prefix and recurses.
|
||||
func (t *typeName) VisitArray(a *proto.Array) {
|
||||
s := &typeName{}
|
||||
a.SubType.Accept(s)
|
||||
t.Name = fmt.Sprintf("[]%s", s.Name)
|
||||
}
|
||||
|
||||
// VisitKind just returns "Object".
|
||||
func (t *typeName) VisitKind(k *proto.Kind) {
|
||||
t.Name = "Object"
|
||||
}
|
||||
|
||||
// VisitMap adds the map[string] prefix and recurses.
|
||||
func (t *typeName) VisitMap(m *proto.Map) {
|
||||
s := &typeName{}
|
||||
m.SubType.Accept(s)
|
||||
t.Name = fmt.Sprintf("map[string]%s", s.Name)
|
||||
}
|
||||
|
||||
// VisitPrimitive returns the name of the primitive.
|
||||
func (t *typeName) VisitPrimitive(p *proto.Primitive) {
|
||||
t.Name = p.Type
|
||||
}
|
||||
|
||||
// VisitReference is just a passthrough.
|
||||
func (t *typeName) VisitReference(r proto.Reference) {
|
||||
r.SubSchema().Accept(t)
|
||||
}
|
||||
|
||||
// GetTypeName returns the type of a schema.
|
||||
func GetTypeName(schema proto.Schema) string {
|
||||
t := &typeName{}
|
||||
schema.Accept(t)
|
||||
return t.Name
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
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"
|
||||
"errors"
|
||||
"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 {
|
||||
generator := NewGenerator()
|
||||
if err := registerBuiltinTemplates(generator); err != nil {
|
||||
return fmt.Errorf("error parsing builtin templates. Please file a bug on GitHub: %w", err)
|
||||
}
|
||||
|
||||
return printModelDescriptionWithGenerator(
|
||||
generator, 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("couldn't find resource for \"%v\"", gvr)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
err = generator.Render(outputFormat, parsedV3Schema, gvr, fieldsPath, recursive, w)
|
||||
|
||||
explainErr := explainError("")
|
||||
if errors.As(err, &explainErr) {
|
||||
return explainErr
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/go-openapi/jsonreference"
|
||||
"k8s.io/kubectl/pkg/util/term"
|
||||
)
|
||||
|
||||
type explainError string
|
||||
|
||||
func (e explainError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func WithBuiltinTemplateFuncs(tmpl *template.Template) *template.Template {
|
||||
return tmpl.Funcs(map[string]interface{}{
|
||||
"throw": func(e string, args ...any) (string, error) {
|
||||
errString := fmt.Sprintf(e, args...)
|
||||
return "", explainError(errString)
|
||||
},
|
||||
"toJson": func(obj any) (string, error) {
|
||||
res, err := json.Marshal(obj)
|
||||
return string(res), err
|
||||
},
|
||||
"toPrettyJson": func(obj any) (string, error) {
|
||||
res, err := json.MarshalIndent(obj, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(res), err
|
||||
},
|
||||
"fail": func(message string) (string, error) {
|
||||
return "", errors.New(message)
|
||||
},
|
||||
"wrap": func(l int, s string) (string, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
writer := term.NewWordWrapWriter(buf, uint(l))
|
||||
_, err := writer.Write([]byte(s))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
},
|
||||
"split": func(s string, sep string) []string {
|
||||
return strings.Split(s, sep)
|
||||
},
|
||||
"join": func(sep string, strs ...string) string {
|
||||
return strings.Join(strs, sep)
|
||||
},
|
||||
"include": func(name string, data interface{}) (string, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
if err := tmpl.ExecuteTemplate(buf, name, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
},
|
||||
"ternary": func(a, b any, condition bool) any {
|
||||
if condition {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
},
|
||||
"first": func(list any) (any, error) {
|
||||
if list == nil {
|
||||
return nil, errors.New("list is empty")
|
||||
}
|
||||
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
if l == 0 {
|
||||
return nil, errors.New("list is empty")
|
||||
}
|
||||
|
||||
return l2.Index(0).Interface(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("first cannot be used on type: %T", list)
|
||||
}
|
||||
},
|
||||
"last": func(list any) (any, error) {
|
||||
if list == nil {
|
||||
return nil, errors.New("list is empty")
|
||||
}
|
||||
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
if l == 0 {
|
||||
return nil, errors.New("list is empty")
|
||||
}
|
||||
|
||||
return l2.Index(l - 1).Interface(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("last cannot be used on type: %T", list)
|
||||
}
|
||||
},
|
||||
"indent": func(amount int, str string) string {
|
||||
pad := strings.Repeat(" ", amount)
|
||||
return pad + strings.Replace(str, "\n", "\n"+pad, -1)
|
||||
},
|
||||
"dict": func(keysAndValues ...any) (map[string]any, error) {
|
||||
if len(keysAndValues)%2 != 0 {
|
||||
return nil, errors.New("expected even # of arguments")
|
||||
}
|
||||
|
||||
res := map[string]any{}
|
||||
for i := 0; i+1 < len(keysAndValues); i = i + 2 {
|
||||
if key, ok := keysAndValues[i].(string); ok {
|
||||
res[key] = keysAndValues[i+1]
|
||||
} else {
|
||||
return nil, fmt.Errorf("key of type %T is not a string as expected", key)
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
},
|
||||
"contains": func(list any, value any) bool {
|
||||
if list == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(list)
|
||||
switch val.Kind() {
|
||||
case reflect.Array:
|
||||
case reflect.Slice:
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
cur := val.Index(i)
|
||||
if cur.CanInterface() && reflect.DeepEqual(cur.Interface(), value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return false
|
||||
},
|
||||
"set": func(dict map[string]any, keysAndValues ...any) (any, error) {
|
||||
if len(keysAndValues)%2 != 0 {
|
||||
return nil, errors.New("expected even number of arguments")
|
||||
}
|
||||
|
||||
copyDict := make(map[string]any, len(dict))
|
||||
for k, v := range dict {
|
||||
copyDict[k] = v
|
||||
}
|
||||
|
||||
for i := 0; i < len(keysAndValues); i += 2 {
|
||||
key, ok := keysAndValues[i].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("keys must be strings")
|
||||
}
|
||||
|
||||
copyDict[key] = keysAndValues[i+1]
|
||||
}
|
||||
|
||||
return copyDict, nil
|
||||
},
|
||||
"list": func(values ...any) ([]any, error) {
|
||||
return values, nil
|
||||
},
|
||||
"add": func(value, operand int) int {
|
||||
return value + operand
|
||||
},
|
||||
"sub": func(value, operand int) int {
|
||||
return value - operand
|
||||
},
|
||||
"mul": func(value, operand int) int {
|
||||
return value * operand
|
||||
},
|
||||
"resolveRef": func(refAny any, document map[string]any) map[string]any {
|
||||
refString, ok := refAny.(string)
|
||||
if !ok {
|
||||
// if passed nil, or wrong type just treat the same
|
||||
// way as unresolved reference (makes for easier templates)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolve field path encoded by the ref
|
||||
ref, err := jsonreference.New(refString)
|
||||
if err != nil {
|
||||
// Unrecognized ref format.
|
||||
return nil
|
||||
}
|
||||
|
||||
if !ref.HasFragmentOnly {
|
||||
// Downloading is not supported. Treat as not found
|
||||
return nil
|
||||
}
|
||||
|
||||
fragment := ref.GetURL().Fragment
|
||||
components := strings.Split(fragment, "/")
|
||||
cur := document
|
||||
|
||||
for _, k := range components {
|
||||
if len(k) == 0 {
|
||||
// first component is usually empty (#/components/) , etc
|
||||
continue
|
||||
}
|
||||
|
||||
next, ok := cur[k].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
cur = next
|
||||
}
|
||||
return cur
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"io"
|
||||
"text/template"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type Generator interface {
|
||||
AddTemplate(name string, contents string) error
|
||||
|
||||
Render(
|
||||
// Template to use for rendering
|
||||
templateName string,
|
||||
// Self-Contained OpenAPI Document Containing all schemas used by $ref
|
||||
// Only OpenAPI V3 documents are supported
|
||||
document map[string]interface{},
|
||||
// Resource within OpenAPI document for which to render explain schema
|
||||
gvr schema.GroupVersionResource,
|
||||
// Field path of child of resource to focus output onto
|
||||
fieldSelector []string,
|
||||
// Boolean indicating whether the fields should be rendered recursively/deeply
|
||||
recursive bool,
|
||||
// Output writer
|
||||
writer io.Writer,
|
||||
) error
|
||||
}
|
||||
|
||||
type TemplateContext struct {
|
||||
GVR schema.GroupVersionResource
|
||||
Document map[string]interface{}
|
||||
Recursive bool
|
||||
FieldPath []string
|
||||
}
|
||||
|
||||
type generator struct {
|
||||
templates map[string]*template.Template
|
||||
}
|
||||
|
||||
func NewGenerator() Generator {
|
||||
return &generator{
|
||||
templates: make(map[string]*template.Template),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) AddTemplate(name string, contents string) error {
|
||||
compiled, err := WithBuiltinTemplateFuncs(template.New(name)).Parse(contents)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.templates[name] = compiled
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *generator) Render(
|
||||
// Template to use for rendering
|
||||
templateName string,
|
||||
// Self-Contained OpenAPI Document Containing all schemas used by $ref
|
||||
// Only OpenAPI V3 documents are supported
|
||||
document map[string]interface{},
|
||||
// Resource within OpenAPI document for which to render explain schema
|
||||
gvr schema.GroupVersionResource,
|
||||
// Field path of child of resource to focus output onto
|
||||
fieldSelector []string,
|
||||
// Boolean indicating whether the fields should be rendered recursively/deeply
|
||||
recursive bool,
|
||||
// Output writer
|
||||
writer io.Writer,
|
||||
) error {
|
||||
compiledTemplate, ok := g.templates[templateName]
|
||||
if !ok {
|
||||
return fmt.Errorf("unrecognized format: %s", templateName)
|
||||
}
|
||||
|
||||
err := compiledTemplate.Execute(writer, TemplateContext{
|
||||
Document: document,
|
||||
Recursive: recursive,
|
||||
FieldPath: fieldSelector,
|
||||
GVR: gvr,
|
||||
})
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
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 (
|
||||
"embed"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed templates/*.tmpl
|
||||
var rawBuiltinTemplates embed.FS
|
||||
|
||||
func registerBuiltinTemplates(gen Generator) error {
|
||||
files, err := rawBuiltinTemplates.ReadDir("templates")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range files {
|
||||
contents, err := rawBuiltinTemplates.ReadFile("templates/" + entry.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = gen.AddTemplate(
|
||||
strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())),
|
||||
string(contents))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,339 @@
|
|||
{{- /* Determine if Path for requested GVR is at /api or /apis based on emptiness of group */ -}}
|
||||
{{- $prefix := (ternary "/api" (join "" "/apis/" $.GVR.Group) (not $.GVR.Group)) -}}
|
||||
|
||||
{{- /* Search both cluster-scoped and namespaced-scoped paths for the GVR to find its GVK */ -}}
|
||||
{{- /* Also search for paths with {name} component in case the list path is missing */ -}}
|
||||
{{- /* Looks for the following paths: */ -}}
|
||||
{{- /* /apis/<group>/<version>/<resource> */ -}}
|
||||
{{- /* /apis/<group>/<version>/<resource>/{name} */ -}}
|
||||
{{- /* /apis/<group>/<version>/namespaces/{namespace}/<resource> */ -}}
|
||||
{{- /* /apis/<group>/<version>/namespaces/{namespace}/<resource>/{name} */ -}}
|
||||
{{- /* Also search for get verb paths in case list verb is missing */ -}}
|
||||
{{- $clusterScopedSearchPath := join "/" $prefix $.GVR.Version $.GVR.Resource -}}
|
||||
{{- $clusterScopedNameSearchPath := join "/" $prefix $.GVR.Version $.GVR.Resource "{name}" -}}
|
||||
{{- $namespaceScopedSearchPath := join "/" $prefix $.GVR.Version "namespaces" "{namespace}" $.GVR.Resource -}}
|
||||
{{- $namespaceScopedNameSearchPath := join "/" $prefix $.GVR.Version "namespaces" "{namespace}" $.GVR.Resource "{name}" -}}
|
||||
{{- $gvk := "" -}}
|
||||
|
||||
{{- /* Pull GVK from operation */ -}}
|
||||
{{- range $index, $searchPath := (list $clusterScopedSearchPath $clusterScopedNameSearchPath $namespaceScopedSearchPath $namespaceScopedNameSearchPath) -}}
|
||||
{{- with $resourcePathElement := index $.Document "paths" $searchPath -}}
|
||||
{{- range $methodIndex, $method := (list "get" "post" "put" "patch" "delete") -}}
|
||||
{{- with $resourceMethodPathElement := index $resourcePathElement $method -}}
|
||||
{{- with $gvk = index $resourceMethodPathElement "x-kubernetes-group-version-kind" -}}
|
||||
{{- break -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- with $gvk -}}
|
||||
{{- if $gvk.group -}}
|
||||
GROUP: {{ $gvk.group }}{{"\n" -}}
|
||||
{{- end -}}
|
||||
KIND: {{ $gvk.kind}}{{"\n" -}}
|
||||
VERSION: {{ $gvk.version }}{{"\n" -}}
|
||||
{{- "\n" -}}
|
||||
|
||||
{{- with include "schema" (dict "gvk" $gvk "Document" $.Document "FieldPath" $.FieldPath "Recursive" $.Recursive) -}}
|
||||
{{- . -}}
|
||||
{{- else -}}
|
||||
{{- throw "error: GVK %v not found in OpenAPI schema" $gvk -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- throw "error: GVR (%v) not found in OpenAPI schema" $.GVR.String -}}
|
||||
{{- end -}}
|
||||
{{- "\n" -}}
|
||||
|
||||
{{- /*
|
||||
Finds a schema with the given GVK and prints its explain output or empty string
|
||||
if GVK was not found
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
gvk: openapiv3 JSON schema
|
||||
Document: entire doc
|
||||
FieldPath: field path to follow
|
||||
Recursive: print recursive
|
||||
*/ -}}
|
||||
{{- define "schema" -}}
|
||||
{{- /* Find definition with this GVK by filtering out the components/schema with the given x-kubernetes-group-version-kind */ -}}
|
||||
{{- range index $.Document "components" "schemas" -}}
|
||||
{{- if contains (index . "x-kubernetes-group-version-kind") $.gvk -}}
|
||||
{{- with include "output" (set $ "schema" .) -}}
|
||||
{{- . -}}
|
||||
{{- else -}}
|
||||
{{- $fieldName := (index $.FieldPath (sub (len $.FieldPath) 1)) -}}
|
||||
{{- throw "error: field \"%v\" does not exist" $fieldName}}
|
||||
{{- end -}}
|
||||
{{- break -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
Follows FieldPath until the FieldPath is empty. Then prints field name and field
|
||||
list of resultant schema. If field path is not found. Prints nothing.
|
||||
Example output:
|
||||
|
||||
FIELD: spec
|
||||
|
||||
DESCRIPTION:
|
||||
<template "description">
|
||||
|
||||
FIELDS:
|
||||
<template "fieldList">
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
schema: openapiv3 JSON schema
|
||||
history: map[string]int
|
||||
Document: entire doc
|
||||
FieldPath: field path to follow
|
||||
Recursive: print recursive
|
||||
*/ -}}
|
||||
{{- define "output" -}}
|
||||
{{- $refString := or (index $.schema "$ref") "" -}}
|
||||
{{- $nextContext := set $ "history" (set $.history $refString 1) -}}
|
||||
{{- $resolved := or (resolveRef $refString $.Document) $.schema -}}
|
||||
{{- if not $.FieldPath -}}
|
||||
DESCRIPTION:{{- "\n" -}}
|
||||
{{- or (include "description" (dict "schema" $resolved "Document" $.Document)) "<empty>" | wrap 76 | indent 4 -}}{{- "\n" -}}
|
||||
{{- with include "fieldList" (dict "schema" $resolved "level" 1 "Document" $.Document "Recursive" $.Recursive) -}}
|
||||
FIELDS:{{- "\n" -}}
|
||||
{{- . -}}
|
||||
{{- end -}}
|
||||
{{- else if and $refString (index $.history $refString) -}}
|
||||
{{- /* Stop and do nothing. Hit a cycle */ -}}
|
||||
{{- else if and $resolved.properties (index $resolved.properties (first $.FieldPath)) -}}
|
||||
{{- /* Schema has this property directly. Traverse to next schema */ -}}
|
||||
{{- $subschema := index $resolved.properties (first $.FieldPath) -}}
|
||||
{{- if eq 1 (len $.FieldPath) -}}
|
||||
{{- /* TODO: The original explain would say RESOURCE instead of FIELD here under some circumstances */ -}}
|
||||
FIELD: {{first $.FieldPath}} <{{ template "typeGuess" (dict "schema" $subschema "Document" $.Document) }}>{{"\n"}}
|
||||
{{- template "extractEnum" (dict "schema" $subschema "Document" $.Document "isLongView" true "limit" -1) -}}{{"\n"}}
|
||||
{{- "\n" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- template "output" (set $nextContext "history" (dict) "FieldPath" (slice $.FieldPath 1) "schema" $subschema ) -}}
|
||||
{{- else if $resolved.items -}}
|
||||
{{- /* If the schema is an array then traverse the array item fields */ -}}
|
||||
{{- template "output" (set $nextContext "schema" $resolved.items) -}}
|
||||
{{- else if $resolved.additionalProperties -}}
|
||||
{{- /* If the schema is a map then traverse the map item fields */ -}}
|
||||
{{- template "output" (set $nextContext "schema" $resolved.additionalProperties) -}}
|
||||
{{- else -}}
|
||||
{{- /* Last thing to try is all the alternatives in the allOf case */ -}}
|
||||
{{- /* Stop when one of the alternatives has an output at all */ -}}
|
||||
{{- range $index, $subschema := $resolved.allOf -}}
|
||||
{{- with include "output" (set $nextContext "schema" $subschema) -}}
|
||||
{{- . -}}
|
||||
{{- break -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
Prints list of fields of a given open api schema in following form:
|
||||
|
||||
field1 <type> -required-
|
||||
DESCRIPTION
|
||||
|
||||
field2 <type> -required-
|
||||
DESCRIPTION
|
||||
|
||||
or if Recursive is enabled:
|
||||
field1 <type> -required-
|
||||
subfield1 <type>
|
||||
subsubfield1 <type>
|
||||
subsubfield2 <type>
|
||||
subfield2 <type>
|
||||
field2 <type> -required-
|
||||
subfield1 <type>
|
||||
subfield2 <type>
|
||||
|
||||
Follows refs for field traversal. If there are cycles in the schema, they are
|
||||
detected and traversal ends.
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
schema: openapiv3 JSON schema
|
||||
level: indentation level
|
||||
history: map[string]int containing all ref names so far
|
||||
Document: entire doc
|
||||
Recursive: print recursive
|
||||
*/ -}}
|
||||
{{- define "fieldList" -}}
|
||||
{{- /* Resolve schema if it is a ref */}}
|
||||
{{- /* If this is a ref seen before, then ignore it */}}
|
||||
{{- $refString := or (index $.schema "$ref") "" }}
|
||||
{{- if and $refString (index (or $.history (dict)) $refString) -}}
|
||||
{{- /* Do nothing for cycle */}}
|
||||
{{- else -}}
|
||||
{{- $nextContext := set $ "history" (set $.history $refString 1) -}}
|
||||
{{- $resolved := or (resolveRef $refString $.Document) $.schema -}}
|
||||
{{- range $k, $v := $resolved.properties -}}
|
||||
{{- template "fieldDetail" (dict "name" $k "schema" $resolved "short" $.Recursive "level" $.level "Document" $.Document) -}}
|
||||
{{- if $.Recursive -}}
|
||||
{{- /* Check if we already know about this element */}}
|
||||
{{- template "fieldList" (set $nextContext "schema" $v "level" (add $.level 1)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $resolved.allOf -}}
|
||||
{{- template "fieldList" (set $nextContext "schema" .)}}
|
||||
{{- end -}}
|
||||
{{- if $resolved.items}}{{- template "fieldList" (set $nextContext "schema" $resolved.items)}}{{end}}
|
||||
{{- if $resolved.additionalProperties}}{{- template "fieldList" (set $nextContext "schema" $resolved.additionalProperties)}}{{end}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
|
||||
{{- /*
|
||||
|
||||
Prints a single field of the given schema
|
||||
Optionally prints in a one-line style
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
schema: openapiv3 JSON schema which contains the field
|
||||
name: name of field
|
||||
short: limit printing to a single-line summary
|
||||
level: indentation amount
|
||||
Document: openapi document
|
||||
*/ -}}
|
||||
{{- define "fieldDetail" -}}
|
||||
{{- $level := or $.level 0 -}}
|
||||
{{- $indentAmount := mul $level 2 -}}
|
||||
{{- $fieldSchema := index $.schema.properties $.name -}}
|
||||
{{- $.name | indent $indentAmount -}}{{"\t"}}<{{ template "typeGuess" (dict "schema" $fieldSchema "Document" $.Document) }}>
|
||||
{{- if contains $.schema.required $.name}} -required-{{- end -}}
|
||||
{{- template "extractEnum" (dict "schema" $fieldSchema "Document" $.Document "isLongView" false "limit" 4 "indentAmount" $indentAmount) -}}
|
||||
{{- "\n" -}}
|
||||
{{- if not $.short -}}
|
||||
{{- or $fieldSchema.description "<no description>" | wrap (sub 78 $indentAmount) | indent (add $indentAmount 2) -}}{{- "\n" -}}
|
||||
{{- "\n" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
|
||||
Prints the description of the given OpenAPI v3 schema. Also walks through all
|
||||
sibling schemas to the provided schema and prints the description of those schemas
|
||||
too
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
schema: openapiv3 JSON schema
|
||||
Document: document to resolve refs within
|
||||
*/ -}}
|
||||
{{- define "description" -}}
|
||||
{{- with or (resolveRef (index $.schema "$ref") $.Document) $.schema -}}
|
||||
{{- if .description -}}
|
||||
{{- .description -}}
|
||||
{{- "\n" -}}
|
||||
{{- end -}}
|
||||
{{- range .allOf -}}
|
||||
{{- template "description" (set $ "schema" .)}}
|
||||
{{- end -}}
|
||||
{{- if .items -}}
|
||||
{{- template "description" (set $ "schema" .items) -}}
|
||||
{{- end -}}
|
||||
{{- if .additionalProperties -}}
|
||||
{{- template "description" (set $ "schema" .additionalProperties) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Renders a shortstring representing an interpretation of what is the "type"
|
||||
of a subschema e.g.:
|
||||
|
||||
`string` `number`, `Object`, `[]Object`, `map[string]string`, etc.
|
||||
|
||||
Serves as a more helpful type hint than raw typical openapi `type` field
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
schema: openapiv3 JSON schema
|
||||
Document: openapi document
|
||||
*/ -}}
|
||||
{{- define "typeGuess" -}}
|
||||
{{- with $.schema -}}
|
||||
{{- if .items -}}
|
||||
[]{{template "typeGuess" (set $ "schema" .items)}}
|
||||
{{- else if .additionalProperties -}}
|
||||
map[string]{{template "typeGuess" (set $ "schema" .additionalProperties)}}
|
||||
{{- else if and .allOf (not .properties) (eq (len .allOf) 1) -}}
|
||||
{{- /* If allOf has a single element and there are no direct
|
||||
properties on the schema, defer to the allOf */ -}}
|
||||
{{- template "typeGuess" (set $ "schema" (first .allOf)) -}}
|
||||
{{- else if index . "$ref"}}
|
||||
{{- /* Parse the #!/components/schemas/io.k8s.Kind string into just the Kind name */ -}}
|
||||
{{- $ref := index . "$ref" -}}
|
||||
{{- /* Look up ref schema to see primitive type. Only put the ref type name if it is an object. */ -}}
|
||||
{{- $refSchema := resolveRef $ref $.Document -}}
|
||||
{{- if (or (not $refSchema) (or (not $refSchema.type) (eq $refSchema.type "object"))) -}}
|
||||
{{- $name := split $ref "/" | last -}}
|
||||
{{- or (split $name "." | last) "Object" -}}
|
||||
{{- else if $refSchema.type -}}
|
||||
{{- or $refSchema.type "Object" -}}
|
||||
{{- else -}}
|
||||
{{- or .type "Object" -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{/* Old explain used capitalized "Object". Just follow suit */}}
|
||||
{{- if eq .type "object" -}}Object
|
||||
{{- else -}}{{- or .type "Object" -}}{{- end -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- fail "expected schema argument to subtemplate 'typeguess'" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Check if there is any enum returns it in this format e.g.:
|
||||
|
||||
ENUM: "", BlockDevice, CharDevice, Directory
|
||||
enum: "", BlockDevice, CharDevice, Directory
|
||||
|
||||
Can change the style of enum in future by modifying this function
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
schema: openapiv3 JSON schema
|
||||
Document: openapi document
|
||||
isLongView: (boolean) Simple view: long list of all fields. Long view: all details of one field
|
||||
limit: (int) truncate the amount of enums that can be printed in simple view, -1 means all items
|
||||
indentAmount: intent of the beginning enum line in longform view
|
||||
*/ -}}
|
||||
{{- define "extractEnum" -}}
|
||||
{{- with $.schema -}}
|
||||
{{- if .enum -}}
|
||||
{{- $enumLen := len .enum -}}
|
||||
{{- $limit := or $.limit -1 -}}
|
||||
{{- if eq $.isLongView false -}}
|
||||
{{- "\n" -}}
|
||||
{{- "" | indent $.indentAmount -}}
|
||||
{{- "enum: " -}}
|
||||
{{- else -}}
|
||||
{{- "ENUM:" -}}
|
||||
{{- end -}}
|
||||
{{- range $index, $element := .enum -}}
|
||||
{{- /* Prints , .... and return the range when it goes over the limit */ -}}
|
||||
{{- if and (gt $limit -1) (ge $index $limit) -}}
|
||||
{{- ", ...." -}}
|
||||
{{- break -}}
|
||||
{{- end -}}
|
||||
{{- /* Use to reflect "" when we see empty string */ -}}
|
||||
{{- $elementType := printf "%T" $element -}}
|
||||
{{- /* Print out either `, ` or `\n ` based of the view */ -}}
|
||||
{{- /* Simple view */ -}}
|
||||
{{- if and (gt $index 0) (eq $.isLongView false) -}}
|
||||
{{- ", " -}}
|
||||
{{- /* Long view */ -}}
|
||||
{{- else if eq $.isLongView true -}}
|
||||
{{- "\n" -}}{{- "" | indent 4 -}}
|
||||
{{- end -}}
|
||||
{{- /* Convert empty string to `""` for more clarification */ -}}
|
||||
{{- if and (eq "string" $elementType) (eq $element "") -}}
|
||||
{{- `""` -}}
|
||||
{{- else -}}
|
||||
{{- $element -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
|
@ -1687,6 +1687,7 @@ k8s.io/kubectl/pkg/cmd/create
|
|||
k8s.io/kubectl/pkg/cmd/delete
|
||||
k8s.io/kubectl/pkg/cmd/describe
|
||||
k8s.io/kubectl/pkg/cmd/exec
|
||||
k8s.io/kubectl/pkg/cmd/explain
|
||||
k8s.io/kubectl/pkg/cmd/get
|
||||
k8s.io/kubectl/pkg/cmd/logs
|
||||
k8s.io/kubectl/pkg/cmd/testing
|
||||
|
@ -1696,6 +1697,8 @@ k8s.io/kubectl/pkg/cmd/util/editor/crlf
|
|||
k8s.io/kubectl/pkg/cmd/util/podcmd
|
||||
k8s.io/kubectl/pkg/cmd/wait
|
||||
k8s.io/kubectl/pkg/describe
|
||||
k8s.io/kubectl/pkg/explain
|
||||
k8s.io/kubectl/pkg/explain/v2
|
||||
k8s.io/kubectl/pkg/generate
|
||||
k8s.io/kubectl/pkg/metricsutil
|
||||
k8s.io/kubectl/pkg/polymorphichelpers
|
||||
|
|
Loading…
Reference in New Issue