Merge pull request #5393 from hulizhe/karmadactlexplain

add new command karmadactl explain
This commit is contained in:
karmada-bot 2024-08-26 18:22:44 +08:00 committed by GitHub
commit 80037ce37f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 2253 additions and 0 deletions

View File

@ -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()
}

View File

@ -38,6 +38,7 @@ import (
"github.com/karmada-io/karmada/pkg/karmadactl/deinit" "github.com/karmada-io/karmada/pkg/karmadactl/deinit"
"github.com/karmada-io/karmada/pkg/karmadactl/describe" "github.com/karmada-io/karmada/pkg/karmadactl/describe"
"github.com/karmada-io/karmada/pkg/karmadactl/exec" "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/get"
"github.com/karmada-io/karmada/pkg/karmadactl/interpret" "github.com/karmada-io/karmada/pkg/karmadactl/interpret"
"github.com/karmada-io/karmada/pkg/karmadactl/join" "github.com/karmada-io/karmada/pkg/karmadactl/join"
@ -89,6 +90,7 @@ func NewKarmadaCtlCommand(cmdUse, parentCommand string) *cobra.Command {
{ {
Message: "Basic Commands:", Message: "Basic Commands:",
Commands: []*cobra.Command{ Commands: []*cobra.Command{
explain.NewCmdExplain(f, parentCommand, ioStreams),
get.NewCmdGet(f, parentCommand, ioStreams), get.NewCmdGet(f, parentCommand, ioStreams),
create.NewCmdCreate(f, parentCommand, ioStreams), create.NewCmdCreate(f, parentCommand, ioStreams),
}, },

234
vendor/k8s.io/kubectl/pkg/cmd/explain/explain.go generated vendored Normal file
View File

@ -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)
}

6
vendor/k8s.io/kubectl/pkg/explain/OWNERS generated vendored Normal file
View File

@ -0,0 +1,6 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- apelisse
reviewers:
- apelisse

168
vendor/k8s.io/kubectl/pkg/explain/explain.go generated vendored Normal file
View File

@ -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)
}

111
vendor/k8s.io/kubectl/pkg/explain/field_lookup.go generated vendored Normal file
View File

@ -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
}

82
vendor/k8s.io/kubectl/pkg/explain/fields_printer.go generated vendored Normal file
View File

@ -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 = &regularFieldsPrinter{}
var _ fieldsPrinter = &regularFieldsPrinter{}
// 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
}

View File

@ -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 &regularFieldsPrinter{
Writer: writer,
}
}

178
vendor/k8s.io/kubectl/pkg/explain/formatter.go generated vendored Normal file
View File

@ -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
}

163
vendor/k8s.io/kubectl/pkg/explain/model_printer.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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"
}
}
}
}
}

102
vendor/k8s.io/kubectl/pkg/explain/test-swagger.json generated vendored Normal file
View File

@ -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"
}
]
}
}
}

66
vendor/k8s.io/kubectl/pkg/explain/typename.go generated vendored Normal file
View File

@ -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
}

97
vendor/k8s.io/kubectl/pkg/explain/v2/explain.go generated vendored Normal file
View File

@ -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
}

240
vendor/k8s.io/kubectl/pkg/explain/v2/funcs.go generated vendored Normal file
View File

@ -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
},
})
}

102
vendor/k8s.io/kubectl/pkg/explain/v2/generator.go generated vendored Normal file
View File

@ -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
}

50
vendor/k8s.io/kubectl/pkg/explain/v2/template.go generated vendored Normal file
View File

@ -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
}

View File

@ -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 -}}

3
vendor/modules.txt vendored
View File

@ -1687,6 +1687,7 @@ k8s.io/kubectl/pkg/cmd/create
k8s.io/kubectl/pkg/cmd/delete k8s.io/kubectl/pkg/cmd/delete
k8s.io/kubectl/pkg/cmd/describe k8s.io/kubectl/pkg/cmd/describe
k8s.io/kubectl/pkg/cmd/exec k8s.io/kubectl/pkg/cmd/exec
k8s.io/kubectl/pkg/cmd/explain
k8s.io/kubectl/pkg/cmd/get k8s.io/kubectl/pkg/cmd/get
k8s.io/kubectl/pkg/cmd/logs k8s.io/kubectl/pkg/cmd/logs
k8s.io/kubectl/pkg/cmd/testing 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/util/podcmd
k8s.io/kubectl/pkg/cmd/wait k8s.io/kubectl/pkg/cmd/wait
k8s.io/kubectl/pkg/describe 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/generate
k8s.io/kubectl/pkg/metricsutil k8s.io/kubectl/pkg/metricsutil
k8s.io/kubectl/pkg/polymorphichelpers k8s.io/kubectl/pkg/polymorphichelpers