Merge pull request #2035 from pwittrock/openapi

Introduce OpenAPI kyaml libraries
This commit is contained in:
Kubernetes Prow Robot
2020-01-09 09:35:45 -08:00
committed by GitHub
18 changed files with 21917 additions and 9 deletions

View File

@@ -9,7 +9,6 @@ require (
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.4.0
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
k8s.io/apimachinery v0.17.0
sigs.k8s.io/kustomize/kyaml v0.0.0
)

View File

@@ -101,6 +101,8 @@ github.com/posener/complete/v2 v2.0.1-alpha.12/go.mod h1://JlL91cS2JV7rOl6LVHrRq
github.com/posener/script v1.0.4 h1:nSuXW5ZdmFnQIueLB2s0qvs4oNsUloM1Zydzh75v42w=
github.com/posener/script v1.0.4/go.mod h1:Rg3ijooqulo05aGLyGsHoLmIOUzHUVK19WVgrYBPU/E=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=

View File

@@ -68,6 +68,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
@@ -213,6 +214,7 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -331,6 +333,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=

View File

@@ -59,6 +59,7 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg=
@@ -217,6 +218,7 @@ github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nL
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=

View File

@@ -1,7 +1,7 @@
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
.PHONY: generate license fix vet fmt test lint tidy
.PHONY: generate license fix vet fmt test lint tidy openapi
GOPATH := $(shell go env GOPATH)
@@ -32,3 +32,7 @@ test:
vet:
go vet ./...
openapi:
(which $(GOPATH)/bin/go-bindata || go get -v github.com/go-bindata/go-bindata)
go-bindata --pkg openapi -o openapi/swagger.go openapi/swagger.json

View File

@@ -6,11 +6,13 @@ package copyutil
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/sergi/go-diff/diffmatchpatch"
"sigs.k8s.io/kustomize/kyaml/sets"
)
@@ -119,6 +121,9 @@ func Diff(sourceDir, destDir string) (sets.String, error) {
return diff, err
}
if !bytes.Equal(b1, b2) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(b1), string(b2), false)
fmt.Println(dmp.DiffPrettyText(diffs))
diff.Insert(f)
}
}

View File

@@ -6,7 +6,9 @@ require (
github.com/davecgh/go-spew v1.1.1
github.com/go-errors/errors v1.0.1
github.com/go-openapi/spec v0.19.5
github.com/sergi/go-diff v1.1.0
github.com/stretchr/testify v1.4.0
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca
gopkg.in/yaml.v2 v2.2.4
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d
)

View File

