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 Jun 25, 2021
1 parent dac84d8 commit ee6b106
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 20 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 @@ -245,4 +245,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=
26 changes: 24 additions & 2 deletions kustomize/commands/openapi/fetch/fetch.go
Expand Up @@ -8,11 +8,14 @@ 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`,
Expand All @@ -21,10 +24,20 @@ in the user's kubeconfig`,
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) {
if format != "json" && format != "yaml" {
fmt.Fprintln(w, "format must be either 'json' or 'yaml'")
return
}

errMsg := `
Error fetching schema from cluster.
Please make sure kubectl is installed and its context is set correctly.
Expand All @@ -46,5 +59,14 @@ Installation and setup instructions: https://kubernetes.io/docs/tasks/tools/inst
output := stdout.Bytes()
json.Unmarshal(output, &jsonSchema)
output, _ = json.MarshalIndent(jsonSchema, "", " ")

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

fmt.Fprintln(w, string(output))
}
3 changes: 3 additions & 0 deletions kyaml/filesys/filesystem_test.go
@@ -1,3 +1,6 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package filesys

import (
Expand Down
1 change: 1 addition & 0 deletions kyaml/go.mod
Expand Up @@ -19,6 +19,7 @@ require (
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
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 @@ -226,4 +226,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=
43 changes: 25 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,13 @@ 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] != '{' {
j, err := yaml.YAMLToJSON(b)
if err == nil {
b = j
}
}
if err := swagger.UnmarshalJSON(b); err != nil {
return errors.Wrap(err)
}
Expand All @@ -553,7 +560,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 ee6b106

Please sign in to comment.