Convert and construct OpenAPI v2 spec from CRD

validation OpenAPI v3 Schema
This commit is contained in:
Haowei Cai 2018-11-15 11:02:47 -08:00
parent 3222a7033c
commit e0d4c65b53
4 changed files with 1096 additions and 0 deletions

View File

@ -0,0 +1,371 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
import (
"fmt"
"strings"
"github.com/go-openapi/spec"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
)
// ResourceKind determines the scope of an API object: if it's the parent resource,
// scale subresource or status subresource.
type ResourceKind string
const (
// Resource specifies an object of custom resource kind
Resource ResourceKind = "Resource"
// Scale specifies an object of custom resource's scale subresource kind
Scale ResourceKind = "Scale"
// Status specifies an object of custom resource's status subresource kind
Status ResourceKind = "Status"
scaleSchemaRef = "#/definitions/io.k8s.api.autoscaling.v1.Scale"
statusSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Status"
listMetaSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta"
patchSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Patch"
)
// SwaggerConstructor takes in CRD OpenAPI schema and CustomResourceDefinitionSpec, and
// constructs the OpenAPI swagger that an apiserver serves.
type SwaggerConstructor struct {
// schema is the CRD's OpenAPI v2 schema
schema *spec.Schema
status, scale bool
group string
version string
kind string
listKind string
plural string
scope apiextensions.ResourceScope
}
// NewSwaggerConstructor creates a new SwaggerConstructor using the CRD OpenAPI schema
// and CustomResourceDefinitionSpec
func NewSwaggerConstructor(schema *spec.Schema, crdSpec *apiextensions.CustomResourceDefinitionSpec, version string) (*SwaggerConstructor, error) {
ret := &SwaggerConstructor{
schema: schema,
group: crdSpec.Group,
version: version,
kind: crdSpec.Names.Kind,
listKind: crdSpec.Names.ListKind,
plural: crdSpec.Names.Plural,
scope: crdSpec.Scope,
}
sub, err := getSubresourcesForVersion(crdSpec, version)
if err != nil {
return nil, err
}
if sub != nil {
ret.status = sub.Status != nil
ret.scale = sub.Scale != nil
}
return ret, nil
}
// ConstructCRDOpenAPISpec constructs the complete OpenAPI swagger (spec).
func (c *SwaggerConstructor) ConstructCRDOpenAPISpec() *spec.Swagger {
basePath := fmt.Sprintf("/apis/%s/%s/%s", c.group, c.version, c.plural)
if c.scope == apiextensions.NamespaceScoped {
basePath = fmt.Sprintf("/apis/%s/%s/namespaces/{namespace}/%s", c.group, c.version, c.plural)
}
model := fmt.Sprintf("%s.%s.%s", c.group, c.version, c.kind)
listModel := fmt.Sprintf("%s.%s.%s", c.group, c.version, c.listKind)
var schema spec.Schema
if c.schema != nil {
schema = *c.schema
}
ret := &spec.Swagger{
SwaggerProps: spec.SwaggerProps{
Paths: &spec.Paths{
Paths: map[string]spec.PathItem{
basePath: {
PathItemProps: spec.PathItemProps{
Get: c.listOperation(),
Post: c.createOperation(),
Delete: c.deleteCollectionOperation(),
Parameters: pathParameters(),
},
},
fmt.Sprintf("%s/{name}", basePath): {
PathItemProps: spec.PathItemProps{
Get: c.readOperation(Resource),
Put: c.replaceOperation(Resource),
Delete: c.deleteOperation(),
Patch: c.patchOperation(Resource),
Parameters: pathParameters(),
},
},
},
},
Definitions: spec.Definitions{
model: schema,
listModel: *c.listSchema(),
},
},
}
if c.status {
ret.SwaggerProps.Paths.Paths[fmt.Sprintf("%s/{name}/status", basePath)] = spec.PathItem{
PathItemProps: spec.PathItemProps{
Get: c.readOperation(Status),
Put: c.replaceOperation(Status),
Patch: c.patchOperation(Status),
Parameters: pathParameters(),
},
}
}
if c.scale {
ret.SwaggerProps.Paths.Paths[fmt.Sprintf("%s/{name}/scale", basePath)] = spec.PathItem{
PathItemProps: spec.PathItemProps{
Get: c.readOperation(Scale),
Put: c.replaceOperation(Scale),
Patch: c.patchOperation(Scale),
Parameters: pathParameters(),
},
}
// TODO(roycaihw): this is a hack to let apiExtension apiserver and generic kube-apiserver
// to have the same io.k8s.api.autoscaling.v1.Scale definition, so that aggregator server won't
// detect name conflict and create a duplicate io.k8s.api.autoscaling.v1.Scale_V2 schema
// when aggregating the openapi spec. It would be better if apiExtension apiserver serves
// identical definition through the same code path (using routes) as generic kube-apiserver.
ret.SwaggerProps.Definitions["io.k8s.api.autoscaling.v1.Scale"] = *scaleSchema()
ret.SwaggerProps.Definitions["io.k8s.api.autoscaling.v1.ScaleSpec"] = *scaleSpecSchema()
ret.SwaggerProps.Definitions["io.k8s.api.autoscaling.v1.ScaleStatus"] = *scaleStatusSchema()
}
return ret
}
// baseOperation initializes a base operation that all operations build upon
func (c *SwaggerConstructor) baseOperation(kind ResourceKind, action string) *spec.Operation {
op := spec.NewOperation(c.operationID(kind, action)).
WithConsumes(
"application/json",
"application/yaml",
).
WithProduces(
"application/json",
"application/yaml",
).
WithTags(fmt.Sprintf("%s_%s", c.group, c.version)).
RespondsWith(401, unauthorizedResponse())
op.Schemes = []string{"https"}
op.AddExtension("x-kubernetes-action", action)
// Add x-kubernetes-group-version-kind extension
// For CRD scale subresource, the x-kubernetes-group-version-kind is autoscaling.v1.Scale
switch kind {
case Scale:
op.AddExtension("x-kubernetes-group-version-kind", []map[string]string{
{
"group": "autoscaling",
"kind": "Scale",
"version": "v1",
},
})
default:
op.AddExtension("x-kubernetes-group-version-kind", []map[string]string{
{
"group": c.group,
"kind": c.kind,
"version": c.version,
},
})
}
return op
}
// listOperation constructs a list operation for a CRD
func (c *SwaggerConstructor) listOperation() *spec.Operation {
op := c.baseOperation(Resource, "list").
WithDescription(fmt.Sprintf("list or watch objects of kind %s", c.kind)).
RespondsWith(200, okResponse(fmt.Sprintf("#/definitions/%s.%s.%s", c.group, c.version, c.listKind)))
return addCollectionOperationParameters(op)
}
// createOperation constructs a create operation for a CRD
func (c *SwaggerConstructor) createOperation() *spec.Operation {
ref := c.constructSchemaRef(Resource)
return c.baseOperation(Resource, "create").
WithDescription(fmt.Sprintf("create a %s", c.kind)).
RespondsWith(200, okResponse(ref)).
RespondsWith(201, createdResponse(ref)).
RespondsWith(202, acceptedResponse(ref)).
AddParam((&spec.Parameter{ParamProps: spec.ParamProps{Schema: spec.RefSchema(ref)}}).
Named("body").
WithLocation("body").
AsRequired())
}
// deleteOperation constructs a delete operation for a CRD
func (c *SwaggerConstructor) deleteOperation() *spec.Operation {
op := c.baseOperation(Resource, "delete").
WithDescription(fmt.Sprintf("delete a %s", c.kind)).
RespondsWith(200, okResponse(statusSchemaRef)).
RespondsWith(202, acceptedResponse(statusSchemaRef))
return addDeleteOperationParameters(op)
}
// deleteCollectionOperation constructs a deletecollection operation for a CRD
func (c *SwaggerConstructor) deleteCollectionOperation() *spec.Operation {
op := c.baseOperation(Resource, "deletecollection").
WithDescription(fmt.Sprintf("delete collection of %s", c.kind))
return addCollectionOperationParameters(op)
}
// readOperation constructs a read operation for a CRD, CRD's scale subresource
// or CRD's status subresource
func (c *SwaggerConstructor) readOperation(kind ResourceKind) *spec.Operation {
ref := c.constructSchemaRef(kind)
action := "read"
return c.baseOperation(kind, action).
WithDescription(c.constructDescription(kind, action)).
RespondsWith(200, okResponse(ref))
}
// replaceOperation constructs a replace operation for a CRD, CRD's scale subresource
// or CRD's status subresource
func (c *SwaggerConstructor) replaceOperation(kind ResourceKind) *spec.Operation {
ref := c.constructSchemaRef(kind)
action := "replace"
return c.baseOperation(kind, action).
WithDescription(c.constructDescription(kind, action)).
RespondsWith(200, okResponse(ref)).
RespondsWith(201, createdResponse(ref)).
AddParam((&spec.Parameter{ParamProps: spec.ParamProps{Schema: spec.RefSchema(ref)}}).
Named("body").
WithLocation("body").
AsRequired())
}
// patchOperation constructs a patch operation for a CRD, CRD's scale subresource
// or CRD's status subresource
func (c *SwaggerConstructor) patchOperation(kind ResourceKind) *spec.Operation {
ref := c.constructSchemaRef(kind)
action := "patch"
return c.baseOperation(kind, action).
WithDescription(c.constructDescription(kind, "partially update")).
RespondsWith(200, okResponse(ref)).
AddParam((&spec.Parameter{ParamProps: spec.ParamProps{Schema: spec.RefSchema(patchSchemaRef)}}).
Named("body").
WithLocation("body").
AsRequired())
}
// listSchema constructs the OpenAPI schema for a list of CRD objects
func (c *SwaggerConstructor) listSchema() *spec.Schema {
ref := c.constructSchemaRef(Resource)
s := new(spec.Schema).
WithDescription(fmt.Sprintf("%s is a list of %s", c.listKind, c.kind)).
WithRequired("items").
SetProperty("apiVersion", *spec.StringProperty().
WithDescription(swaggerTypeMetaDescriptions["apiVersion"])).
SetProperty("items", *spec.ArrayProperty(spec.RefSchema(ref)).
WithDescription(fmt.Sprintf("List of %s. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md", c.plural))).
SetProperty("kind", *spec.StringProperty().
WithDescription(swaggerTypeMetaDescriptions["kind"])).
SetProperty("metadata", *spec.RefSchema(listMetaSchemaRef).
WithDescription(swaggerListDescriptions["metadata"]))
s.AddExtension("x-kubernetes-group-version-kind", map[string]string{
"group": c.group,
"kind": c.listKind,
"version": c.version,
})
return s
}
// operationID generates the ID for an operation
func (c *SwaggerConstructor) operationID(kind ResourceKind, action string) string {
var collectionTemplate, namespacedTemplate, subresourceTemplate string
if action == "deletecollection" {
action = "delete"
collectionTemplate = "Collection"
}
if c.scope == apiextensions.NamespaceScoped {
namespacedTemplate = "Namespaced"
}
switch kind {
case Status:
subresourceTemplate = "Status"
case Scale:
subresourceTemplate = "Scale"
}
return fmt.Sprintf("%s%s%s%s%s%s%s", action, strings.Title(c.group), strings.Title(c.version), collectionTemplate, namespacedTemplate, c.kind, subresourceTemplate)
}
// constructSchemaRef generates a reference to an object schema, based on the ResourceKind
// used by an operation
func (c *SwaggerConstructor) constructSchemaRef(kind ResourceKind) string {
var ref string
switch kind {
case Scale:
ref = scaleSchemaRef
default:
ref = fmt.Sprintf("#/definitions/%s.%s.%s", c.group, c.version, c.kind)
}
return ref
}
// constructDescription generates a description for READ, REPLACE and PATCH operations, based on
// the ResourceKind used by the operation
func (c *SwaggerConstructor) constructDescription(kind ResourceKind, action string) string {
var descriptionTemplate string
switch kind {
case Status:
descriptionTemplate = "status of "
case Scale:
descriptionTemplate = "scale of "
}
return fmt.Sprintf("%s %sthe specified %s", action, descriptionTemplate, c.kind)
}
// hasPerVersionSubresources returns true if a CRD spec uses per-version subresources.
func hasPerVersionSubresources(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if v.Subresources != nil {
return true
}
}
return false
}
// getSubresourcesForVersion returns the subresources for given version in given CRD spec.
func getSubresourcesForVersion(spec *apiextensions.CustomResourceDefinitionSpec, version string) (*apiextensions.CustomResourceSubresources, error) {
if !hasPerVersionSubresources(spec.Versions) {
return spec.Subresources, nil
}
if spec.Subresources != nil {
return nil, fmt.Errorf("malformed CustomResourceDefinitionSpec version %s: top-level and per-version subresources must be mutual exclusive", version)
}
for _, v := range spec.Versions {
if version == v.Name {
return v.Subresources, nil
}
}
return nil, fmt.Errorf("version %s not found in CustomResourceDefinitionSpec", version)
}

