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

Refactor for testability #21

Merged
merged 2 commits into from
Apr 27, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions explainer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import (
"fmt"
"io"
"strings"

"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kube-openapi/pkg/util/proto"
"k8s.io/kubectl/pkg/explain"
)

type explainer struct {
schemaByGvk proto.Schema
gvk schema.GroupVersionKind
pathSchema map[string]proto.Schema
}

// explain explains the field associated with the given path.
func (e *explainer) explain(w io.Writer, path string) error {
if path == "" {
return fmt.Errorf("path is empty: gvk=%s", e.gvk)
}
// This is the case that path specifies the top-level field,
// for example, "pod.spec", "pod.metadata"
if strings.Count(path, ".") == 1 {
fieldPath := []string{path[strings.LastIndex(path, ".")+1:]}
return explain.PrintModelDescription(fieldPath, w, e.schemaByGvk, e.gvk, false)
}

// get the parent schema to explain.
// e.g. "pod.spec.containers.env" -> "pod.spec.containers"
parent, ok := e.pathSchema[path[:strings.LastIndex(path, ".")]]
if !ok {
return fmt.Errorf("%q is not found", path)
}

// get the key from the path.
// e.g. "pod.spec.containers.env" -> "env"
fieldPath := []string{path[strings.LastIndex(path, ".")+1:]}
if err := explain.PrintModelDescription(fieldPath, w, parent, e.gvk, false); err != nil {
return fmt.Errorf("explain %q: %w", path, err)
}
return nil
}
154 changes: 32 additions & 122 deletions explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,43 @@ import (
"bytes"
"fmt"
"io"
"sort"
"strings"

"github.com/ktr0731/go-fuzzyfinder"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kube-openapi/pkg/util/proto"
"k8s.io/kubectl/pkg/explain"
"k8s.io/kubectl/pkg/util/openapi"
)

