Merge pull request #114439 from alexzielenski/apiserver/smd/conversion-smaller

add direct construction of TypeConverter from OpenAPI
This commit is contained in:
Kubernetes Prow Robot 2023-02-01 14:51:35 -08:00 committed by GitHub
commit d475085776
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 254 additions and 32 deletions

View File

@ -493,7 +493,9 @@ func addTypeMetaProperties(s *spec.Schema, v2 bool) {
func (b *builder) buildListSchema(v2 bool) *spec.Schema { func (b *builder) buildListSchema(v2 bool) *spec.Schema {
name := definitionPrefix + util.ToRESTFriendlyName(fmt.Sprintf("%s/%s/%s", b.group, b.version, b.kind)) name := definitionPrefix + util.ToRESTFriendlyName(fmt.Sprintf("%s/%s/%s", b.group, b.version, b.kind))
doc := fmt.Sprintf("List of %s. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md", b.plural) doc := fmt.Sprintf("List of %s. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md", b.plural)
s := new(spec.Schema).WithDescription(fmt.Sprintf("%s is a list of %s", b.listKind, b.kind)). s := new(spec.Schema).
Typed("object", "").
WithDescription(fmt.Sprintf("%s is a list of %s", b.listKind, b.kind)).
WithRequired("items"). WithRequired("items").
SetProperty("items", *spec.ArrayProperty(spec.RefSchema(refForOpenAPIVersion(name, v2))).WithDescription(doc)). SetProperty("items", *spec.ArrayProperty(spec.RefSchema(refForOpenAPIVersion(name, v2))).WithDescription(doc)).
SetProperty("metadata", *spec.RefSchema(refForOpenAPIVersion(listMetaSchemaRef, v2)).WithDescription(swaggerPartialObjectMetadataListDescriptions["metadata"])) SetProperty("metadata", *spec.RefSchema(refForOpenAPIVersion(listMetaSchemaRef, v2)).WithDescription(swaggerPartialObjectMetadataListDescriptions["metadata"]))

View File

@ -43,19 +43,3 @@ var fakeTypeConverter = func() internal.TypeConverter {
} }
return typeConverter return typeConverter
}() }()
var testTypeConverter = func() internal.TypeConverter {
data, err := ioutil.ReadFile(filepath.Join("testdata", "swagger.json"))
if err != nil {
panic(err)
}
spec := spec.Swagger{}
if err := json.Unmarshal(data, &spec); err != nil {
panic(err)
}
typeConverter, err := internal.NewTypeConverter(&spec, false)
if err != nil {
panic(err)
}
return typeConverter
}()

View File

@ -21,9 +21,10 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/managedfields" "k8s.io/apimachinery/pkg/runtime/schema"
utilopenapi "k8s.io/apiserver/pkg/util/openapi" "k8s.io/kube-openapi/pkg/schemaconv"
"k8s.io/kube-openapi/pkg/validation/spec" "k8s.io/kube-openapi/pkg/validation/spec"
smdschema "sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/typed" "sigs.k8s.io/structured-merge-diff/v4/typed"
"sigs.k8s.io/structured-merge-diff/v4/value" "sigs.k8s.io/structured-merge-diff/v4/value"
) )
@ -36,29 +37,35 @@ type TypeConverter interface {
} }
type typeConverter struct { type typeConverter struct {
parser *managedfields.GvkParser parser map[schema.GroupVersionKind]*typed.ParseableType
} }
var _ TypeConverter = &typeConverter{} var _ TypeConverter = &typeConverter{}
// NewTypeConverter builds a TypeConverter from a proto.Models. This // NewTypeConverter builds a TypeConverter from a spec.Swagger. This
// will automatically find the proper version of the object, and the // will automatically find the proper version of the object, and the
// corresponding schema information. // corresponding schema information.
func NewTypeConverter(openapiSpec *spec.Swagger, preserveUnknownFields bool) (TypeConverter, error) { func NewTypeConverter(openapiSpec *spec.Swagger, preserveUnknownFields bool) (TypeConverter, error) {
models, err := utilopenapi.ToProtoModels(openapiSpec) pointerDefs := map[string]*spec.Schema{}
if err != nil { for k, v := range openapiSpec.Definitions {
return nil, err vCopy := v
pointerDefs[k] = &vCopy
} }
parser, err := managedfields.NewGVKParser(models, preserveUnknownFields)
typeSchema, err := schemaconv.ToSchemaFromOpenAPI(pointerDefs, preserveUnknownFields)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to convert models to schema: %v", err)
} }
return &typeConverter{parser: parser}, nil
typeParser := typed.Parser{Schema: smdschema.Schema{Types: typeSchema.Types}}
tr := indexModels(&typeParser, pointerDefs)
return &typeConverter{parser: tr}, nil
} }
func (c *typeConverter) ObjectToTyped(obj runtime.Object) (*typed.TypedValue, error) { func (c *typeConverter) ObjectToTyped(obj runtime.Object) (*typed.TypedValue, error) {
gvk := obj.GetObjectKind().GroupVersionKind() gvk := obj.GetObjectKind().GroupVersionKind()
t := c.parser.Type(gvk) t := c.parser[gvk]
if t == nil { if t == nil {
return nil, NewNoCorrespondingTypeError(gvk) return nil, NewNoCorrespondingTypeError(gvk)
} }
@ -110,3 +117,86 @@ func valueToObject(val value.Value) (runtime.Object, error) {
return nil, fmt.Errorf("failed to convert value to unstructured for type %T", vu) return nil, fmt.Errorf("failed to convert value to unstructured for type %T", vu)
} }
} }
func indexModels(
typeParser *typed.Parser,
openAPISchemas map[string]*spec.Schema,
) map[schema.GroupVersionKind]*typed.ParseableType {
tr := map[schema.GroupVersionKind]*typed.ParseableType{}
for modelName, model := range openAPISchemas {
gvkList := parseGroupVersionKind(model.Extensions)
if len(gvkList) == 0 {
continue
}
parsedType := typeParser.Type(modelName)
for _, gvk := range gvkList {
if len(gvk.Kind) > 0 {
tr[schema.GroupVersionKind(gvk)] = &parsedType
}
}
}
return tr
}
// Get and parse GroupVersionKind from the extension. Returns empty if it doesn't have one.
func parseGroupVersionKind(extensions map[string]interface{}) []schema.GroupVersionKind {
gvkListResult := []schema.GroupVersionKind{}
// Get the extensions
gvkExtension, ok := extensions["x-kubernetes-group-version-kind"]
if !ok {
return []schema.GroupVersionKind{}
}
// gvk extension must be a list of at least 1 element.
gvkList, ok := gvkExtension.([]interface{})
if !ok {
return []schema.GroupVersionKind{}
}
for _, gvk := range gvkList {
var group, version, kind string
// gvk extension list must be a map with group, version, and
// kind fields
if gvkMap, ok := gvk.(map[interface{}]interface{}); ok {
group, ok = gvkMap["group"].(string)
if !ok {
continue
}
version, ok = gvkMap["version"].(string)
if !ok {
continue
}
kind, ok = gvkMap["kind"].(string)
if !ok {
continue
}
} else if gvkMap, ok := gvk.(map[string]interface{}); ok {
group, ok = gvkMap["group"].(string)
if !ok {
continue
}
version, ok = gvkMap["version"].(string)
if !ok {
continue
}
kind, ok = gvkMap["kind"].(string)
if !ok {
continue
}
} else {
continue
}
gvkListResult = append(gvkListResult, schema.GroupVersionKind{
Group: group,
Version: version,
Kind: kind,
})
}
return gvkListResult
}

