Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kubectl explain --output plaintext #113146

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion staging/src/k8s.io/kubectl/go.mod
Expand Up @@ -13,6 +13,7 @@ require (
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d
github.com/fatih/camelcase v1.0.0
github.com/fvbommel/sortorder v1.0.1
github.com/go-openapi/jsonreference v0.20.0
github.com/google/gnostic v0.5.7-v3refs
github.com/google/go-cmp v0.5.9
github.com/jonboulle/clockwork v0.2.2
Expand Down Expand Up @@ -52,7 +53,6 @@ require (
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
Expand Down
4 changes: 2 additions & 2 deletions staging/src/k8s.io/kubectl/pkg/cmd/explain/explain.go
Expand Up @@ -128,11 +128,11 @@ func (o *ExplainOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []

// Only openapi v3 needs the discovery client.
if o.EnableOpenAPIV3 {
clientset, err := f.KubernetesClientSet()
discoveryClient, err := f.ToDiscoveryClient()
if err != nil {
return err
}
o.DiscoveryClient = clientset.DiscoveryClient
o.DiscoveryClient = discoveryClient
}

o.args = args
Expand Down
9 changes: 7 additions & 2 deletions staging/src/k8s.io/kubectl/pkg/explain/v2/explain.go
Expand Up @@ -37,13 +37,18 @@ func PrintModelDescription(
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(
NewGenerator(), fieldsPath, w, client, gvr, recursive, outputFormat)
generator, fieldsPath, w, client, gvr, recursive, outputFormat)
}

// Factored out for testability
func printModelDescriptionWithGenerator(
generator *generator,
generator Generator,
fieldsPath []string,
w io.Writer,
client openapi.Client,
Expand Down
227 changes: 227 additions & 0 deletions staging/src/k8s.io/kubectl/pkg/explain/v2/funcs.go
@@ -0,0 +1,227 @@
/*
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"
)

func WithBuiltinTemplateFuncs(tmpl *template.Template) *template.Template {
return tmpl.Funcs(map[string]interface{}{
"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
},
"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
},
})
}