diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder/builder.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder/builder.go index d27cd378d87..e04f3b33e0b 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder/builder.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder/builder.go @@ -493,7 +493,9 @@ func addTypeMetaProperties(s *spec.Schema, v2 bool) { func (b *builder) buildListSchema(v2 bool) *spec.Schema { 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) - 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"). SetProperty("items", *spec.ArrayProperty(spec.RefSchema(refForOpenAPIVersion(name, v2))).WithDescription(doc)). SetProperty("metadata", *spec.RefSchema(refForOpenAPIVersion(listMetaSchemaRef, v2)).WithDescription(swaggerPartialObjectMetadataListDescriptions["metadata"])) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fieldmanager_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fieldmanager_test.go index f8edf6913dc..0d3c432a867 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fieldmanager_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fieldmanager_test.go @@ -43,19 +43,3 @@ var fakeTypeConverter = func() internal.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 -}() diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go index 3ff7507b32c..a6254a4dc50 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go @@ -21,9 +21,10 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/managedfields" - utilopenapi "k8s.io/apiserver/pkg/util/openapi" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kube-openapi/pkg/schemaconv" "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/value" ) @@ -36,29 +37,35 @@ type TypeConverter interface { } type typeConverter struct { - parser *managedfields.GvkParser + parser map[schema.GroupVersionKind]*typed.ParseableType } 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 // corresponding schema information. func NewTypeConverter(openapiSpec *spec.Swagger, preserveUnknownFields bool) (TypeConverter, error) { - models, err := utilopenapi.ToProtoModels(openapiSpec) - if err != nil { - return nil, err + pointerDefs := map[string]*spec.Schema{} + for k, v := range openapiSpec.Definitions { + vCopy := v + pointerDefs[k] = &vCopy } - parser, err := managedfields.NewGVKParser(models, preserveUnknownFields) + + typeSchema, err := schemaconv.ToSchemaFromOpenAPI(pointerDefs, preserveUnknownFields) 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) { gvk := obj.GetObjectKind().GroupVersionKind() - t := c.parser.Type(gvk) + t := c.parser[gvk] if t == nil { 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) } } + +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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter_test.go index 83197ea886a..f396a2b2f1a 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter_test.go @@ -14,22 +14,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -package internal_test +package internal import ( "fmt" "reflect" "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/yaml" "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) { - dtc := internal.NewDeducedTypeConverter() + dtc := NewDeducedTypeConverter() testCases := []struct { 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{}{}} if err := yaml.Unmarshal([]byte(y), &obj.Object); err != nil { t.Fatalf("Failed to parse yaml object: %v", err) @@ -169,3 +172,146 @@ spec: } 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", + }) +}