View File

@ -0,0 +1,49 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
import (
"encoding/json"
"github.com/go-openapi/spec"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
)
// ConvertJSONSchemaPropsToOpenAPIv2Schema converts our internal OpenAPI v3 schema
// (*apiextensions.JSONSchemaProps) to an OpenAPI v2 schema (*spec.Schema).
// NOTE: we use versioned type (v1beta1) here so that we can properly marshal the object
// using the JSON tags
func ConvertJSONSchemaPropsToOpenAPIv2Schema(in *v1beta1.JSONSchemaProps) (*spec.Schema, error) {
if in == nil {
return nil, nil
}
// Marshal JSONSchemaProps into JSON and unmarshal the data into spec.Schema
data, err := json.Marshal(*in)
if err != nil {
return nil, err
}
out := new(spec.Schema)
if err := out.UnmarshalJSON(data); err != nil {
return nil, err
}
// Remove unsupported fields in OpenAPI v2
out.OneOf = nil
out.AnyOf = nil
out.Not = nil
return out, nil
}

View File

@ -0,0 +1,448 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
import (
"encoding/json"
"reflect"
"testing"
"github.com/go-openapi/spec"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
)
func Test_ConvertJSONSchemaPropsToOpenAPIv2Schema(t *testing.T) {
testStr := "test"
testStr2 := "test2"
testFloat64 := float64(6.4)
testInt64 := int64(64)
raw, _ := json.Marshal(testStr)
raw2, _ := json.Marshal(testStr2)
testApiextensionsJSON := v1beta1.JSON{Raw: raw}
tests := []struct {
name string
in *v1beta1.JSONSchemaProps
expected *spec.Schema
}{
{
name: "id",
in: &v1beta1.JSONSchemaProps{
ID: testStr,
},
expected: new(spec.Schema).
WithID(testStr),
},
{
name: "$schema",
in: &v1beta1.JSONSchemaProps{
Schema: "test",
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Schema: "test",
},
},
},
{
name: "$ref",
in: &v1beta1.JSONSchemaProps{
Ref: &testStr,
},
expected: spec.RefSchema(testStr),
},
{
name: "description",
in: &v1beta1.JSONSchemaProps{
Description: testStr,
},
expected: new(spec.Schema).
WithDescription(testStr),
},
{
name: "type and format",
in: &v1beta1.JSONSchemaProps{
Type: testStr,
Format: testStr2,
},
expected: new(spec.Schema).
Typed(testStr, testStr2),
},
{
name: "title",
in: &v1beta1.JSONSchemaProps{
Title: testStr,
},
expected: new(spec.Schema).
WithTitle(testStr),
},
{
name: "default",
in: &v1beta1.JSONSchemaProps{
Default: &testApiextensionsJSON,
},
expected: new(spec.Schema).
WithDefault(testStr),
},
{
name: "maximum and exclusiveMaximum",
in: &v1beta1.JSONSchemaProps{
Maximum: &testFloat64,
ExclusiveMaximum: true,
},
expected: new(spec.Schema).
WithMaximum(testFloat64, true),
},
{
name: "minimum and exclusiveMinimum",
in: &v1beta1.JSONSchemaProps{
Minimum: &testFloat64,
ExclusiveMinimum: true,
},
expected: new(spec.Schema).
WithMinimum(testFloat64, true),
},
{
name: "maxLength",
in: &v1beta1.JSONSchemaProps{
MaxLength: &testInt64,
},
expected: new(spec.Schema).
WithMaxLength(testInt64),
},
{
name: "minLength",
in: &v1beta1.JSONSchemaProps{
MinLength: &testInt64,
},
expected: new(spec.Schema).
WithMinLength(testInt64),
},
{
name: "pattern",
in: &v1beta1.JSONSchemaProps{
Pattern: testStr,
},
expected: new(spec.Schema).
WithPattern(testStr),
},
{
name: "maxItems",
in: &v1beta1.JSONSchemaProps{
MaxItems: &testInt64,
},
expected: new(spec.Schema).
WithMaxItems(testInt64),
},
{
name: "minItems",
in: &v1beta1.JSONSchemaProps{
MinItems: &testInt64,
},
expected: new(spec.Schema).
WithMinItems(testInt64),
},
{
name: "uniqueItems",
in: &v1beta1.JSONSchemaProps{
UniqueItems: true,
},
expected: new(spec.Schema).
UniqueValues(),
},
{
name: "multipleOf",
in: &v1beta1.JSONSchemaProps{
MultipleOf: &testFloat64,
},
expected: new(spec.Schema).
WithMultipleOf(testFloat64),
},
{
name: "enum",
in: &v1beta1.JSONSchemaProps{
Enum: []v1beta1.JSON{{Raw: raw}, {Raw: raw2}},
},
expected: new(spec.Schema).
WithEnum(testStr, testStr2),
},
{
name: "maxProperties",
in: &v1beta1.JSONSchemaProps{
MaxProperties: &testInt64,
},
expected: new(spec.Schema).
WithMaxProperties(testInt64),
},
{
name: "minProperties",
in: &v1beta1.JSONSchemaProps{
MinProperties: &testInt64,
},
expected: new(spec.Schema).
WithMinProperties(testInt64),
},
{
name: "required",
in: &v1beta1.JSONSchemaProps{
Required: []string{testStr, testStr2},
},
expected: new(spec.Schema).
WithRequired(testStr, testStr2),
},
{
name: "items single props",
in: &v1beta1.JSONSchemaProps{
Items: &v1beta1.JSONSchemaPropsOrArray{
Schema: &v1beta1.JSONSchemaProps{
Type: "boolean",
},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Items: &spec.SchemaOrArray{
Schema: spec.BooleanProperty(),
},
},
},
},
{
name: "items array props",
in: &v1beta1.JSONSchemaProps{
Items: &v1beta1.JSONSchemaPropsOrArray{
JSONSchemas: []v1beta1.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Items: &spec.SchemaOrArray{
Schemas: []spec.Schema{
*spec.BooleanProperty(),
*spec.StringProperty(),
},
},
},
},
},
{
name: "allOf",
in: &v1beta1.JSONSchemaProps{
AllOf: []v1beta1.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
expected: new(spec.Schema).
WithAllOf(*spec.BooleanProperty(), *spec.StringProperty()),
},
{
name: "oneOf",
in: &v1beta1.JSONSchemaProps{
OneOf: []v1beta1.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
expected: new(spec.Schema),
// expected: &spec.Schema{
// SchemaProps: spec.SchemaProps{
// OneOf: []spec.Schema{
// *spec.BooleanProperty(),
// *spec.StringProperty(),
// },
// },
// },
},
{
name: "anyOf",
in: &v1beta1.JSONSchemaProps{
AnyOf: []v1beta1.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
expected: new(spec.Schema),
// expected: &spec.Schema{
// SchemaProps: spec.SchemaProps{
// AnyOf: []spec.Schema{
// *spec.BooleanProperty(),
// *spec.StringProperty(),
// },
// },
// },
},
{
name: "not",
in: &v1beta1.JSONSchemaProps{
Not: &v1beta1.JSONSchemaProps{
Type: "boolean",
},
},
expected: new(spec.Schema),
// expected: &spec.Schema{
// SchemaProps: spec.SchemaProps{
// Not: spec.BooleanProperty(),
// },
// },
},
{
name: "properties",
in: &v1beta1.JSONSchemaProps{
Properties: map[string]v1beta1.JSONSchemaProps{
testStr: {Type: "boolean"},
},
},
expected: new(spec.Schema).
SetProperty(testStr, *spec.BooleanProperty()),
},
{
name: "additionalProperties",
in: &v1beta1.JSONSchemaProps{
AdditionalProperties: &v1beta1.JSONSchemaPropsOrBool{
Allows: true,
Schema: &v1beta1.JSONSchemaProps{Type: "boolean"},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: spec.BooleanProperty(),
},
},
},
},
{
name: "patternProperties",
in: &v1beta1.JSONSchemaProps{
PatternProperties: map[string]v1beta1.JSONSchemaProps{
testStr: {Type: "boolean"},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
PatternProperties: map[string]spec.Schema{
testStr: *spec.BooleanProperty(),
},
},
},
},
{
name: "dependencies schema",
in: &v1beta1.JSONSchemaProps{
Dependencies: v1beta1.JSONSchemaDependencies{
testStr: v1beta1.JSONSchemaPropsOrStringArray{
Schema: &v1beta1.JSONSchemaProps{Type: "boolean"},
},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Dependencies: spec.Dependencies{
testStr: spec.SchemaOrStringArray{
Schema: spec.BooleanProperty(),
},
},
},
},
},
{
name: "dependencies string array",
in: &v1beta1.JSONSchemaProps{
Dependencies: v1beta1.JSONSchemaDependencies{
testStr: v1beta1.JSONSchemaPropsOrStringArray{
Property: []string{testStr2},
},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Dependencies: spec.Dependencies{
testStr: spec.SchemaOrStringArray{
Property: []string{testStr2},
},
},
},
},
},
{
name: "additionalItems",
in: &v1beta1.JSONSchemaProps{
AdditionalItems: &v1beta1.JSONSchemaPropsOrBool{
Allows: true,
Schema: &v1beta1.JSONSchemaProps{Type: "boolean"},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
AdditionalItems: &spec.SchemaOrBool{
Allows: true,
Schema: spec.BooleanProperty(),
},
},
},
},
{
name: "definitions",
in: &v1beta1.JSONSchemaProps{
Definitions: v1beta1.JSONSchemaDefinitions{
testStr: v1beta1.JSONSchemaProps{Type: "boolean"},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Definitions: spec.Definitions{
testStr: *spec.BooleanProperty(),
},
},
},
},
{
name: "externalDocs",
in: &v1beta1.JSONSchemaProps{
ExternalDocs: &v1beta1.ExternalDocumentation{
Description: testStr,
URL: testStr2,
},
},
expected: new(spec.Schema).
WithExternalDocs(testStr, testStr2),
},
{
name: "example",
in: &v1beta1.JSONSchemaProps{
Example: &testApiextensionsJSON,
},
expected: new(spec.Schema).
WithExample(testStr),
},
}
for _, test := range tests {
out, err := ConvertJSONSchemaPropsToOpenAPIv2Schema(test.in)
if err != nil {
t.Errorf("unexpected error in converting openapi schema: %v", test.name)
}
if !reflect.DeepEqual(out, test.expected) {
t.Errorf("result of conversion test '%v' didn't match, want: %v; got: %v", test.name, *test.expected, *out)
}
}
}

View File

@ -0,0 +1,228 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
import (
"crypto/sha512"
"encoding/json"
"fmt"
"github.com/go-openapi/spec"
autoscalingv1 "k8s.io/api/autoscaling/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
deleteOptionsSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions"
objectMetaSchemaRef = "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
scaleSpecSchemaRef = "#/definitions/io.k8s.api.autoscaling.v1.ScaleSpec"
scaleStatusSchemaRef = "#/definitions/io.k8s.api.autoscaling.v1.ScaleStatus"
)
var swaggerTypeMetaDescriptions = metav1.TypeMeta{}.SwaggerDoc()
var swaggerDeleteOptionsDescriptions = metav1.DeleteOptions{}.SwaggerDoc()
var swaggerListDescriptions = metav1.List{}.SwaggerDoc()
var swaggerListOptionsDescriptions = metav1.ListOptions{}.SwaggerDoc()
var swaggerScaleDescriptions = autoscalingv1.Scale{}.SwaggerDoc()
var swaggerScaleSpecDescriptions = autoscalingv1.ScaleSpec{}.SwaggerDoc()
var swaggerScaleStatusDescriptions = autoscalingv1.ScaleStatus{}.SwaggerDoc()
// calcSwaggerEtag calculates an etag of the OpenAPI swagger (spec)
func calcSwaggerEtag(openAPISpec *spec.Swagger) (string, error) {
specBytes, err := json.MarshalIndent(openAPISpec, " ", " ")
if err != nil {
return "", err
}
return fmt.Sprintf("\"%X\"", sha512.Sum512(specBytes)), nil
}
// pathParameters constructs the Parameter used by all paths in the CRD swagger (spec)
func pathParameters() []spec.Parameter {
return []spec.Parameter{
*spec.QueryParam("pretty").
Typed("string", "").
UniqueValues().
WithDescription("If 'true', then the output is pretty printed."),
}
}
// addDeleteOperationParameters add the body&query parameters used by a delete operation
func addDeleteOperationParameters(op *spec.Operation) *spec.Operation {
return op.
AddParam((&spec.Parameter{ParamProps: spec.ParamProps{Schema: spec.RefSchema(deleteOptionsSchemaRef)}}).
Named("body").
WithLocation("body").
AsRequired()).
AddParam(spec.QueryParam("dryRun").
Typed("string", "").
UniqueValues().
WithDescription(swaggerDeleteOptionsDescriptions["dryRun"])).
AddParam(spec.QueryParam("gracePeriodSeconds").
Typed("integer", "").
UniqueValues().
WithDescription(swaggerDeleteOptionsDescriptions["gracePeriodSeconds"])).
AddParam(spec.QueryParam("orphanDependents").
Typed("boolean", "").
UniqueValues().
WithDescription(swaggerDeleteOptionsDescriptions["orphanDependents"])).
AddParam(spec.QueryParam("propagationPolicy").
Typed("string", "").
UniqueValues().
WithDescription(swaggerDeleteOptionsDescriptions["propagationPolicy"]))
}
// addCollectionOperationParameters adds the query parameters used by list and deletecollection
// operations
func addCollectionOperationParameters(op *spec.Operation) *spec.Operation {
return op.
AddParam(spec.QueryParam("continue").
Typed("string", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["continue"])).
AddParam(spec.QueryParam("fieldSelector").
Typed("string", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["fieldSelector"])).
AddParam(spec.QueryParam("includeUninitialized").
Typed("boolean", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["includeUninitialized"])).
AddParam(spec.QueryParam("labelSelector").
Typed("string", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["labelSelector"])).
AddParam(spec.QueryParam("limit").
Typed("integer", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["limit"])).
AddParam(spec.QueryParam("resourceVersion").
Typed("string", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["resourceVersion"])).
AddParam(spec.QueryParam("timeoutSeconds").
Typed("integer", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["timeoutSeconds"])).
AddParam(spec.QueryParam("watch").
Typed("boolean", "").
UniqueValues().
WithDescription(swaggerListOptionsDescriptions["watch"]))
}
// okResponse constructs a 200 OK response with the input object schema reference
func okResponse(ref string) *spec.Response {
return spec.NewResponse().
WithDescription("OK").
WithSchema(spec.RefSchema(ref))
}
// createdResponse constructs a 201 Created response with the input object schema reference
func createdResponse(ref string) *spec.Response {
return spec.NewResponse().
WithDescription("Created").
WithSchema(spec.RefSchema(ref))
}
// acceptedResponse constructs a 202 Accepted response with the input object schema reference
func acceptedResponse(ref string) *spec.Response {
return spec.NewResponse().
WithDescription("Accepted").
WithSchema(spec.RefSchema(ref))
}
// unauthorizedResponse constructs a 401 Unauthorized response
func unauthorizedResponse() *spec.Response {
return spec.NewResponse().
WithDescription("Unauthorized")
}
// scaleSchema constructs the OpenAPI schema for io.k8s.api.autoscaling.v1.Scale objects
// TODO(roycaihw): this is a hack to let apiExtension apiserver and generic kube-apiserver
// to have the same io.k8s.api.autoscaling.v1.Scale definition, so that aggregator server won't
// detect name conflict and create a duplicate io.k8s.api.autoscaling.v1.Scale_V2 schema
// when aggregating the openapi spec. It would be better if apiExtension apiserver serves
// identical definition through the same code path (using routes) as generic kube-apiserver.
func scaleSchema() *spec.Schema {
s := new(spec.Schema).
WithDescription(swaggerScaleDescriptions[""]).
SetProperty("apiVersion", *spec.StringProperty().
WithDescription(swaggerTypeMetaDescriptions["apiVersion"])).
SetProperty("kind", *spec.StringProperty().
WithDescription(swaggerTypeMetaDescriptions["kind"])).
SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef).
WithDescription(swaggerScaleDescriptions["metadata"])).
SetProperty("spec", *spec.RefSchema(scaleSpecSchemaRef).
WithDescription(swaggerScaleDescriptions["spec"])).
SetProperty("status", *spec.RefSchema(scaleStatusSchemaRef).
WithDescription(swaggerScaleDescriptions["status"]))
s.AddExtension("x-kubernetes-group-version-kind", []map[string]string{
{
"group": "autoscaling",
"kind": "Scale",
"version": "v1",
},
})
return s
}
// scaleSchema constructs the OpenAPI schema for io.k8s.api.autoscaling.v1.ScaleSpec objects
func scaleSpecSchema() *spec.Schema {
return new(spec.Schema).
WithDescription(swaggerScaleSpecDescriptions[""]).
SetProperty("replicas", *spec.Int32Property().
WithDescription(swaggerScaleSpecDescriptions["replicas"]))
}
// scaleSchema constructs the OpenAPI schema for io.k8s.api.autoscaling.v1.ScaleStatus objects
func scaleStatusSchema() *spec.Schema {
return new(spec.Schema).
WithDescription(swaggerScaleStatusDescriptions[""]).
WithRequired("replicas").
SetProperty("replicas", *spec.Int32Property().
WithDescription(swaggerScaleStatusDescriptions["replicas"])).
SetProperty("selector", *spec.StringProperty().
WithDescription(swaggerScaleStatusDescriptions["selector"]))
}
// CustomResourceDefinitionOpenAPISpec constructs the OpenAPI spec (swagger) and calculates
// etag for a given CustomResourceDefinitionSpec.
// NOTE: in apiserver we general operates on internal types. We are using versioned (v1beta1)
// validation schema here because we need the json tags to properly marshal the object to
// JSON.
func CustomResourceDefinitionOpenAPISpec(crdSpec *apiextensions.CustomResourceDefinitionSpec, version string, validationSchema *v1beta1.CustomResourceValidation) (*spec.Swagger, string, error) {
schema := &spec.Schema{}
if validationSchema != nil && validationSchema.OpenAPIV3Schema != nil {
var err error
schema, err = ConvertJSONSchemaPropsToOpenAPIv2Schema(validationSchema.OpenAPIV3Schema)
if err != nil {
return nil, "", err
}
}
crdSwaggerConstructor, err := NewSwaggerConstructor(schema, crdSpec, version)
if err != nil {
return nil, "", err
}
crdOpenAPISpec := crdSwaggerConstructor.ConstructCRDOpenAPISpec()
etag, err := calcSwaggerEtag(crdOpenAPISpec)
if err != nil {
return nil, "", err
}
return crdOpenAPISpec, etag, nil
}