View File

@ -14,22 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package internal_test package internal
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"testing" "testing"
"github.com/stretchr/testify/require"
smdschema "sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/typed" "sigs.k8s.io/structured-merge-diff/v4/typed"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kube-openapi/pkg/validation/spec"
) )
func TestTypeConverter(t *testing.T) { func TestTypeConverter(t *testing.T) {
dtc := internal.NewDeducedTypeConverter() dtc := NewDeducedTypeConverter()
testCases := []struct { testCases := []struct {
name string name string
@ -106,7 +109,7 @@ spec:
} }
} }
func testObjectToTyped(t *testing.T, tc internal.TypeConverter, y string) { func testObjectToTyped(t *testing.T, tc TypeConverter, y string) {
obj := &unstructured.Unstructured{Object: map[string]interface{}{}} obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal([]byte(y), &obj.Object); err != nil { if err := yaml.Unmarshal([]byte(y), &obj.Object); err != nil {
t.Fatalf("Failed to parse yaml object: %v", err) t.Fatalf("Failed to parse yaml object: %v", err)
@ -169,3 +172,146 @@ spec:
} }
result = *r result = *r
} }
func TestIndexModels(t *testing.T) {
myDefs := map[string]*spec.Schema{
// Show empty GVK extension is ignored
"def0": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-group-version-kind": []interface{}{},
},
},
},
// Show nil GVK is ignored
"def0.0": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-group-version-kind": nil,
},
},
},
// Show this is ignored
"def0.1": {},
// Show allows binding a single GVK
"def1": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-group-version-kind": []interface{}{
map[string]interface{}{
"group": "mygroup",
"version": "v1",
"kind": "MyKind",
},
},
},
},
},
// Show allows bindings with two versions
"def2": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-group-version-kind": []interface{}{
map[string]interface{}{
"group": "mygroup",
"version": "v1",
"kind": "MyOtherKind",
},
map[string]interface{}{
"group": "mygroup",
"version": "v2",
"kind": "MyOtherKind",
},
},
},
},
},
// Show that we can mix and match GVKs from other definitions, and
// that both map[interface{}]interface{} and map[string]interface{}
// are allowed
"def3": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-group-version-kind": []interface{}{
map[string]interface{}{
"group": "mygroup",
"version": "v3",
"kind": "MyKind",
},
map[interface{}]interface{}{
"group": "mygroup",
"version": "v3",
"kind": "MyOtherKind",
},
},
},
},
},
}
myTypes := []smdschema.TypeDef{
{
Name: "def0",
Atom: smdschema.Atom{},
},
{
Name: "def0.1",
Atom: smdschema.Atom{},
},
{
Name: "def0.2",
Atom: smdschema.Atom{},
},
{
Name: "def1",
Atom: smdschema.Atom{},
},
{
Name: "def2",
Atom: smdschema.Atom{},
},
{
Name: "def3",
Atom: smdschema.Atom{},
},
}
parser := typed.Parser{Schema: smdschema.Schema{Types: myTypes}}
gvkIndex := indexModels(&parser, myDefs)
require.Len(t, gvkIndex, 5)
resultNames := map[schema.GroupVersionKind]string{}
for k, v := range gvkIndex {
require.NotNil(t, v.TypeRef.NamedType)
resultNames[k] = *v.TypeRef.NamedType
}
require.Equal(t, resultNames, map[schema.GroupVersionKind]string{
{
Group: "mygroup",
Version: "v1",
Kind: "MyKind",
}: "def1",
{
Group: "mygroup",
Version: "v1",
Kind: "MyOtherKind",
}: "def2",
{
Group: "mygroup",
Version: "v2",
Kind: "MyOtherKind",
}: "def2",
{
Group: "mygroup",
Version: "v3",
Kind: "MyKind",
}: "def3",
{
Group: "mygroup",
Version: "v3",
Kind: "MyOtherKind",
}: "def3",
})
}