@@ -27,6 +27,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
@@ -45,6 +47,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -27,6 +27,7 @@ import (
"sort"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
@@ -54,7 +55,9 @@ func FormatFileOrDirectory(path string) error {
}.Execute()
}
type FormatFilter struct{}
type FormatFilter struct {
Process func(n *yaml.Node) error
}
var _ kio.Filter = FormatFilter{}
@@ -75,7 +78,9 @@ func (f FormatFilter) Filter(slice []*yaml.RNode) ([]*yaml.RNode, error) {
continue
}
kind, apiVersion := kindNode.YNode().Value, apiVersionNode.YNode().Value
err = (&formatter{apiVersion: apiVersion, kind: kind}).fmtNode(slice[i].YNode(), "")
s := openapi.SchemaForResourceType(yaml.TypeMeta{APIVersion: apiVersion, Kind: kind})
err = (&formatter{apiVersion: apiVersion, kind: kind, process: f.Process}).
fmtNode(slice[i].YNode(), "", s)
if err != nil {
return nil, err
}
@@ -86,10 +91,18 @@ func (f FormatFilter) Filter(slice []*yaml.RNode) ([]*yaml.RNode, error) {
type formatter struct {
apiVersion string
kind string
process func(n *yaml.Node) error
}
// fmtNode recursively formats the Document Contents.
func (f *formatter) fmtNode(n *yaml.Node, path string) error {
// See: https://godoc.org/gopkg.in/yaml.v3#Node
func (f *formatter) fmtNode(n *yaml.Node, path string, schema *openapi.ResourceSchema) error {
if n.Kind == yaml.ScalarNode && schema != nil && schema.Schema != nil {
// ensure values that are interpreted as non-string values (e.g. "true")
// are properly quoted
yaml.FormatNonStringStyle(n, *schema.Schema)
}
// sort the order of mapping fields
if n.Kind == yaml.MappingNode {
sort.Sort(sortedMapContents(*n))
@@ -104,12 +117,43 @@ func (f *formatter) fmtNode(n *yaml.Node, path string) error {
}
}
}
// format the Content
for i := range n.Content {
p := path
if n.Kind == yaml.MappingNode && i%2 == 1 {
p = fmt.Sprintf("%s.%s", path, n.Content[i-1].Value)
// MappingNode are structured as having their fields as Content,
// with the field-key and field-value alternating. e.g. Even elements
// are the keys and odd elements are the values
isFieldKey := n.Kind == yaml.MappingNode && i%2 == 0
isFieldValue := n.Kind == yaml.MappingNode && i%2 == 1
isElement := n.Kind == yaml.SequenceNode
// run the process callback on the node if it has been set
// don't process keys: their format should be fixed
if f.process != nil && !isFieldKey {
if err := f.process(n.Content[i]); err != nil {
return err
}
err := f.fmtNode(n.Content[i], p)
}
// get the schema for this Node
p := path
var s *openapi.ResourceSchema
switch {
case isFieldValue:
// if the node is a field, lookup the schema using the field name
p = fmt.Sprintf("%s.%s", path, n.Content[i-1].Value)
if schema != nil {
s = schema.Field(n.Content[i-1].Value)
}
case isElement:
// if the node is a list element, lookup the schema for the array items
if schema != nil {
s = schema.Elements()
}
}
// format the node using the schema
err := f.fmtNode(n.Content[i], p, s)
if err != nil {
return err
}
@@ -143,6 +187,7 @@ func (s sortedMapContents) Swap(i, j int) {
s.Content[iFieldValueIndex], s.Content[jFieldValueIndex] = s.
Content[jFieldValueIndex], s.Content[iFieldValueIndex]
}
func (s sortedMapContents) Less(i, j int) bool {
iFieldNameIndex := i * 2
jFieldNameIndex := j * 2

View File

@@ -13,10 +13,195 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/kio"
. "sigs.k8s.io/kustomize/kyaml/kio/filters"
"sigs.k8s.io/kustomize/kyaml/kio/filters/testyaml"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func TestFormatInput_FixYaml1_1Compatibility(t *testing.T) {
y := `
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
labels:
foo: on
foo2: hello1
annotations:
bar: 1
bar2: hello2
spec:
template:
spec:
containers:
- name: nginx
image: nginx:1.0.0
args:
- on
- 1
- hello
ports:
- name: http
targetPort: 80
containerPort: 80
`
// keep the style on values that parse as non-string types
expected := `apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
labels:
foo: "on"
foo2: hello1
annotations:
bar: "1"
bar2: hello2
spec:
template:
spec:
containers:
- name: nginx
image: nginx:1.0.0
args:
- "on"
- "1"
- hello
ports:
- name: http
targetPort: 80
containerPort: 80
`
buff := &bytes.Buffer{}
err := kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: strings.NewReader(y)}},
Filters: []kio.Filter{FormatFilter{}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: buff}},
}.Execute()
assert.NoError(t, err)
assert.Equal(t, expected, buff.String())
}
func TestFormatInput_PostprocessStyle(t *testing.T) {
y := `
apiVersion: v1
kind: Foo
metadata:
name: foo
spec:
notBoolean: "true"
notBoolean2: "on"
isBoolean: on
isBoolean2: true
notInt: "12345"
isInt: 12345
isString1: hello world
isString2: "hello world"
`
// keep the style on values that parse as non-string types
expected := `apiVersion: v1
kind: Foo
metadata:
name: foo
spec:
isBoolean: on
isBoolean2: true
isInt: 12345
isString1: hello world
isString2: hello world
notBoolean: "true"
notBoolean2: "on"
notInt: "12345"
`
buff := &bytes.Buffer{}
err := kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: strings.NewReader(y)}},
Filters: []kio.Filter{FormatFilter{Process: func(n *yaml.Node) error {
if yaml.IsYaml1_1NonString(n) {
// don't change these styles, they are important for backwards compatibility
// e.g. "on" must remain quoted, on must remain unquoted
return nil
}
// style does not have semantic meaning
n.Style = 0
return nil
}}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: buff}},
}.Execute()
assert.NoError(t, err)
assert.Equal(t, expected, buff.String())
y = `
apiVersion: v1
kind: Foo
metadata:
name: 'foo'
spec:
notBoolean: "true"
notBoolean2: "on"
notBoolean3: y is yes
isBoolean: on
isBoolean2: true
isBoolean3: y
notInt2: 1234 five
notInt3: one 2345
notInt: "12345"
isInt1: 12345
isInt2: -12345
isFloat1: 1.1234
isFloat2: 1.1234
isString1: hello world
isString2: "hello world"
isString3: 'hello world'
`
// keep the style on values that parse as non-string types
expected = `apiVersion: 'v1'
kind: 'Foo'
metadata:
name: 'foo'
spec:
isBoolean: on
isBoolean2: true
isBoolean3: y
isFloat1: 1.1234
isFloat2: 1.1234
isInt1: 12345
isInt2: -12345
isString1: 'hello world'
isString2: 'hello world'
isString3: 'hello world'
notBoolean: "true"
notBoolean2: "on"
notBoolean3: 'y is yes'
notInt: "12345"
notInt2: '1234 five'
notInt3: 'one 2345'
`
buff = &bytes.Buffer{}
err = kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: strings.NewReader(y)}},
Filters: []kio.Filter{FormatFilter{Process: func(n *yaml.Node) error {
if yaml.IsYaml1_1NonString(n) {
// don't change these styles, they are important for backwards compatibility
// e.g. "on" must remain quoted, on must remain unquoted
return nil
}
// style does not have semantic meaning
n.Style = yaml.SingleQuotedStyle
return nil
}}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: buff}},
}.Execute()
assert.NoError(t, err)
assert.Equal(t, expected, buff.String())
}
func TestFormatInput_Style(t *testing.T) {
y := `
apiVersion: v1

View File

@@ -0,0 +1,75 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package openapi_test
import (
"fmt"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func Example() {
s := openapi.SchemaForResourceType(yaml.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"})
f := s.Lookup("spec", "replicas")
fmt.Println(f.Schema.Description[:70] + "...")
fmt.Println(f.Schema.Type)
// Output:
// Number of desired pods. This is a pointer to distinguish between expli...
// [integer]
}
func Example_arrayMerge() {
s := openapi.SchemaForResourceType(yaml.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"})
f := s.Lookup("spec", "template", "spec", "containers")
fmt.Println(f.Schema.Description[:70] + "...")
fmt.Println(f.Schema.Type)
fmt.Println(f.PatchStrategyAndKey()) // merge patch strategy on name
// Output:
// List of containers belonging to the pod. Containers cannot currently b...
// [array]
// merge name
}
func Example_arrayReplace() {
s := openapi.SchemaForResourceType(yaml.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"})
f := s.Lookup("spec", "template", "spec", "containers", openapi.Elements, "args")
fmt.Println(f.Schema.Description[:70] + "...")
fmt.Println(f.Schema.Type)
fmt.Println(f.PatchStrategyAndKey()) // no patch strategy or merge key
// Output:
// Arguments to the entrypoint. The docker image's CMD is used if this is...
// [array]
}
func Example_arrayElement() {
s := openapi.SchemaForResourceType(yaml.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"})
f := s.Lookup("spec", "template", "spec", "containers",
openapi.Elements, "ports", openapi.Elements, "containerPort")
fmt.Println(f.Schema.Description[:70] + "...")
fmt.Println(f.Schema.Type)
// Output:
// Number of port to expose on the pod's IP address. This must be a valid...
// [integer]
}
func Example_map() {
s := openapi.SchemaForResourceType(yaml.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"})
f := s.Lookup("metadata", "labels")
fmt.Println(f.Schema.Description[:70] + "...")
fmt.Println(f.Schema.Type)
// Output:
// Map of string keys and values that can be used to organize and categor...
// [object]
}

200
kyaml/openapi/openapi.go Normal file
View File

@@ -0,0 +1,200 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package openapi
import (
"sync"
"github.com/go-openapi/spec"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
var setup sync.Once
var schema spec.Schema
var schemaByResourceType map[yaml.TypeMeta]*spec.Schema
// ResourceSchema wraps the OpenAPI Schema.
type ResourceSchema struct {
// Schema is the OpenAPI schema for a Resource or field
Schema *spec.Schema
}
// SchemaForResourceType returns the Schema for the given Resource
// 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 {
initSchema()
rs, found := schemaByResourceType[t]
if !found {
return nil
}
return &ResourceSchema{Schema: rs}
}
// Elements returns the Schema for the elements of an array.
func (r *ResourceSchema) Elements() *ResourceSchema {
// load the schema from swagger.json
initSchema()
if len(r.Schema.Type) != 1 || r.Schema.Type[0] != "array" {
// either not an array, or array has multiple types
return nil
}
s := *r.Schema.Items.Schema
for s.Ref.String() != "" {
sc, e := spec.ResolveRef(rootSchema(), &s.Ref)
if e != nil {
return nil
}
s = *sc
}
return &ResourceSchema{Schema: &s}
}
const Elements = "[]"
// Lookup calls either Field or Elements for each item in the path.
// If the path item is "[]", then Elements is called, otherwise
// Field is called.
// If any Field or Elements call returns nil, then Lookup returns
// nil immediately.
func (r *ResourceSchema) Lookup(path ...string) *ResourceSchema {
s := r
for _, p := range path {
if s == nil {
break
}
if p == Elements {
s = s.Elements()
continue
}
s = s.Field(p)
}
return s
}
// Field returns the Schema for a field.
func (r *ResourceSchema) Field(field string) *ResourceSchema {
// load the schema from swagger.json
initSchema()
// locate the Schema
s, found := r.Schema.Properties[field]
switch {
case found:
// no-op, continue with s as the schema
case r.Schema.AdditionalProperties != nil && r.Schema.AdditionalProperties.Schema != nil:
// map field type -- use Schema of the value
// (the key doesn't matter, they all have the same value type)
s = *r.Schema.AdditionalProperties.Schema
default:
// no Schema found from either swagger.json or line comments
return nil
}
// resolve the reference to the Schema if the Schema has one
for s.Ref.String() != "" {
sc, e := spec.ResolveRef(rootSchema(), &s.Ref)
if e != nil {
return nil
}
s = *sc
}
// return the merged Schema
return &ResourceSchema{Schema: &s}
}
// PatchStrategyAndKey returns the patch strategy and merge key extensions
func (r *ResourceSchema) PatchStrategyAndKey() (string, string) {
ps, found := r.Schema.Extensions[kubernetesPatchStrategyExtensionKey]
if !found {
// merge key and patch strategy must appear together
return "", ""
}
mk, found := r.Schema.Extensions[kubernetesMergeKeyExtensionKey]
if !found {
// merge key and patch strategy must appear together
return "", ""
}
return ps.(string), mk.(string)
}
const (
// openAPIAssetName is the name of the asset containing the statically compiled in
// OpenAPI definitions for Kubernetes built-in types
openAPIAssetName = "openapi/swagger.json"
// kubernetesGVKExtensionKey is the key to lookup the kubernetes group version kind extension
// -- the extension is an array of objects containing a gvk
kubernetesGVKExtensionKey = "x-kubernetes-group-version-kind"
// kubernetesMergeKeyExtensionKey is the key to lookup the kubernetes merge key extension
// -- the extension is a string
kubernetesMergeKeyExtensionKey = "x-kubernetes-patch-merge-key"
// kubernetesPatchStrategyExtensionKey is the key to lookup the kubernetes patch strategy
// extension -- the extension is a string
kubernetesPatchStrategyExtensionKey = "x-kubernetes-patch-strategy"
// groupKey is the key to lookup the group from the GVK extension
groupKey = "group"
// versionKey is the key to lookup the version from the GVK extension
versionKey = "version"
// kindKey is the the to lookup the kind from the GVK extension
kindKey = "kind"
)
// initSchema parses the json schema
func initSchema() {
setup.Do(func() {
// initialize the map
schemaByResourceType = map[yaml.TypeMeta]*spec.Schema{}
// parse the swagger, this should never fail
parse(MustAsset(openAPIAssetName))
// TODO(pwittrock): add support for parsing additional schemas from
// environment variables, files or other sources
})
}
// parse parses and indexes a single json schema
func parse(b []byte) {
if err := schema.UnmarshalJSON(b); err != nil {
panic(err)
}
// index the schema definitions so we can lookup them up for Resources
for k := range schema.Definitions {
// index by GVK, if no GVK is found then it is the schema for a subfield
// of a Resource
d := schema.Definitions[k]
gvk, found := d.VendorExtensible.Extensions[kubernetesGVKExtensionKey]
if !found {
continue
}
// cast the extension to a []map[string]string
exts, ok := gvk.([]interface{})
if !ok || len(exts) != 1 {
continue
}
m, ok := exts[0].(map[string]interface{})
if !ok {
continue
}
// build the index key and save it
g := m[groupKey].(string)
apiVersion := m[versionKey].(string)
if g != "" {
apiVersion = g + "/" + apiVersion
}
schemaByResourceType[yaml.TypeMeta{Kind: m[kindKey].(string), APIVersion: apiVersion}] = &d
}
}
func rootSchema() *spec.Schema {
initSchema()
return &schema
}

View File

@@ -0,0 +1,69 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package openapi_test
import (
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func TestSchemaForResourceType(t *testing.T) {
s := openapi.SchemaForResourceType(
yaml.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"})
if !assert.NotNil(t, s) {
t.FailNow()
}
f := s.Field("spec")
if !assert.NotNil(t, f) {
t.FailNow()
}
if !assert.Equal(t, "DeploymentSpec is the specification of the desired behavior of the Deployment.",
f.Schema.Description) {
t.FailNow()
}
replicas := f.Field("replicas")
if !assert.NotNil(t, replicas) {
t.FailNow()
}
if !assert.Equal(t, "Number of desired pods. This is a pointer to distinguish between explicit zero and not specified. Defaults to 1.",
replicas.Schema.Description) {
t.FailNow()
}
temp := f.Field("template")
if !assert.NotNil(t, temp) {
t.FailNow()
}
if !assert.Equal(t, "PodTemplateSpec describes the data a pod should have when created from a template",
temp.Schema.Description) {
t.FailNow()
}
containers := temp.Field("spec").Field("containers").Elements()
if !assert.NotNil(t, containers) {
t.FailNow()
}
targetPort := containers.Field("ports").Elements().Field("containerPort")
if !assert.NotNil(t, targetPort) {
t.FailNow()
}
if !assert.Equal(t, "Number of port to expose on the pod's IP address. This must be a valid port number, 0 < x < 65536.",
targetPort.Schema.Description) {
t.FailNow()
}
arg := containers.Field("args").Elements()
if !assert.NotNil(t, arg) {
t.FailNow()
}
if !assert.Equal(t, "string", arg.Schema.Type[0]) {
t.FailNow()
}
}

249
kyaml/openapi/swagger.go Normal file

File diff suppressed because one or more lines are too long

20840
kyaml/openapi/swagger.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package yaml
import (
"reflect"
"strings"
"github.com/go-openapi/spec"
y1_1 "gopkg.in/yaml.v2"
y1_2 "gopkg.in/yaml.v3"
)
// typeToTag maps OpenAPI schema types to yaml 1.2 tags
var typeToTag = map[string]string{
"string": "!!str",
"integer": "!!int",
"boolean": "!!bool",
"number": "!!float",
}
// FormatNonStringStyle makes sure that values which parse as non-string values in yaml 1.1
// are correctly formatted given the Schema type.
func FormatNonStringStyle(node *Node, schema spec.Schema) {
if len(schema.Type) != 1 {
return
}
t := schema.Type[0]
if !IsYaml1_1NonString(node) {
return
}
switch {
case t == "string" && schema.Format != "int-or-string":
if (node.Style&DoubleQuotedStyle == 0) && (node.Style&SingleQuotedStyle == 0) {
// must quote values so they are parsed as strings
node.Style = DoubleQuotedStyle
}
case t == "boolean" || t == "integer" || t == "number":
if (node.Style&DoubleQuotedStyle != 0) || (node.Style&SingleQuotedStyle != 0) {
// must NOT quote the values so they aren't parsed as strings
node.Style = 0
}
default:
return
}
if tag, found := typeToTag[t]; found {
// make sure the right tag is set
node.Tag = tag
}
}
// IsYaml1_1NonString returns true if the value parses as a non-string value in yaml 1.1
// when unquoted.
//
// Note: yaml 1.2 uses different keywords than yaml 1.1. Example: yaml 1.2 interprets
// `field: on` and `field: "on"` as equivalent (both strings). However Yaml 1.1 interprets
// `field: on` as on being a bool and `field: "on"` as on being a string.
// If an input is read with `field: "on"`, and the style is changed from DoubleQuote to 0,
// it will change the type of the field from a string to a bool. For this reason, fields
// which are keywords in yaml 1.1 should never have their style changed, as it would break
// backwards compatibility with yaml 1.1 -- which is what is used by the Kubernetes apiserver.
func IsYaml1_1NonString(node *Node) bool {
if node.Kind != y1_2.ScalarNode {
// not a keyword
return false
}
if strings.Contains(node.Value, "\n") {
// multi-line strings will fail to unmarshal
return false
}
// check if the value will unmarshal into a non-string value using a yaml 1.1 parser
var i1 interface{}
if err := y1_1.Unmarshal([]byte(node.Value), &i1); err != nil {
return false
}
if reflect.TypeOf(i1) != stringType {
return true
}
return false
}
var stringType = reflect.TypeOf("string")

View File

@@ -0,0 +1,134 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package yaml_test
import (
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func TestIsYaml1_1NonString(t *testing.T) {
type testCase struct {
val string
expected bool
}
testCases := []testCase{
{val: "hello world", expected: false},
{val: "2.0", expected: true},
{val: "1.0\nhello", expected: false}, // multiline strings should always be false
}
for k := range valueToTagMap {
testCases = append(testCases, testCase{val: k, expected: true})
}
for _, test := range testCases {
assert.Equal(t, test.expected,
yaml.IsYaml1_1NonString(&yaml.Node{Kind: yaml.ScalarNode, Value: test.val}), test.val)
}
}
func TestFormatNonStringStyle(t *testing.T) {
n := yaml.MustParse(`apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: foo
args:
- bar
- on
image: nginx:1.7.9
ports:
- name: http
containerPort: "80"
`)
s := openapi.SchemaForResourceType(
yaml.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"})
args, err := n.Pipe(yaml.Lookup(
"spec", "template", "spec", "containers", "[name=foo]", "args"))
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.NotNil(t, args) {
t.FailNow()
}
on := args.YNode().Content[1]
onS := s.Lookup(
"spec", "template", "spec", "containers", openapi.Elements, "args", openapi.Elements)
yaml.FormatNonStringStyle(on, *onS.Schema)
containerPort, err := n.Pipe(yaml.Lookup(
"spec", "template", "spec", "containers", "[name=foo]", "ports",
"[name=http]", "containerPort"))
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.NotNil(t, containerPort) {
t.FailNow()
}
cpS := s.Lookup("spec", "template", "spec", "containers", openapi.Elements,
"ports", openapi.Elements, "containerPort")
if !assert.NotNil(t, cpS) {
t.FailNow()
}
yaml.FormatNonStringStyle(containerPort.YNode(), *cpS.Schema)
actual := n.MustString()
expected := `apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: foo
args:
- bar
- "on"
image: nginx:1.7.9
ports:
- name: http
containerPort: 80
`
assert.Equal(t, expected, actual)
}
// valueToTagMap is a map of values interpreted as non-strings in yaml 1.1 when left
// unquoted.
// To keep compatibility with the yaml parser used by Kubernetes (yaml 1.1) make sure the values
// which are treated as non-string values are kept as non-string values.
// https://github.com/go-yaml/yaml/blob/v2/resolve.go
var valueToTagMap = func() map[string]string {
val := map[string]string{}
// https://yaml.org/type/null.html
values := []string{"", "~", "null", "Null", "NULL"}
for i := range values {
val[values[i]] = "!!null"
}
// https://yaml.org/type/bool.html
values = []string{
"y", "Y", "yes", "Yes", "YES", "true", "True", "TRUE", "on", "On", "ON", "n", "N", "no",
"No", "NO", "false", "False", "FALSE", "off", "Off", "OFF"}
for i := range values {
val[values[i]] = "!!bool"
}
// https://yaml.org/type/float.html
values = []string{
".nan", ".NaN", ".NAN", ".inf", ".Inf", ".INF",
"+.inf", "+.Inf", "+.INF", "-.inf", "-.Inf", "-.INF"}
for i := range values {
val[values[i]] = "!!float"
}
return val
}()

View File

@@ -223,6 +223,11 @@ func (m MapNodeSlice) Values() []*RNode {
return values
}
type TypeMeta struct {
Kind string
APIVersion string
}
// ResourceMeta contains the metadata for a both Resource Type and Resource.
type ResourceMeta struct {
// APIVersion is the apiVersion field of a Resource