diff --git a/pkg/schema/definitions/fixtures_test.go b/pkg/schema/definitions/fixtures_test.go index afc4f811..2eaf1623 100644 --- a/pkg/schema/definitions/fixtures_test.go +++ b/pkg/schema/definitions/fixtures_test.go @@ -4,8 +4,10 @@ import ( "bytes" "fmt" + openapi_v2 "github.com/google/gnostic-models/openapiv2" "github.com/rancher/wrangler/v3/pkg/yaml" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/kube-openapi/pkg/util/proto" ) var ( @@ -398,3 +400,114 @@ definitions: kind: "ConfigMap" version: "v1" ` + +var ( + rawSchemalessCRDs = ` +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: schemaless.management.cattle.io +spec: + conversion: + strategy: None + group: management.cattle.io + names: + kind: Schemaless + listKind: SchemalessList + plural: schemalese + singular: schemaless + scope: Cluster + versions: + - name: v2 + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + rkeConfig: + type: object + nullable: true + x-kubernetes-preserve-unknown-fields: true + served: true + storage: true +` + + rawSchemalessModels = ` +swagger: "2.0" +info: + title: "Test openapi spec" + version: "v1.0.0" +paths: + /apis/management.cattle.io/v3/globalroles: + get: + description: "get a global role" + responses: + 200: + description: "OK" +definitions: + io.cattle.management.v2.schemaless: + description: "this kind has no schema" + type: "object" + properties: + apiVersion: + description: "The APIVersion of this resource" + type: "string" + kind: + description: "The kind" + type: "string" + metadata: + description: "The metadata" + $ref: "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + spec: + description: "The spec for the resource" + type: "object" + required: + - "name" + properties: + name: + description: "The name of the resource" + type: "string" + notRequired: + description: "Some field that isn't required" + type: "boolean" + x-kubernetes-group-version-kind: + - group: "management.cattle.io" + version: "v2" + kind: "Schemaless" + io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta: + description: "Object Metadata" + properties: + annotations: + description: "annotations of the resource" + type: "object" + additionalProperties: + type: "string" + name: + description: "name of the resource" + type: "string" +` +) + +func getSchemalessCRDs() ([]*apiextv1.CustomResourceDefinition, error) { + crds, err := yaml.UnmarshalWithJSONDecoder[*apiextv1.CustomResourceDefinition](bytes.NewBuffer([]byte(rawSchemalessCRDs))) + if err != nil { + return nil, fmt.Errorf("unmarshaling raw CRDs: %w", err) + } + for _, crd := range crds { + for _, crdVersion := range crd.Spec.Versions { + crdVersion.Schema = nil + } + } + return crds, err +} + +func getSchemalessModels() (proto.Models, error) { + doc, err := openapi_v2.ParseDocument([]byte(rawSchemalessModels)) + if err != nil { + return nil, fmt.Errorf("unmarshaling raw models: %w", err) + } + models, err := proto.NewOpenAPIData(doc) + return models, err +} diff --git a/pkg/schema/definitions/handler_test.go b/pkg/schema/definitions/handler_test.go index 2010f0ce..85cc20f0 100644 --- a/pkg/schema/definitions/handler_test.go +++ b/pkg/schema/definitions/handler_test.go @@ -196,6 +196,59 @@ func TestRefresh(t *testing.T) { } } +func TestRefreshSchemalessCRDs(t *testing.T) { + schemalessModels, err := getSchemalessModels() + require.NoError(t, err) + + crds, err := getSchemalessCRDs() + require.NoError(t, err) + + for _, crd := range crds { + for _, crdVersion := range crd.Spec.Versions { + crdVersion.Schema = nil + } + } + + test := struct { + name string + nilGroups bool + wantModels proto.Models + wantGVKModels map[string]gvkModel + }{ + name: "problem - missing schema", + wantModels: schemalessModels, + wantGVKModels: map[string]gvkModel{ + "management.cattle.io.schemaless": { + ModelName: "io.cattle.management.v2.schemaless", + }, + }, + } + t.Run(test.name, func(t *testing.T) { + client, err := buildDefaultServerlessDiscovery() + require.Nil(t, err) + baseSchemas := types.EmptyAPISchemas() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + crdCache := fake.NewMockNonNamespacedCacheInterface[*apiextv1.CustomResourceDefinition](ctrl) + crdCache.EXPECT().List(labels.Everything()).Return(crds, nil).AnyTimes() + + handler := NewSchemaDefinitionHandler(baseSchemas, crdCache, client) + err = handler.Refresh() + require.NoError(t, err) + + handler.lock.RLock() + defer handler.lock.RUnlock() + require.Equal(t, test.wantModels, handler.models) + // Just test the model names, because the schema and crd will be null in the input + require.Equal(t, len(test.wantGVKModels), len(handler.gvkModels)) + for k, v := range test.wantGVKModels { + require.Equal(t, v.ModelName, handler.gvkModels[k].ModelName) + } + }) +} + func Test_byID(t *testing.T) { discoveryClient, err := buildDefaultDiscovery() require.NoError(t, err) @@ -735,6 +788,65 @@ func buildDefaultDiscovery() (*fakeDiscovery, error) { }, nil } +func buildDefaultServerlessDiscovery() (*fakeDiscovery, error) { + document, err := openapi_v2.ParseDocument([]byte(rawSchemalessModels)) + if err != nil { + return nil, fmt.Errorf("unable to parse rawSchemalessModels: %w", err) + } + groups := []metav1.APIGroup{ + // The core groups (eg: Pods, ConfigMaps, etc) + { + Name: "", + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "v1", + Version: "v1", + }, + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "v1", + Version: "v1", + }, + }, + }, + { + Name: "management.cattle.io", + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "management.cattle.io/v2", + Version: "v2", + }, + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "management.cattle.io/v1", + Version: "v1", + }, + { + GroupVersion: "management.cattle.io/v2", + Version: "v2", + }, + }, + }, + { + Name: "noversion.cattle.io", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "noversion.cattle.io/v1", + Version: "v1", + }, + { + GroupVersion: "noversion.cattle.io/v2", + Version: "v2", + }, + }, + }, + } + return &fakeDiscovery{ + Groups: &metav1.APIGroupList{ + Groups: groups, + }, + Document: document, + }, nil +} + type fakeDiscovery struct { Groups *metav1.APIGroupList Document *openapi_v2.Document