Merge pull request #113024 from alexzielenski/explain-output-openapiv3
kubectl explain openapiv3 template foundations Kubernetes-commit: 33bc8750fb97657d3cd995bd4b32857947f1dac1
This commit is contained in:
commit
abd5295393
10
go.mod
10
go.mod
|
@ -32,8 +32,8 @@ require (
|
||||||
k8s.io/api v0.0.0-20221012115127-0184bd884c5e
|
k8s.io/api v0.0.0-20221012115127-0184bd884c5e
|
||||||
k8s.io/apimachinery v0.0.0-20221017194938-70a38aaa19ef
|
k8s.io/apimachinery v0.0.0-20221017194938-70a38aaa19ef
|
||||||
k8s.io/cli-runtime v0.0.0-20221015041739-b6a5653c8754
|
k8s.io/cli-runtime v0.0.0-20221015041739-b6a5653c8754
|
||||||
k8s.io/client-go v0.0.0-20221018035516-42a0e1ca70c4
|
k8s.io/client-go v0.0.0-20221018195526-197e4799dc56
|
||||||
k8s.io/component-base v0.0.0-20221017200238-034e08cbfdfb
|
k8s.io/component-base v0.0.0-20221019000337-4f487d08c46c
|
||||||
k8s.io/component-helpers v0.0.0-20221017200342-f55d4a0c1767
|
k8s.io/component-helpers v0.0.0-20221017200342-f55d4a0c1767
|
||||||
k8s.io/klog/v2 v2.80.1
|
k8s.io/klog/v2 v2.80.1
|
||||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280
|
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280
|
||||||
|
@ -94,9 +94,9 @@ replace (
|
||||||
k8s.io/api => k8s.io/api v0.0.0-20221012115127-0184bd884c5e
|
k8s.io/api => k8s.io/api v0.0.0-20221012115127-0184bd884c5e
|
||||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20221017194938-70a38aaa19ef
|
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20221017194938-70a38aaa19ef
|
||||||
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20221015041739-b6a5653c8754
|
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20221015041739-b6a5653c8754
|
||||||
k8s.io/client-go => k8s.io/client-go v0.0.0-20221018035516-42a0e1ca70c4
|
k8s.io/client-go => k8s.io/client-go v0.0.0-20221018195526-197e4799dc56
|
||||||
k8s.io/code-generator => k8s.io/code-generator v0.0.0-20221017194732-d6a8b70c7bea
|
k8s.io/code-generator => k8s.io/code-generator v0.0.0-20221018161754-557ce1f667c7
|
||||||
k8s.io/component-base => k8s.io/component-base v0.0.0-20221017200238-034e08cbfdfb
|
k8s.io/component-base => k8s.io/component-base v0.0.0-20221019000337-4f487d08c46c
|
||||||
k8s.io/component-helpers => k8s.io/component-helpers v0.0.0-20221017200342-f55d4a0c1767
|
k8s.io/component-helpers => k8s.io/component-helpers v0.0.0-20221017200342-f55d4a0c1767
|
||||||
k8s.io/metrics => k8s.io/metrics v0.0.0-20221017201900-57e823688239
|
k8s.io/metrics => k8s.io/metrics v0.0.0-20221017201900-57e823688239
|
||||||
)
|
)
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -545,10 +545,10 @@ k8s.io/apimachinery v0.0.0-20221017194938-70a38aaa19ef h1:4lKLUMgwg9xi0UV4qjwS2N
|
||||||
k8s.io/apimachinery v0.0.0-20221017194938-70a38aaa19ef/go.mod h1:/x4E+/xaA5ap3q0tWNh5IPFt63dzY1I2qP8zT4sr5Yg=
|
k8s.io/apimachinery v0.0.0-20221017194938-70a38aaa19ef/go.mod h1:/x4E+/xaA5ap3q0tWNh5IPFt63dzY1I2qP8zT4sr5Yg=
|
||||||
k8s.io/cli-runtime v0.0.0-20221015041739-b6a5653c8754 h1:DFW+3b/iGJ7pDcxg0WxRwXIGzHh32tkKoGW7zvl/SFE=
|
k8s.io/cli-runtime v0.0.0-20221015041739-b6a5653c8754 h1:DFW+3b/iGJ7pDcxg0WxRwXIGzHh32tkKoGW7zvl/SFE=
|
||||||
k8s.io/cli-runtime v0.0.0-20221015041739-b6a5653c8754/go.mod h1:VhlC3GpPPnhI7o+8McywZaDeruyDfbOeYgSxxChhlto=
|
k8s.io/cli-runtime v0.0.0-20221015041739-b6a5653c8754/go.mod h1:VhlC3GpPPnhI7o+8McywZaDeruyDfbOeYgSxxChhlto=
|
||||||
k8s.io/client-go v0.0.0-20221018035516-42a0e1ca70c4 h1:c2OVHhALzI2exr09upcz5xYJJJIYE7/CvAmj7UuTiIg=
|
k8s.io/client-go v0.0.0-20221018195526-197e4799dc56 h1:RW41+G8r/tFt+qUnYILOeGV9WAyGtjKGeOvuv0TYJzI=
|
||||||
k8s.io/client-go v0.0.0-20221018035516-42a0e1ca70c4/go.mod h1:r+Jiu2RH1zXcJsmml1qRHg9oBq4sHHcMRaiEV0GN0ME=
|
k8s.io/client-go v0.0.0-20221018195526-197e4799dc56/go.mod h1:r+Jiu2RH1zXcJsmml1qRHg9oBq4sHHcMRaiEV0GN0ME=
|
||||||
k8s.io/component-base v0.0.0-20221017200238-034e08cbfdfb h1:xi+Q1olbNRt1UKQO/NTBEwCql1vBI7y48dUACoSnYMc=
|
k8s.io/component-base v0.0.0-20221019000337-4f487d08c46c h1:JEJlkqGJMe1WCIri8MWhUc1vryNLbP6SFmspoSwMbt8=
|
||||||
k8s.io/component-base v0.0.0-20221017200238-034e08cbfdfb/go.mod h1:C6bKv2+Cn5xLxGAlDAFSkSqGtDiPEUrEA4y0BKdUO2I=
|
k8s.io/component-base v0.0.0-20221019000337-4f487d08c46c/go.mod h1:GzuOBZLTCAovez80ouX6fNL7eTjPnb0b2WDJkqL1k5c=
|
||||||
k8s.io/component-helpers v0.0.0-20221017200342-f55d4a0c1767 h1:1NIJhCNc61Jv0Opi1E0ke8c5fwNsy2ZU0r/zXzcoh00=
|
k8s.io/component-helpers v0.0.0-20221017200342-f55d4a0c1767 h1:1NIJhCNc61Jv0Opi1E0ke8c5fwNsy2ZU0r/zXzcoh00=
|
||||||
k8s.io/component-helpers v0.0.0-20221017200342-f55d4a0c1767/go.mod h1:QwKUXpBsJXPkBJiSLYaWUEvPyq0O7yWEvqo4/RXeRsc=
|
k8s.io/component-helpers v0.0.0-20221017200342-f55d4a0c1767/go.mod h1:QwKUXpBsJXPkBJiSLYaWUEvPyq0O7yWEvqo4/RXeRsc=
|
||||||
k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4=
|
k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4=
|
||||||
|
|
|
@ -18,14 +18,17 @@ package explain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/client-go/discovery"
|
||||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
"k8s.io/kubectl/pkg/explain"
|
"k8s.io/kubectl/pkg/explain"
|
||||||
|
explainv2 "k8s.io/kubectl/pkg/explain/v2"
|
||||||
"k8s.io/kubectl/pkg/util/i18n"
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
"k8s.io/kubectl/pkg/util/openapi"
|
"k8s.io/kubectl/pkg/util/openapi"
|
||||||
"k8s.io/kubectl/pkg/util/templates"
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
@ -62,12 +65,25 @@ type ExplainOptions struct {
|
||||||
|
|
||||||
Mapper meta.RESTMapper
|
Mapper meta.RESTMapper
|
||||||
Schema openapi.Resources
|
Schema openapi.Resources
|
||||||
|
|
||||||
|
// Toggles whether the OpenAPI v3 template-based renderer should be used to show
|
||||||
|
// output.
|
||||||
|
EnableOpenAPIV3 bool
|
||||||
|
|
||||||
|
// Name of the template to use with the openapiv3 template renderer. If
|
||||||
|
// `EnableOpenAPIV3` is disabled, this does nothing
|
||||||
|
OutputFormat string
|
||||||
|
|
||||||
|
// Client capable of fetching openapi documents from the user's cluster
|
||||||
|
DiscoveryClient discovery.DiscoveryInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExplainOptions(parent string, streams genericclioptions.IOStreams) *ExplainOptions {
|
func NewExplainOptions(parent string, streams genericclioptions.IOStreams) *ExplainOptions {
|
||||||
return &ExplainOptions{
|
return &ExplainOptions{
|
||||||
IOStreams: streams,
|
IOStreams: streams,
|
||||||
CmdParent: parent,
|
CmdParent: parent,
|
||||||
|
EnableOpenAPIV3: os.Getenv("KUBECTL_EXPLAIN_OPENAPIV3") == "true",
|
||||||
|
OutputFormat: "plaintext",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,6 +105,12 @@ func NewCmdExplain(parent string, f cmdutil.Factory, streams genericclioptions.I
|
||||||
}
|
}
|
||||||
cmd.Flags().BoolVar(&o.Recursive, "recursive", o.Recursive, "Print the fields of fields (Currently only 1 level deep)")
|
cmd.Flags().BoolVar(&o.Recursive, "recursive", o.Recursive, "Print the fields of fields (Currently only 1 level deep)")
|
||||||
cmd.Flags().StringVar(&o.APIVersion, "api-version", o.APIVersion, "Get different explanations for particular API version (API group/version)")
|
cmd.Flags().StringVar(&o.APIVersion, "api-version", o.APIVersion, "Get different explanations for particular API version (API group/version)")
|
||||||
|
|
||||||
|
// Only enable --output as a valid flag if the feature is enabled
|
||||||
|
if o.EnableOpenAPIV3 {
|
||||||
|
cmd.Flags().StringVar(&o.OutputFormat, "output", o.OutputFormat, "Format in which to render the schema")
|
||||||
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +126,15 @@ func (o *ExplainOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only openapi v3 needs the discovery client.
|
||||||
|
if o.EnableOpenAPIV3 {
|
||||||
|
clientset, err := f.KubernetesClientSet()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.DiscoveryClient = clientset.DiscoveryClient
|
||||||
|
}
|
||||||
|
|
||||||
o.args = args
|
o.args = args
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -142,6 +173,17 @@ func (o *ExplainOptions) Run() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.EnableOpenAPIV3 {
|
||||||
|
return explainv2.PrintModelDescription(
|
||||||
|
fieldsPath,
|
||||||
|
o.Out,
|
||||||
|
o.DiscoveryClient.OpenAPIV3(),
|
||||||
|
fullySpecifiedGVR,
|
||||||
|
recursive,
|
||||||
|
o.OutputFormat,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
gvk, _ := o.Mapper.KindFor(fullySpecifiedGVR)
|
gvk, _ := o.Mapper.KindFor(fullySpecifiedGVR)
|
||||||
if gvk.Empty() {
|
if gvk.Empty() {
|
||||||
gvk, err = o.Mapper.KindFor(fullySpecifiedGVR.GroupResource().WithVersion(""))
|
gvk, err = o.Mapper.KindFor(fullySpecifiedGVR.GroupResource().WithVersion(""))
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
"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 {
|
||||||
|
return printModelDescriptionWithGenerator(
|
||||||
|
NewGenerator(), 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("could not locate schema for %s", resourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return generator.Render(outputFormat, parsedV3Schema, gvr, fieldsPath, recursive, w)
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/openapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
var apiDiscoveryJSON string = `{"openapi":"3.0.0","info":{"title":"Kubernetes","version":"v1.26.0"},"paths":{"/apis/discovery.k8s.io/":{"get":{"tags":["discovery"],"description":"get information of a group","operationId":"getDiscoveryAPIGroup","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIGroup"}},"application/vnd.kubernetes.protobuf":{"schema":{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIGroup"}},"application/yaml":{"schema":{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIGroup"}}}},"401":{"description":"Unauthorized"}}}}},"components":{"schemas":{"io.k8s.apimachinery.pkg.apis.meta.v1.APIGroup":{"description":"APIGroup contains the name, the supported versions, and the preferred version of a group.","type":"object","required":["name","versions"],"properties":{"apiVersion":{"description":"APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources","type":"string"},"kind":{"description":"Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds","type":"string"},"name":{"description":"name is the name of the group.","type":"string","default":""},"preferredVersion":{"description":"preferredVersion is the version preferred by the API server, which probably is the storage version.","default":{},"allOf":[{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.GroupVersionForDiscovery"}]},"serverAddressByClientCIDRs":{"description":"a map of client CIDR to server address that is serving this group. This is to help clients reach servers in the most network-efficient way possible. Clients can use the appropriate server address as per the CIDR that they match. In case of multiple matches, clients should use the longest matching CIDR. The server returns only those CIDRs that it thinks that the client can match. For example: the master will return an internal IP CIDR only, if the client reaches the server using an internal IP. Server looks at X-Forwarded-For header or X-Real-Ip header or request.RemoteAddr (in that order) to get the client IP.","type":"array","items":{"default":{},"allOf":[{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ServerAddressByClientCIDR"}]}},"versions":{"description":"versions are the versions supported in this group.","type":"array","items":{"default":{},"allOf":[{"$ref":"#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.GroupVersionForDiscovery"}]}}},"x-kubernetes-group-version-kind":[{"group":"","kind":"APIGroup","version":"v1"}]},"io.k8s.apimachinery.pkg.apis.meta.v1.GroupVersionForDiscovery":{"description":"GroupVersion contains the \"group/version\" and \"version\" string of a version. It is made a struct to keep extensibility.","type":"object","required":["groupVersion","version"],"properties":{"groupVersion":{"description":"groupVersion specifies the API group and version in the form \"group/version\"","type":"string","default":""},"version":{"description":"version specifies the version in the form of \"version\". This is to save the clients the trouble of splitting the GroupVersion.","type":"string","default":""}}},"io.k8s.apimachinery.pkg.apis.meta.v1.ServerAddressByClientCIDR":{"description":"ServerAddressByClientCIDR helps the client to determine the server address that they should use, depending on the clientCIDR that they match.","type":"object","required":["clientCIDR","serverAddress"],"properties":{"clientCIDR":{"description":"The CIDR with which clients can match their IP to figure out the server address that they should use.","type":"string","default":""},"serverAddress":{"description":"Address of this server, suitable for a client that matches the above CIDR. This can be a hostname, hostname:port, IP or IP:port.","type":"string","default":""}}}},"securitySchemes":{"BearerToken":{"type":"apiKey","description":"Bearer Token authentication","name":"authorization","in":"header"}}}}`
|
||||||
|
var apiGroupsGVR schema.GroupVersionResource = schema.GroupVersionResource{
|
||||||
|
Group: "discovery.k8s.io",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "apigroups",
|
||||||
|
}
|
||||||
|
var apiGroupsDocument map[string]interface{} = func() map[string]interface{} {
|
||||||
|
var doc map[string]interface{}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(apiDiscoveryJSON), &doc)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}()
|
||||||
|
|
||||||
|
type FakeOpenAPIV3Client struct {
|
||||||
|
// Path:
|
||||||
|
// ContentType:
|
||||||
|
// OpenAPIV3 Schema bytes
|
||||||
|
Values map[string]map[string][]byte
|
||||||
|
FetchCounts map[string]map[string]int
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type FakeGroupVersion struct {
|
||||||
|
Data map[string][]byte
|
||||||
|
FetchCounts map[string]int
|
||||||
|
Lock *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeGroupVersion) Schema(contentType string) ([]byte, error) {
|
||||||
|
f.Lock.Lock()
|
||||||
|
defer f.Lock.Unlock()
|
||||||
|
|
||||||
|
if count, ok := f.FetchCounts[contentType]; ok {
|
||||||
|
f.FetchCounts[contentType] = count + 1
|
||||||
|
} else {
|
||||||
|
f.FetchCounts[contentType] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := f.Data[contentType]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeOpenAPIV3Client) Paths() (map[string]openapi.GroupVersion, error) {
|
||||||
|
if f.Values == nil {
|
||||||
|
return nil, errors.New("values is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := map[string]openapi.GroupVersion{}
|
||||||
|
if f.FetchCounts == nil {
|
||||||
|
f.FetchCounts = map[string]map[string]int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range f.Values {
|
||||||
|
counts, ok := f.FetchCounts[k]
|
||||||
|
if !ok {
|
||||||
|
counts = map[string]int{}
|
||||||
|
f.FetchCounts[k] = counts
|
||||||
|
}
|
||||||
|
res[k] = &FakeGroupVersion{Data: v, FetchCounts: counts, Lock: &f.lock}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExplainErrors(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
// A client with nil `Values` will return error on returning paths
|
||||||
|
failFetchPaths := &FakeOpenAPIV3Client{}
|
||||||
|
|
||||||
|
err := PrintModelDescription(nil, &buf, failFetchPaths, apiGroupsGVR, false, "unknown-format")
|
||||||
|
require.ErrorContains(t, err, "failed to fetch list of groupVersions")
|
||||||
|
|
||||||
|
// Missing Schema
|
||||||
|
fakeClient := &FakeOpenAPIV3Client{
|
||||||
|
Values: map[string]map[string][]byte{
|
||||||
|
"apis/test1.example.com/v1": {
|
||||||
|
"unknown/content-type": []byte(apiDiscoveryJSON),
|
||||||
|
},
|
||||||
|
"apis/test2.example.com/v1": {
|
||||||
|
runtime.ContentTypeJSON: []byte(`<some invalid json!>`),
|
||||||
|
},
|
||||||
|
"apis/discovery.k8s.io/v1": {
|
||||||
|
runtime.ContentTypeJSON: []byte(apiDiscoveryJSON),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = PrintModelDescription(nil, &buf, fakeClient, schema.GroupVersionResource{
|
||||||
|
Group: "test0.example.com",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "doesntmatter",
|
||||||
|
}, false, "unknown-format")
|
||||||
|
require.ErrorContains(t, err, "could not locate schema")
|
||||||
|
|
||||||
|
// Missing JSON
|
||||||
|
err = PrintModelDescription(nil, &buf, fakeClient, schema.GroupVersionResource{
|
||||||
|
Group: "test1.example.com",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "doesntmatter",
|
||||||
|
}, false, "unknown-format")
|
||||||
|
require.ErrorContains(t, err, "failed to fetch openapi schema ")
|
||||||
|
|
||||||
|
err = PrintModelDescription(nil, &buf, fakeClient, schema.GroupVersionResource{
|
||||||
|
Group: "test2.example.com",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "doesntmatter",
|
||||||
|
}, false, "unknown-format")
|
||||||
|
require.ErrorContains(t, err, "failed to parse openapi schema")
|
||||||
|
|
||||||
|
err = PrintModelDescription(nil, &buf, fakeClient, apiGroupsGVR, false, "unknown-format")
|
||||||
|
require.ErrorContains(t, err, "unrecognized format: unknown-format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows that the correct GVR is fetched from the open api client when
|
||||||
|
// given to explain
|
||||||
|
func TestExplainOpenAPIClient(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
fakeClient := &FakeOpenAPIV3Client{
|
||||||
|
Values: map[string]map[string][]byte{
|
||||||
|
"apis/discovery.k8s.io/v1": {
|
||||||
|
runtime.ContentTypeJSON: []byte(apiDiscoveryJSON),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
gen := NewGenerator()
|
||||||
|
err := gen.AddTemplate("Context", "{{ toJson . }}")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedContext := TemplateContext{
|
||||||
|
Document: apiGroupsDocument,
|
||||||
|
GVR: apiGroupsGVR,
|
||||||
|
Recursive: false,
|
||||||
|
FieldPath: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = printModelDescriptionWithGenerator(gen, nil, &buf, fakeClient, apiGroupsGVR, false, "Context")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var actualContext TemplateContext
|
||||||
|
err = json.Unmarshal(buf.Bytes(), &actualContext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expectedContext, actualContext)
|
||||||
|
require.Equal(t, fakeClient.FetchCounts["apis/discovery.k8s.io/v1"][runtime.ContentTypeJSON], 1)
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 := template.
|
||||||
|
New(name).
|
||||||
|
Funcs(map[string]interface{}{
|
||||||
|
"toJson": func(obj any) (string, error) {
|
||||||
|
res, err := json.Marshal(obj)
|
||||||
|
return string(res), err
|
||||||
|
},
|
||||||
|
}).
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Shows generic throws error when attempting to `Render“ an invalid output name
|
||||||
|
// And if it is then added as a template, no error is thrown upon `Render`
|
||||||
|
func TestGeneratorMissingOutput(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
var doc map[string]interface{}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(apiDiscoveryJSON), &doc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
gen := NewGenerator()
|
||||||
|
badTemplateName := "bad-template"
|
||||||
|
err = gen.Render(badTemplateName, doc, apiGroupsGVR, nil, false, &buf)
|
||||||
|
|
||||||
|
require.ErrorContains(t, err, "unrecognized format: "+badTemplateName)
|
||||||
|
require.Zero(t, buf.Len())
|
||||||
|
|
||||||
|
err = gen.AddTemplate(badTemplateName, "ok")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = gen.Render(badTemplateName, doc, apiGroupsGVR, nil, false, &buf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "ok", buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows that correct context with the passed object is passed to the template
|
||||||
|
func TestGeneratorContext(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
var doc map[string]interface{}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(apiDiscoveryJSON), &doc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
gen := NewGenerator()
|
||||||
|
err = gen.AddTemplate("Context", "{{ toJson . }}")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedContext := TemplateContext{
|
||||||
|
Document: doc,
|
||||||
|
GVR: apiGroupsGVR,
|
||||||
|
Recursive: false,
|
||||||
|
FieldPath: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gen.Render("Context",
|
||||||
|
expectedContext.Document,
|
||||||
|
expectedContext.GVR,
|
||||||
|
expectedContext.FieldPath,
|
||||||
|
expectedContext.Recursive,
|
||||||
|
&buf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var actualContext TemplateContext
|
||||||
|
err = json.Unmarshal(buf.Bytes(), &actualContext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expectedContext, actualContext)
|
||||||
|
}
|
Loading…
Reference in New Issue