// Explorer fields associated with each supported API resource to explain.
type Explorer struct {
openAPISchema openapi.Resources
err error
type explorer struct {
// inputFieldPath must use the full-formed kind in lower case.
// e.g., "hpa.spec" --> "horizontalpodautoscaler.spec"
// "deploy.spec.template" --> "deployment.spec.template"
// "sts.spec.template" --> "statefulset.spec.template"
inputFieldPath string
prevPath string
pathSchema map[string]proto.Schema
schemaByGvk proto.Schema
gvk schema.GroupVersionKind
schemaVisitor schemaVisitor
explainer explainer
}

// NewExplorer initializes Explorer.
func NewExplorer(fieldPath string, r openapi.Resources, gvk schema.GroupVersionKind) (*Explorer, error) {
s := r.LookupResource(gvk)
func newExplorer(o *Options) (*explorer, error) {
s := o.Schema.LookupResource(o.gvk)
if s == nil {
return nil, fmt.Errorf("%#v is not found on the Open API schema", gvk)
return nil, fmt.Errorf("no schema found for %s", o.gvk)
}
fullformedKind := strings.ToLower(gvk.Kind)
return &Explorer{
openAPISchema: r,
inputFieldPath: fullformInputFieldPath(fieldPath, fullformedKind),
prevPath: fullformedKind,
pathSchema: make(map[string]proto.Schema),
fullformedKind := strings.ToLower(o.gvk.Kind)
pathSchema := make(map[string]proto.Schema)
return &explorer{
inputFieldPath: fullformInputFieldPath(o.inputFieldPath, fullformedKind),
schemaByGvk: s,
gvk: gvk,
schemaVisitor: schemaVisitor{
prevPath: fullformedKind,
pathSchema: pathSchema,
err: nil,
},
explainer: explainer{
schemaByGvk: s,
gvk: o.gvk,
pathSchema: pathSchema,
},
}, nil
}

Expand All @@ -59,27 +57,24 @@ func fullformInputFieldPath(inputFieldPath, fullformedKind string) string {
return inputFieldPath
}

// Explore finds the field explanation, for example "pod.spec", "cronJob.spec.jobTemplate", etc.
func (e *Explorer) Explore(w io.Writer) error {
e.schemaByGvk.Accept(e)
if e.err != nil {
return e.err
func (e *explorer) explore(w io.Writer) error {
e.schemaByGvk.Accept(&e.schemaVisitor)
if e.schemaVisitor.err != nil {
return e.schemaVisitor.err
}

path, err := getPathToExplain(e)
path, err := e.resolvePathWithUserInput()
if err != nil {
return fmt.Errorf("get the path to explain: %w", err)
}

return e.explain(w, path)
return e.explainer.explain(w, path)
}

// getPathToExplain gets the path to explain by a user's input.
// Define this func as a variable for overwriting when tests.
var getPathToExplain = func(e *Explorer) (string, error) {
paths := e.paths()
func (e *explorer) resolvePathWithUserInput() (string, error) {
paths := e.schemaVisitor.listPaths(func(s string) bool {
return strings.Contains(s, e.inputFieldPath)
})
if len(paths) == 0 {
return "", nil
return "", fmt.Errorf("no paths found for %q", e.inputFieldPath)
}
if len(paths) == 1 {
return paths[0], nil
Expand All @@ -96,7 +91,7 @@ var getPathToExplain = func(e *Explorer) (string, error) {
return ""
}
var w bytes.Buffer
if err := e.explain(&w, paths[i]); err != nil {
if err := e.explainer.explain(&w, paths[i]); err != nil {
return fmt.Sprintf("preview is broken: %s", err)
}
return w.String()
Expand All @@ -107,88 +102,3 @@ var getPathToExplain = func(e *Explorer) (string, error) {
}
return paths[idx], nil
}

// paths returns paths explorer collects. paths that don't contain
// the path a user input will be ignored.
func (e *Explorer) paths() []string {
ps := make([]string, 0, len(e.pathSchema))
for p := range e.pathSchema {
if strings.Contains(p, e.inputFieldPath) {
ps = append(ps, p)
}
}
sort.Strings(ps)
return ps
}

// explain explains the field associated with the given path.
func (e *Explorer) explain(w io.Writer, path string) error {
// This is the case that selected resource doesn't have any fields to explain.
if path == "" {
return explain.PrintModelDescription([]string{}, w, e.schemaByGvk, e.gvk, false)
}
// This is the case that path specifies the top-level field,
// for example, "pod.spec", "pod.metadata"
if strings.Count(path, ".") == 1 {
fieldPath := []string{path[strings.LastIndex(path, ".")+1:]}
return explain.PrintModelDescription(fieldPath, w, e.schemaByGvk, e.gvk, false)
}

// get the parent schema to explain.
// e.g. "pod.spec.containers.env" -> "pod.spec.containers"
parent, ok := e.pathSchema[path[:strings.LastIndex(path, ".")]]
if !ok {
return fmt.Errorf("%q is not found", path)
}

// get the key from the path.
// e.g. "pod.spec.containers.env" -> "env"
fieldPath := []string{path[strings.LastIndex(path, ".")+1:]}
if err := explain.PrintModelDescription(fieldPath, w, parent, e.gvk, false); err != nil {
return fmt.Errorf("explain %q: %w", path, err)
}
return nil
}

var _ proto.SchemaVisitor = (*Explorer)(nil)

func (e *Explorer) VisitKind(k *proto.Kind) {
keys := k.Keys()
paths := make([]string, len(keys))
for i, key := range keys {
paths[i] = strings.Join([]string{e.prevPath, key}, ".")
}
for i, key := range keys {
schema, err := explain.LookupSchemaForField(k, []string{key})
if err != nil {
e.err = err
return
}
e.pathSchema[paths[i]] = schema
e.prevPath = paths[i]
schema.Accept(e)
}
}

var visitedReferences = map[string]struct{}{}

func (e *Explorer) VisitReference(r proto.Reference) {
if _, ok := visitedReferences[r.Reference()]; ok {
return
}
visitedReferences[r.Reference()] = struct{}{}
r.SubSchema().Accept(e)
delete(visitedReferences, r.Reference())
}

func (e *Explorer) VisitPrimitive(p *proto.Primitive) {
// Nothing to do.
}

func (e *Explorer) VisitArray(a *proto.Array) {
a.SubType.Accept(e)
}

func (e *Explorer) VisitMap(m *proto.Map) {
m.SubType.Accept(e)
}
Loading
Loading