Files
kustomize/kyaml/openapi/openapi.go
Phillip Wittrock 8a2c886ab2 update kyaml go.mod and go.sum
also update cmd/config,cmd/kubectl,cmd/resource
2020-01-09 08:50:11 -08:00

201 lines
5.5 KiB
Go

// 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
}