Skip to content

Commit

Permalink
support yaml formatted openapi schema
Browse files Browse the repository at this point in the history
  • Loading branch information
natasha41575 committed Jul 2, 2021
1 parent d818cca commit 6c56a99
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 27 deletions.
20 changes: 20 additions & 0 deletions api/krusty/openapicustomschema_test.go
Expand Up @@ -17,6 +17,11 @@ func writeTestSchema(th kusttest_test.Harness, filepath string) {
th.WriteF(filepath+"mycrd_schema.json", string(bytes))
}

func writeTestSchemaYaml(th kusttest_test.Harness, filepath string) {
bytes, _ := ioutil.ReadFile("testdata/customschema.yaml")
th.WriteF(filepath+"mycrd_schema.yaml", string(bytes))
}

func writeCustomResource(th kusttest_test.Harness, filepath string) {
th.WriteF(filepath, `
apiVersion: example.com/v1alpha1
Expand Down Expand Up @@ -103,6 +108,21 @@ openapi:
th.AssertActualEqualsExpected(m, patchedCustomResource)
}

func TestCustomOpenApiFieldYaml(t *testing.T) {
th := kusttest_test.MakeHarness(t)
th.WriteK(".", `
resources:
- mycrd.yaml
openapi:
path: mycrd_schema.yaml
`+customSchemaPatch)
writeCustomResource(th, "mycrd.yaml")
writeTestSchemaYaml(th, "./")
openapi.ResetOpenAPI()
m := th.Run(".", th.MakeDefaultOptions())
th.AssertActualEqualsExpected(m, patchedCustomResource)
}

// Error if user tries to specify both builtin version
// and custom schema
func TestCustomOpenApiFieldBothPathAndVersion(t *testing.T) {
Expand Down
75 changes: 75 additions & 0 deletions api/krusty/testdata/customschema.yaml
@@ -0,0 +1,75 @@
definitions:
v1alpha1.MyCRD:
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
properties:
template:
"$ref": "#/definitions/io.k8s.api.core.v1.PodTemplateSpec"
type: object
status:
properties:
success:
type: boolean
type: object
type: object
x-kubernetes-group-version-kind:
- group: example.com
kind: MyCRD
version: v1alpha1
io.k8s.api.core.v1.PodTemplateSpec:
properties:
metadata:
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
spec:
"$ref": "#/definitions/io.k8s.api.core.v1.PodSpec"
type: object
io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta:
properties:
name:
type: string
type: object
io.k8s.api.core.v1.PodSpec:
properties:
containers:
items:
"$ref": "#/definitions/io.k8s.api.core.v1.Container"
type: array
x-kubernetes-patch-merge-key: name
x-kubernetes-patch-strategy: merge
type: object
io.k8s.api.core.v1.Container:
properties:
command:
items:
type: string
type: array
image:
type: string
name:
type: string
ports:
items:
"$ref": "#/definitions/io.k8s.api.core.v1.ContainerPort"
type: array
x-kubernetes-list-map-keys:
- containerPort
- protocol
x-kubernetes-list-type: map
x-kubernetes-patch-merge-key: containerPort
x-kubernetes-patch-strategy: merge
type: object
io.k8s.api.core.v1.ContainerPort:
properties:
containerPort:
type: integer
name:
type: string
protocol:
type: string
type: object
1 change: 1 addition & 0 deletions cmd/config/go.sum
Expand Up @@ -248,4 +248,5 @@ k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2R
sigs.k8s.io/kustomize/kyaml v0.11.0 h1:9KhiCPKaVyuPcgOLJXkvytOvjMJLoxpjodiycb4gHsA=
sigs.k8s.io/kustomize/kyaml v0.11.0/go.mod h1:GNMwjim4Ypgp/MueD3zXHLRJEjz7RvtPae0AwlvEMFM=
sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
40 changes: 31 additions & 9 deletions kustomize/commands/openapi/fetch/fetch.go
Expand Up @@ -8,26 +8,38 @@ import (
"os/exec"

"github.com/spf13/cobra"
"sigs.k8s.io/yaml"
)

var format string

// NewCmdFetch makes a new fetch command.
func NewCmdFetch(w io.Writer) *cobra.Command {
infoCmd := cobra.Command{
fetchCmd := cobra.Command{
Use: "fetch",
Short: `Fetches the OpenAPI specification from the current kubernetes cluster specified
in the user's kubeconfig`,
Example: `kustomize openapi fetch`,
Run: func(cmd *cobra.Command, args []string) {
printSchema(w)
RunE: func(cmd *cobra.Command, args []string) error {
return printSchema(w)
},
}
return &infoCmd
fetchCmd.Flags().StringVar(
&format,
"format",
"json",
"Specify format for fetched schema ('json' or 'yaml')")
return &fetchCmd
}

func printSchema(w io.Writer) {
func printSchema(w io.Writer) error {
if format != "json" && format != "yaml" {
return fmt.Errorf("format must be either 'json' or 'yaml'")
}

errMsg := `
Error fetching schema from cluster.
Please make sure kubectl is installed and its context is set correctly.
Please make sure kubectl is installed, its context is set correctly, and your cluster is up.
Installation and setup instructions: https://kubernetes.io/docs/tasks/tools/install-kubectl/`

command := exec.Command("kubectl", []string{"get", "--raw", "/openapi/v2"}...)
Expand All @@ -36,15 +48,25 @@ Installation and setup instructions: https://kubernetes.io/docs/tasks/tools/inst
command.Stdout = &stdout
command.Stderr = &stderr
err := command.Run()
if err != nil || stdout.String() == "" {
fmt.Fprintln(w, err, stderr.String()+errMsg)
return
if err != nil {
return fmt.Errorf(err.Error(), stderr.String()+errMsg)
} else if stdout.String() == "" {
return fmt.Errorf(stderr.String() + errMsg)
}

// format and output
var jsonSchema map[string]interface{}
output := stdout.Bytes()
json.Unmarshal(output, &jsonSchema)
output, _ = json.MarshalIndent(jsonSchema, "", " ")

if format == "yaml" {
output, err = yaml.JSONToYAML(output)
if err != nil {
return err
}
}

fmt.Fprintln(w, string(output))
return nil
}
1 change: 1 addition & 0 deletions kyaml/go.mod
Expand Up @@ -19,6 +19,7 @@ require (
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15
gopkg.in/yaml.v2 v2.4.0
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e
sigs.k8s.io/yaml v1.2.0
)

// These can be removed after upgrading golangci-lint (Issue #3663)
Expand Down
1 change: 1 addition & 0 deletions kyaml/go.sum
Expand Up @@ -225,4 +225,5 @@ k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM=
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=
sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
44 changes: 26 additions & 18 deletions kyaml/openapi/openapi.go
Expand Up @@ -15,7 +15,8 @@ import (
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/openapi/kubernetesapi"
"sigs.k8s.io/kustomize/kyaml/openapi/kustomizationapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/yaml"
)

// globalSchema contains global state information about the openapi
Expand All @@ -34,11 +35,11 @@ type openapiData struct {
schema spec.Schema

// schemaForResourceType is a map of Resource types to their schemas
schemaByResourceType map[yaml.TypeMeta]*spec.Schema
schemaByResourceType map[kyaml.TypeMeta]*spec.Schema

// namespaceabilityByResourceType stores whether a given Resource type
// is namespaceable or not
namespaceabilityByResourceType map[yaml.TypeMeta]bool
namespaceabilityByResourceType map[kyaml.TypeMeta]bool

// noUseBuiltInSchema stores whether we want to prevent using the built-n
// Kubernetes schema as part of the global schema
Expand Down Expand Up @@ -67,7 +68,7 @@ func (rs *ResourceSchema) IsMissingOrNull() bool {
// TODO(pwittrock): create a version of this function that will return a schema
// which can be used for duck-typed Resources -- e.g. contains common fields such
// as metadata, replicas and spec.template.spec
func SchemaForResourceType(t yaml.TypeMeta) *ResourceSchema {
func SchemaForResourceType(t kyaml.TypeMeta) *ResourceSchema {
initSchema()
rs, found := globalSchema.schemaByResourceType[t]
if !found {
Expand Down Expand Up @@ -108,8 +109,8 @@ func DefinitionRefs(openAPIPath string) ([]string, error) {

// definitionRefsFromRNode returns the list of openAPI definitions keys from input
// yaml RNode
func definitionRefsFromRNode(object *yaml.RNode) ([]string, error) {
definitions, err := object.Pipe(yaml.Lookup(SupplementaryOpenAPIFieldName, Definitions))
func definitionRefsFromRNode(object *kyaml.RNode) ([]string, error) {
definitions, err := object.Pipe(kyaml.Lookup(SupplementaryOpenAPIFieldName, Definitions))
if definitions == nil {
return nil, err
}
Expand All @@ -120,13 +121,13 @@ func definitionRefsFromRNode(object *yaml.RNode) ([]string, error) {
}

// parseOpenAPI reads openAPIPath yaml and converts it to RNode
func parseOpenAPI(openAPIPath string) (*yaml.RNode, error) {
func parseOpenAPI(openAPIPath string) (*kyaml.RNode, error) {
b, err := ioutil.ReadFile(openAPIPath)
if err != nil {
return nil, err
}

object, err := yaml.Parse(string(b))
object, err := kyaml.Parse(string(b))
if err != nil {
return nil, errors.Errorf("invalid file %q: %v", openAPIPath, err)
}
Expand All @@ -135,7 +136,7 @@ func parseOpenAPI(openAPIPath string) (*yaml.RNode, error) {

// addSchemaUsingField parses the OpenAPI definitions from the specified field.
// If field is the empty string, use the whole document as OpenAPI.
func schemaUsingField(object *yaml.RNode, field string) (*spec.Schema, error) {
func schemaUsingField(object *kyaml.RNode, field string) (*spec.Schema, error) {
if field != "" {
// get the field containing the openAPI
m := object.Field(field)
Expand All @@ -154,7 +155,7 @@ func schemaUsingField(object *yaml.RNode, field string) (*spec.Schema, error) {
// convert the yaml openAPI to a JSON string by unmarshalling it to an
// interface{} and the marshalling it to a string
var o interface{}
err = yaml.Unmarshal([]byte(oAPI), &o)
err = kyaml.Unmarshal([]byte(oAPI), &o)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -186,7 +187,7 @@ func ResetOpenAPI() {
func AddDefinitions(definitions spec.Definitions) {
// initialize values if they have not yet been set
if globalSchema.schemaByResourceType == nil {
globalSchema.schemaByResourceType = map[yaml.TypeMeta]*spec.Schema{}
globalSchema.schemaByResourceType = map[kyaml.TypeMeta]*spec.Schema{}
}
if globalSchema.schema.Definitions == nil {
globalSchema.schema.Definitions = spec.Definitions{}
Expand Down Expand Up @@ -218,18 +219,18 @@ func AddDefinitions(definitions spec.Definitions) {
}
}

func toTypeMeta(ext interface{}) (yaml.TypeMeta, bool) {
func toTypeMeta(ext interface{}) (kyaml.TypeMeta, bool) {
m, ok := ext.(map[string]interface{})
if !ok {
return yaml.TypeMeta{}, false
return kyaml.TypeMeta{}, false
}

g := m[groupKey].(string)
apiVersion := m[versionKey].(string)
if g != "" {
apiVersion = g + "/" + apiVersion
}
return yaml.TypeMeta{Kind: m[kindKey].(string), APIVersion: apiVersion}, true
return kyaml.TypeMeta{Kind: m[kindKey].(string), APIVersion: apiVersion}, true
}

// Resolve resolves the reference against the global schema
Expand Down Expand Up @@ -267,7 +268,7 @@ func GetSchema(s string, schema *spec.Schema) (*ResourceSchema, error) {
// resource is not known. If the type if found, the first return value will
// be true if the resource is namespace-scoped, and false if the type is
// cluster-scoped.
func IsNamespaceScoped(typeMeta yaml.TypeMeta) (bool, bool) {
func IsNamespaceScoped(typeMeta kyaml.TypeMeta) (bool, bool) {
initSchema()
isNamespaceScoped, found := globalSchema.namespaceabilityByResourceType[typeMeta]
return isNamespaceScoped, found
Expand All @@ -277,7 +278,7 @@ func IsNamespaceScoped(typeMeta yaml.TypeMeta) (bool, bool) {
// false for Pod, Deployment, etc. and kinds that aren't recognized in the
// openapi data. See:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces
func IsCertainlyClusterScoped(typeMeta yaml.TypeMeta) bool {
func IsCertainlyClusterScoped(typeMeta kyaml.TypeMeta) bool {
nsScoped, found := IsNamespaceScoped(typeMeta)
return found && !nsScoped
}
Expand Down Expand Up @@ -536,7 +537,14 @@ func parseBuiltinSchema(version string) {
// parse parses and indexes a single json schema
func parse(b []byte) error {
var swagger spec.Swagger

s := string(b)
if len(s) > 0 && s[0] != '{' {
var err error
b, err = yaml.YAMLToJSON(b)
if err != nil {
return err
}
}
if err := swagger.UnmarshalJSON(b); err != nil {
return errors.Wrap(err)
}
Expand All @@ -553,7 +561,7 @@ func parse(b []byte) error {
// parameter, the resource is namespace-scoped.
func findNamespaceability(paths *spec.Paths) {
if globalSchema.namespaceabilityByResourceType == nil {
globalSchema.namespaceabilityByResourceType = make(map[yaml.TypeMeta]bool)
globalSchema.namespaceabilityByResourceType = make(map[kyaml.TypeMeta]bool)
}

if paths == nil {
Expand Down

0 comments on commit 6c56a99

Please sign in to comment.