Merge pull request #63893 from jennybuckley/expose-openapi-to-registry3

Automatic merge from submit-queue (batch tested with PRs 64175, 63893). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Expose openapi schema to handlers

**What this PR does / why we need it**:
Build an openapi spec for each api resource handler. This spec will be able to be consumed by server-side apply and server-side openapi validation.
The reason for putting it into master is so we can work on implementing server side validation against the openapi spec as well as server side apply, and it will make merging the server side apply feature branch a smaller, less risky PR

/sig api-machinery
/kind feature
cc @liggitt @lavalamp @seans3 @mbohlool @apelisse

**Release note**:
```release-note
NONE
```

Kubernetes-commit: 28f171bd66937dec8b24a05c4b7a1414432f9fe8
This commit is contained in:
Kubernetes Publisher 2018-05-25 03:50:14 -07:00
commit 7b21116d89
12 changed files with 334 additions and 321 deletions

450
Godeps/Godeps.json generated

File diff suppressed because it is too large Load Diff

View File

@ -450,15 +450,12 @@ message ConfigMapNodeConfigSource {
optional string name = 2;
// UID is the metadata.UID of the referenced ConfigMap.
// This field is currently reqired in Node.Spec.
// TODO(#61643): This field will be forbidden in Node.Spec when #61643 is resolved.
// TODO(#56896): This field will be required in Node.Status when #56896 is resolved.
// This field is forbidden in Node.Spec, and required in Node.Status.
// +optional
optional string uid = 3;
// ResourceVersion is the metadata.ResourceVersion of the referenced ConfigMap.
// This field is forbidden in Node.Spec.
// TODO(#56896): This field will be required in Node.Status when #56896 is resolved.
// This field is forbidden in Node.Spec, and required in Node.Status.
// +optional
optional string resourceVersion = 4;

7
vendor/k8s.io/api/core/v1/types.go generated vendored
View File

@ -3666,15 +3666,12 @@ type ConfigMapNodeConfigSource struct {
Name string `json:"name" protobuf:"bytes,2,opt,name=name"`
// UID is the metadata.UID of the referenced ConfigMap.
// This field is currently reqired in Node.Spec.
// TODO(#61643): This field will be forbidden in Node.Spec when #61643 is resolved.
// TODO(#56896): This field will be required in Node.Status when #56896 is resolved.
// This field is forbidden in Node.Spec, and required in Node.Status.
// +optional
UID types.UID `json:"uid,omitempty" protobuf:"bytes,3,opt,name=uid"`
// ResourceVersion is the metadata.ResourceVersion of the referenced ConfigMap.
// This field is forbidden in Node.Spec.
// TODO(#56896): This field will be required in Node.Status when #56896 is resolved.
// This field is forbidden in Node.Spec, and required in Node.Status.
// +optional
ResourceVersion string `json:"resourceVersion,omitempty" protobuf:"bytes,4,opt,name=resourceVersion"`

View File

@ -266,8 +266,8 @@ var map_ConfigMapNodeConfigSource = map[string]string{
"": "ConfigMapNodeConfigSource contains the information to reference a ConfigMap as a config source for the Node.",
"namespace": "Namespace is the metadata.namespace of the referenced ConfigMap. This field is required in all cases.",
"name": "Name is the metadata.name of the referenced ConfigMap. This field is required in all cases.",
"uid": "UID is the metadata.UID of the referenced ConfigMap. This field is currently reqired in Node.Spec.",
"resourceVersion": "ResourceVersion is the metadata.ResourceVersion of the referenced ConfigMap. This field is forbidden in Node.Spec.",
"uid": "UID is the metadata.UID of the referenced ConfigMap. This field is forbidden in Node.Spec, and required in Node.Status.",
"resourceVersion": "ResourceVersion is the metadata.ResourceVersion of the referenced ConfigMap. This field is forbidden in Node.Spec, and required in Node.Status.",
"kubeletConfigKey": "KubeletConfigKey declares which key of the referenced ConfigMap corresponds to the KubeletConfiguration structure This field is required in all cases.",
}

View File

@ -49,6 +49,7 @@ message APIGroup {
// The server returns only those CIDRs that it thinks that the client can match.
// For example: the master will return an internal IP CIDR only, if the client reaches the server using an internal IP.
// Server looks at X-Forwarded-For header or X-Real-Ip header or request.RemoteAddr (in that order) to get the client IP.
// +optional
repeated ServerAddressByClientCIDR serverAddressByClientCIDRs = 4;
}

View File

@ -799,7 +799,8 @@ type APIGroup struct {
// The server returns only those CIDRs that it thinks that the client can match.
// For example: the master will return an internal IP CIDR only, if the client reaches the server using an internal IP.
// Server looks at X-Forwarded-For header or X-Real-Ip header or request.RemoteAddr (in that order) to get the client IP.
ServerAddressByClientCIDRs []ServerAddressByClientCIDR `json:"serverAddressByClientCIDRs" protobuf:"bytes,4,rep,name=serverAddressByClientCIDRs"`
// +optional
ServerAddressByClientCIDRs []ServerAddressByClientCIDR `json:"serverAddressByClientCIDRs,omitempty" protobuf:"bytes,4,rep,name=serverAddressByClientCIDRs"`
}
// ServerAddressByClientCIDR helps the client to determine the server address that they should use, depending on the clientCIDR that they match.

View File

@ -18,7 +18,6 @@ package unstructured
import (
gojson "encoding/json"
"errors"
"fmt"
"io"
"strings"
@ -450,44 +449,3 @@ func (c JSONFallbackEncoder) Encode(obj runtime.Object, w io.Writer) error {
}
return err
}
// UnstructuredObjectConverter is an ObjectConverter for use with
// Unstructured objects. Since it has no schema or type information,
// it will only succeed for no-op conversions. This is provided as a
// sane implementation for APIs that require an object converter.
type UnstructuredObjectConverter struct{}
func (UnstructuredObjectConverter) Convert(in, out, context interface{}) error {
unstructIn, ok := in.(*Unstructured)
if !ok {
return fmt.Errorf("input type %T in not valid for unstructured conversion", in)
}
unstructOut, ok := out.(*Unstructured)
if !ok {
return fmt.Errorf("output type %T in not valid for unstructured conversion", out)
}
// maybe deep copy the map? It is documented in the
// ObjectConverter interface that this function is not
// guaranteed to not mutate the input. Or maybe set the input
// object to nil.
unstructOut.Object = unstructIn.Object
return nil
}
func (UnstructuredObjectConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
if kind := in.GetObjectKind().GroupVersionKind(); !kind.Empty() {
gvk, ok := target.KindForGroupVersionKinds([]schema.GroupVersionKind{kind})
if !ok {
// TODO: should this be a typed error?
return nil, fmt.Errorf("%v is unstructured and is not suitable for converting to %q", kind, target)
}
in.GetObjectKind().SetGroupVersionKind(gvk)
}
return in, nil
}
func (UnstructuredObjectConverter) ConvertFieldLabel(version, kind, label, value string) (string, string, error) {
return "", "", errors.New("unstructured cannot convert field labels")
}

View File

@ -122,7 +122,7 @@ func (s *Scheme) nameFunc(t reflect.Type) string {
for _, gvk := range gvks {
internalGV := gvk.GroupVersion()
internalGV.Version = "__internal" // this is hacky and maybe should be passed in
internalGV.Version = APIVersionInternal // this is hacky and maybe should be passed in
internalGVK := internalGV.WithKind(gvk.Kind)
if internalType, exists := s.gvkToType[internalGVK]; exists {

View File

@ -19,6 +19,7 @@ package versioning
import (
"io"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
@ -170,17 +171,22 @@ func (c *codec) Encode(obj runtime.Object, w io.Writer) error {
case *runtime.Unknown:
return c.encoder.Encode(obj, w)
case runtime.Unstructured:
// avoid conversion roundtrip if GVK is the right one already or is empty (yes, this is a hack, but the old behaviour we rely on in kubectl)
objGVK := obj.GetObjectKind().GroupVersionKind()
if len(objGVK.Version) == 0 {
return c.encoder.Encode(obj, w)
}
targetGVK, ok := c.encodeVersion.KindForGroupVersionKinds([]schema.GroupVersionKind{objGVK})
if !ok {
return runtime.NewNotRegisteredGVKErrForTarget(objGVK, c.encodeVersion)
}
if targetGVK == objGVK {
return c.encoder.Encode(obj, w)
// An unstructured list can contain objects of multiple group version kinds. don't short-circuit just
// because the top-level type matches our desired destination type. actually send the object to the converter
// to give it a chance to convert the list items if needed.
if _, ok := obj.(*unstructured.UnstructuredList); !ok {
// avoid conversion roundtrip if GVK is the right one already or is empty (yes, this is a hack, but the old behaviour we rely on in kubectl)
objGVK := obj.GetObjectKind().GroupVersionKind()
if len(objGVK.Version) == 0 {
return c.encoder.Encode(obj, w)
}
targetGVK, ok := c.encodeVersion.KindForGroupVersionKinds([]schema.GroupVersionKind{objGVK})
if !ok {
return runtime.NewNotRegisteredGVKErrForTarget(objGVK, c.encodeVersion)
}
if targetGVK == objGVK {
return c.encoder.Encode(obj, w)
}
}
}

View File

@ -260,11 +260,11 @@
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/common",
"Rev": "61db125d227fc9d4e373819a059516f32f7f23c7"
"Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/generators",
"Rev": "61db125d227fc9d4e373819a059516f32f7f23c7"
"Rev": "86e28c192d2743f0232b9bc5f0a531568ef9f2a5"
}
]
}

View File

@ -27,18 +27,33 @@ import (
const extensionPrefix = "x-kubernetes-"
// Extension tag to openapi extension
var tagToExtension = map[string]string{
"patchMergeKey": "x-kubernetes-patch-merge-key",
"patchStrategy": "x-kubernetes-patch-strategy",
"listType": "x-kubernetes-list-type",
"listMapKey": "x-kubernetes-list-map-keys",
// extensionAttributes encapsulates common traits for particular extensions.
type extensionAttributes struct {
xName string
kind types.Kind
allowedValues sets.String
}
// Enum values per extension
var allowedExtensionValues = map[string]sets.String{
"x-kubernetes-patch-strategy": sets.NewString("merge", "retainKeys"),
"x-kubernetes-list-type": sets.NewString("atomic", "set", "map"),
// Extension tag to openapi extension attributes
var tagToExtension = map[string]extensionAttributes{
"patchMergeKey": extensionAttributes{
xName: "x-kubernetes-patch-merge-key",
kind: types.Slice,
},
"patchStrategy": extensionAttributes{
xName: "x-kubernetes-patch-strategy",
kind: types.Slice,
allowedValues: sets.NewString("merge", "retainKeys"),
},
"listMapKey": extensionAttributes{
xName: "x-kubernetes-list-map-keys",
kind: types.Slice,
},
"listType": extensionAttributes{
xName: "x-kubernetes-list-type",
kind: types.Slice,
allowedValues: sets.NewString("atomic", "set", "map"),
},
}
// Extension encapsulates information necessary to generate an OpenAPI extension.
@ -48,10 +63,25 @@ type extension struct {
values []string // Example: [atomic]
}
func (e extension) hasAllowedValues() bool {
return tagToExtension[e.idlTag].allowedValues.Len() > 0
}
func (e extension) allowedValues() sets.String {
return tagToExtension[e.idlTag].allowedValues
}
func (e extension) hasKind() bool {
return len(tagToExtension[e.idlTag].kind) > 0
}
func (e extension) kind() types.Kind {
return tagToExtension[e.idlTag].kind
}
func (e extension) validateAllowedValues() error {
// allowedValues not set means no restrictions on values.
allowedValues, exists := allowedExtensionValues[e.xName]
if !exists {
if !e.hasAllowedValues() {
return nil
}
// Check for missing value.
@ -59,6 +89,7 @@ func (e extension) validateAllowedValues() error {
return fmt.Errorf("%s needs a value, none given.", e.idlTag)
}
// For each extension value, validate that it is allowed.
allowedValues := e.allowedValues()
if !allowedValues.HasAll(e.values...) {
return fmt.Errorf("%v not allowed for %s. Allowed values: %v",
e.values, e.idlTag, allowedValues.List())
@ -66,6 +97,18 @@ func (e extension) validateAllowedValues() error {
return nil
}
func (e extension) validateType(kind types.Kind) error {
// If this extension class has no kind, then don't validate the type.
if !e.hasKind() {
return nil
}
if kind != e.kind() {
return fmt.Errorf("tag %s on type %v; only allowed on type %v",
e.idlTag, kind, e.kind())
}
return nil
}
func (e extension) hasMultipleValues() bool {
return len(e.values) > 1
}
@ -82,7 +125,9 @@ func sortedMapKeys(m map[string][]string) []string {
return keys
}
// Parses comments to return openapi extensions.
// Parses comments to return openapi extensions. Returns a list of
// extensions which parsed correctly, as well as a list of the
// parse errors. Validating extensions is performed separately.
// NOTE: Non-empty errors does not mean extensions is empty.
func parseExtensions(comments []string) ([]extension, []error) {
extensions := []extension{}
@ -108,21 +153,30 @@ func parseExtensions(comments []string) ([]extension, []error) {
// Next, generate extensions from "idlTags" (e.g. +listType)
tagValues := types.ExtractCommentTags("+", comments)
for _, idlTag := range sortedMapKeys(tagValues) {
xName, exists := tagToExtension[idlTag]
xAttrs, exists := tagToExtension[idlTag]
if !exists {
continue
}
values := tagValues[idlTag]
e := extension{
idlTag: idlTag, // listType
xName: xName, // x-kubernetes-list-type
values: values, // [atomic]
}
if err := e.validateAllowedValues(); err != nil {
// For now, only log the extension validation errors.
errors = append(errors, err)
idlTag: idlTag, // listType
xName: xAttrs.xName, // x-kubernetes-list-type
values: values, // [atomic]
}
extensions = append(extensions, e)
}
return extensions, errors
}
func validateMemberExtensions(extensions []extension, m *types.Member) []error {
errors := []error{}
for _, e := range extensions {
if err := e.validateAllowedValues(); err != nil {
errors = append(errors, err)
}
if err := e.validateType(m.Type.Kind); err != nil {
errors = append(errors, err)
}
}
return errors
}

View File

@ -432,7 +432,7 @@ func (g openAPITypeWriter) generateStructExtensions(t *types.Type) error {
extensions, errors := parseExtensions(t.CommentLines)
// Initially, we will only log struct extension errors.
if len(errors) > 0 {
for e := range errors {
for _, e := range errors {
glog.V(2).Infof("[%s]: %s\n", t.String(), e)
}
}
@ -442,17 +442,16 @@ func (g openAPITypeWriter) generateStructExtensions(t *types.Type) error {
}
func (g openAPITypeWriter) generateMemberExtensions(m *types.Member, parent *types.Type) error {
extensions, errors := parseExtensions(m.CommentLines)
extensions, parseErrors := parseExtensions(m.CommentLines)
validationErrors := validateMemberExtensions(extensions, m)
errors := append(parseErrors, validationErrors...)
// Initially, we will only log member extension errors.
if len(errors) > 0 {
errorPrefix := fmt.Sprintf("[%s] %s:", parent.String(), m.String())
for e := range errors {
for _, e := range errors {
glog.V(2).Infof("%s %s\n", errorPrefix, e)
}
}
// TODO(seans3): Validate member extensions here.
// Example: listType extension is only on a Slice.
// Example: cross-extension validation - listMapKey only makes sense with listType=map
g.emitExtensions(extensions)
return nil
}