Skip to content

Commit

Permalink
Merge pull request #21 from keisku/refactor
Browse files Browse the repository at this point in the history
Refactor for testability
  • Loading branch information
keisku authored Apr 27, 2024
2 parents 25613c6 + 73f96d9 commit ee94484
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 294 deletions.
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

0 comments on commit ee94484

Please sign in to comment.