mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-12 01:14:22 +00:00
Library for getting Resource and field Schema from OpenAPI
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Copyright 2019 The Kubernetes Authors.
|
# Copyright 2019 The Kubernetes Authors.
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# 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)
|
GOPATH := $(shell go env GOPATH)
|
||||||
|
|
||||||
@@ -32,3 +32,7 @@ test:
|
|||||||
|
|
||||||
vet:
|
vet:
|
||||||
go 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
|
||||||
|
|||||||
87
kyaml/openapi/example_test.go
Normal file
87
kyaml/openapi/example_test.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// 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.SchemaForField("spec").
|
||||||
|
SchemaForField("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.SchemaForField("spec").
|
||||||
|
SchemaForField("template").
|
||||||
|
SchemaForField("spec").
|
||||||
|
SchemaForField("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.SchemaForField("spec").
|
||||||
|
SchemaForField("template").
|
||||||
|
SchemaForField("spec").
|
||||||
|
SchemaForField("containers").SchemaForElements().
|
||||||
|
SchemaForField("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.SchemaForField("spec").
|
||||||
|
SchemaForField("template").
|
||||||
|
SchemaForField("spec").
|
||||||
|
SchemaForField("containers").SchemaForElements().
|
||||||
|
SchemaForField("ports").SchemaForElements().
|
||||||
|
SchemaForField("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.SchemaForField("metadata").SchemaForField("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]
|
||||||
|
}
|
||||||
178
kyaml/openapi/openapi.go
Normal file
178
kyaml/openapi/openapi.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// 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}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaForElements returns the Schema for the elements of an array.
|
||||||
|
func (r *ResourceSchema) SchemaForElements() *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}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaForField returns the Schema for a field.
|
||||||
|
func (r *ResourceSchema) SchemaForField(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
|
||||||
|
}
|
||||||
74
kyaml/openapi/openapi_test.go
Normal file
74
kyaml/openapi/openapi_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// 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.SchemaForField("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.SchemaForField("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.SchemaForField("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.SchemaForField("spec").
|
||||||
|
SchemaForField("containers").
|
||||||
|
SchemaForElements()
|
||||||
|
if !assert.NotNil(t, containers) {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPort := containers.SchemaForField("ports").
|
||||||
|
SchemaForElements().
|
||||||
|
SchemaForField("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.SchemaForField("args").
|
||||||
|
SchemaForElements()
|
||||||
|
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
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
20840
kyaml/openapi/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -216,6 +216,11 @@ func (m MapNodeSlice) Values() []*RNode {
|
|||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TypeMeta struct {
|
||||||
|
Kind string
|
||||||
|
APIVersion string
|
||||||
|
}
|
||||||
|
|
||||||
// ResourceMeta contains the metadata for a both Resource Type and Resource.
|
// ResourceMeta contains the metadata for a both Resource Type and Resource.
|
||||||
type ResourceMeta struct {
|
type ResourceMeta struct {
|
||||||
// APIVersion is the apiVersion field of a Resource
|
// APIVersion is the apiVersion field of a Resource
|
||||||
|
|||||||
Reference in New Issue
Block a user