diff --git a/explainer.go b/explainer.go new file mode 100644 index 0000000..036c1f2 --- /dev/null +++ b/explainer.go @@ -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 +} diff --git a/explorer.go b/explorer.go index 8a36392..3994568 100644 --- a/explorer.go +++ b/explorer.go @@ -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 } @@ -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 @@ -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() @@ -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) -} diff --git a/explorer_test.go b/explorer_test.go index 7319e3c..e291560 100644 --- a/explorer_test.go +++ b/explorer_test.go @@ -5,173 +5,48 @@ import ( "fmt" "io" "net/http" + "strings" "testing" openapi_v2 "github.com/google/gnostic/openapiv2" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kube-openapi/pkg/util/proto" "k8s.io/kubectl/pkg/util/openapi" ) -func Test_Explorer_Explore(t *testing.T) { - openAPIResources := fetchOpenAPIResources(t) - tests := []struct { - inputFieldPath string - gvk schema.GroupVersionKind - wantW string - wantErr string - }{ - { - inputFieldPath: "node.spec.hoge", - gvk: schema.GroupVersionKind{ - Group: "", - Version: "v1", - Kind: "Node", - }, - wantErr: `explain "node.spec.hoge": field "hoge" does not exist`, - }, - { - inputFieldPath: "pod.spec.tolerations.key", - gvk: schema.GroupVersionKind{ - Group: "", - Version: "v1", - Kind: "Pod", - }, - wantW: `KIND: Pod -VERSION: v1 - -FIELD: key - -DESCRIPTION: - Key is the taint key that the toleration applies to. Empty means match all - taint keys. If the key is empty, operator must be Exists; this combination - means to match all values and all keys. -`, - }, - { - inputFieldPath: "pod.spec.serviceAccount", - gvk: schema.GroupVersionKind{ - Group: "", - Version: "v1", - Kind: "Pod", - }, - wantW: `KIND: Pod -VERSION: v1 - -FIELD: serviceAccount - -DESCRIPTION: - DeprecatedServiceAccount is a depreciated alias for ServiceAccountName. - Deprecated: Use serviceAccountName instead. -`, - }, - { - inputFieldPath: "node.spec", - gvk: schema.GroupVersionKind{ - Group: "", - Version: "v1", - Kind: "Node", - }, - wantW: `KIND: Node -VERSION: v1 - -RESOURCE: spec - -DESCRIPTION: - Spec defines the behavior of a node. - https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status - - NodeSpec describes the attributes that a node is created with. - -FIELDS: - configSource - Deprecated: Previously used to specify the source of the node's - configuration for the DynamicKubeletConfig feature. This feature is removed - from Kubelets as of 1.24 and will be fully removed in 1.26. - - externalID - Deprecated. Not all kubelets will set this field. Remove field after 1.13. - see: https://issues.k8s.io/61966 - - podCIDR - PodCIDR represents the pod IP range assigned to the node. - - podCIDRs <[]string> - podCIDRs represents the IP ranges assigned to the node for usage by Pods on - that node. If this field is specified, the 0th entry must match the podCIDR - field. It may contain at most 1 value for each of IPv4 and IPv6. - - providerID - ID of the node assigned by the cloud provider in the format: - :// - - taints <[]Object> - If specified, the node's taints. - - unschedulable - Unschedulable controls node schedulability of new pods. By default, node is - schedulable. More info: - https://kubernetes.io/docs/concepts/nodes/node/#manual-node-administration - -`, - }, - } - for _, tt := range tests { - t.Run(fmt.Sprintf(`Explain "%s"`, tt.inputFieldPath), func(t *testing.T) { - e, err := NewExplorer( - tt.inputFieldPath, - openAPIResources, - tt.gvk, - ) - assert.Nil(t, err) - // Overwrite this func for testing. - // Usually, the result depends on the user's input. - getPathToExplain = func(_ *Explorer) (string, error) { - return tt.inputFieldPath, nil - } - var b bytes.Buffer - err = e.Explore(&b) - if tt.wantErr == "" { - assert.Nil(t, err) - } else { - assert.EqualError(t, err, tt.wantErr) - } - assert.Equal(t, tt.wantW, b.String()) - }) +var k8sVersions = []string{"1.25", "1.26", "1.27", "1.28", "1.29", "1.30"} +var APIResourceByK8sVersion = func() map[string]openapi.Resources { + resources := make(map[string]openapi.Resources, len(k8sVersions)) + for _, version := range k8sVersions { + resources[version] = fetchOpenAPIResources(version) } -} + return resources +}() const urlToSwaggerJsonFormat = "https://raw.githubusercontent.com/kubernetes/kubernetes/release-%s/api/openapi-spec/swagger.json" -const swaggerJsonVersion = "1.25" // fetchOpenAPIResources fetches swagger.json from the Kubernetes release on GitHub. -func fetchOpenAPIResources(t *testing.T) openapi.Resources { - t.Helper() - - resp, err := http.DefaultClient.Get(fmt.Sprintf(urlToSwaggerJsonFormat, swaggerJsonVersion)) +func fetchOpenAPIResources(version string) openapi.Resources { + resp, err := http.DefaultClient.Get(fmt.Sprintf(urlToSwaggerJsonFormat, version)) if err != nil { - t.Fatalf("fetch swagger.json: %s", err) - return nil + panic(fmt.Sprintf("fetch swagger.json: %s", err)) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - t.Fatalf("read response body: %s", err) - return nil + panic(fmt.Sprintf("read response body: %s", err)) } doc, err := openapi_v2.ParseDocument(body) if err != nil { - t.Fatalf("parse swagger.json: %s", err) - return nil + panic(fmt.Sprintf("parse swagger.json: %s", err)) } r, err := openapi.NewOpenAPIData(doc) if err != nil { - t.Fatalf("creates a new resource from the doc: %s", err) - return nil + panic(fmt.Sprintf("creates a new resource from the doc: %s", err)) } return r } - func Test_fullformInputFieldPath(t *testing.T) { tests := []struct { inputFieldPath string @@ -197,3 +72,156 @@ func Test_fullformInputFieldPath(t *testing.T) { }) } } + +func Test_explain(t *testing.T) { + tests := []struct { + gvk schema.GroupVersionKind + expectUnsupport map[string]bool + // key: path, value: section keys to check + expectExplainOutput map[string][]string + }{ + { + gvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, + expectExplainOutput: map[string][]string{ + ".spec": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".status": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".metadata": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.hoge": {}, + ".spec.containers": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.affinity.podAffinity": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.affinity.podAffinity.preferredDuringSchedulingIgnoredDuringExecution.weight": {"KIND", "VERSION", "FIELD", "DESCRIPTION"}, + }, + }, + { + gvk: schema.GroupVersionKind{Group: "", Version: "v2", Kind: "Pod"}, + expectUnsupport: map[string]bool{"1.25": true, "1.26": true, "1.27": true, "1.28": true, "1.29": true, "1.30": true}, + }, + { + gvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"}, + expectExplainOutput: map[string][]string{ + ".spec": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".status": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".metadata": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.hoge": {}, + }, + }, + { + gvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}, + expectExplainOutput: map[string][]string{ + ".spec": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".status": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".metadata": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.hoge": {}, + }, + }, + { + gvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}, + expectExplainOutput: map[string][]string{ + ".spec": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".status": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".metadata": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.hoge": {}, + }, + }, + { + gvk: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + expectExplainOutput: map[string][]string{ + ".spec": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".status": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".metadata": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.hoge": {}, + }, + }, + { + gvk: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "DaemonSet"}, + expectExplainOutput: map[string][]string{ + ".spec": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".status": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".metadata": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.hoge": {}, + }, + }, + { + gvk: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}, + expectExplainOutput: map[string][]string{ + ".spec": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".status": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".metadata": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.hoge": {}, + }, + }, + { + gvk: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"}, + expectExplainOutput: map[string][]string{ + ".spec": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".status": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".metadata": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.hoge": {}, + }, + }, + { + gvk: schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "Job"}, + expectExplainOutput: map[string][]string{ + ".spec": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".status": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".metadata": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.hoge": {}, + }, + }, + { + gvk: schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "CronJob"}, + expectExplainOutput: map[string][]string{ + ".spec": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".status": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".metadata": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.hoge": {}, + }, + }, + { + gvk: schema.GroupVersionKind{Group: "autoscaling", Version: "v2", Kind: "HorizontalPodAutoscaler"}, + expectExplainOutput: map[string][]string{ + ".spec": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".status": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".metadata": {"KIND", "VERSION", "FIELD", "DESCRIPTION", "FIELDS"}, + ".spec.hoge": {}, + ".spec.maxReplicas": {"KIND", "VERSION", "FIELD", "DESCRIPTION"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.gvk.String(), func(t *testing.T) { + for _, version := range k8sVersions { + schema := APIResourceByK8sVersion[version].LookupResource(tt.gvk) + if tt.expectUnsupport[version] { + require.Empty(t, schema, "%s: schema found for %s", version, tt.gvk) + continue + } + require.NotNil(t, schema, "%s: schema not found for %s", version, tt.gvk) + pathSchema := make(map[string]proto.Schema) + e := &explainer{ + schemaByGvk: schema, + gvk: tt.gvk, + pathSchema: pathSchema, + } + v := &schemaVisitor{ + prevPath: "", + pathSchema: pathSchema, + err: nil, + } + schema.Accept(v) + require.NoError(t, v.err, "%s: schemaVisitor must not return an error", version) + for path, keys := range tt.expectExplainOutput { + var buf bytes.Buffer + err := e.explain(&buf, path) + if len(keys) == 0 { + require.Error(t, err, "%s: explain %q must return an error", version, path) + } else { + for _, key := range keys { + require.True(t, strings.Contains(buf.String(), key), "%s: explain %q must contain %q: actual output: %q", version, path, key, buf) + } + } + } + } + }) + } +} diff --git a/options.go b/options.go index ad18336..39df5d6 100644 --- a/options.go +++ b/options.go @@ -26,6 +26,10 @@ type Options struct { Mapper meta.RESTMapper Discovery discovery.CachedDiscoveryInterface Schema openapi.Resources + + inputFieldPath string + resource string + gvk schema.GroupVersionKind } func NewCmd() *cobra.Command { @@ -66,9 +70,8 @@ kubectl explore --context=onecontext f := cmdutil.NewFactory(matchVersionKubeConfigFlags) cmd.Run = func(_ *cobra.Command, args []string) { - cmdutil.CheckErr(o.Complete(f)) - cmdutil.CheckErr(o.Validate(args)) - cmdutil.CheckErr(o.Run(args)) + cmdutil.CheckErr(o.Complete(f, args)) + cmdutil.CheckErr(o.Run()) } return cmd } @@ -79,7 +82,13 @@ func NewOptions(streams genericclioptions.IOStreams) *Options { } } -func (o *Options) Complete(f cmdutil.Factory) error { +func (o *Options) Complete(f cmdutil.Factory, args []string) error { + if 0 < len(args) { + o.inputFieldPath = args[0] + } + if len(args) == 1 { + o.resource = args[0] + } var err error o.Discovery, err = f.ToDiscoveryClient() if err != nil { @@ -93,41 +102,23 @@ func (o *Options) Complete(f cmdutil.Factory) error { if err != nil { return err } - return nil -} - -func (o *Options) Validate(args []string) error { - if len(args) > 1 { - return fmt.Errorf("We accept only this format: explore RESOURCE") - } - - return nil -} - -func (o *Options) Run(args []string) error { - var inputFieldPath string - if 0 < len(args) { - inputFieldPath = args[0] - } - var resource string - if len(args) == 1 { - resource = args[0] - } - var gvk schema.GroupVersionKind - var err error - if resource == "" { - gvk, err = o.findGVK() + if o.resource == "" { + o.gvk, err = o.findGVK() } else { - gvk, err = o.getGVK(strings.Split(resource, ".")[0]) + o.gvk, err = o.getGVK(strings.Split(o.resource, ".")[0]) } if err != nil { return err } - e, err := NewExplorer(inputFieldPath, o.Schema, gvk) + return nil +} + +func (o *Options) Run() error { + e, err := newExplorer(o) if err != nil { return err } - return e.Explore(o.Out) + return e.explore(o.Out) } func (o *Options) findGVK() (schema.GroupVersionKind, error) { diff --git a/schema_visitor.go b/schema_visitor.go new file mode 100644 index 0000000..ac42ca8 --- /dev/null +++ b/schema_visitor.go @@ -0,0 +1,69 @@ +package main + +import ( + "sort" + "strings" + + "k8s.io/kube-openapi/pkg/util/proto" + "k8s.io/kubectl/pkg/explain" +) + +type schemaVisitor struct { + prevPath string + pathSchema map[string]proto.Schema + err error +} + +var _ proto.SchemaVisitor = (*schemaVisitor)(nil) + +func (v *schemaVisitor) VisitKind(k *proto.Kind) { + keys := k.Keys() + paths := make([]string, len(keys)) + for i, key := range keys { + paths[i] = strings.Join([]string{v.prevPath, key}, ".") + } + for i, key := range keys { + schema, err := explain.LookupSchemaForField(k, []string{key}) + if err != nil { + v.err = err + return + } + v.pathSchema[paths[i]] = schema + v.prevPath = paths[i] + schema.Accept(v) + } +} + +var visitedReferences = map[string]struct{}{} + +func (v *schemaVisitor) VisitReference(r proto.Reference) { + if _, ok := visitedReferences[r.Reference()]; ok { + return + } + visitedReferences[r.Reference()] = struct{}{} + r.SubSchema().Accept(v) + delete(visitedReferences, r.Reference()) +} + +func (*schemaVisitor) VisitPrimitive(*proto.Primitive) { + // Nothing to do. +} + +func (v *schemaVisitor) VisitArray(a *proto.Array) { + a.SubType.Accept(v) +} + +func (v *schemaVisitor) VisitMap(m *proto.Map) { + m.SubType.Accept(v) +} + +func (v *schemaVisitor) listPaths(filter func(string) bool) []string { + paths := make([]string, 0, len(v.pathSchema)) + for path := range v.pathSchema { + if filter(path) { + paths = append(paths, path) + } + } + sort.Strings(paths) + return paths +}