diff --git a/pkg/crd/gen_integration_test.go b/pkg/crd/gen_integration_test.go index 3358305fe..cc0a572e3 100644 --- a/pkg/crd/gen_integration_test.go +++ b/pkg/crd/gen_integration_test.go @@ -34,8 +34,8 @@ import ( var _ = Describe("CRD Generation proper defaulting", func() { var ( - ctx, ctx2 *genall.GenerationContext - out *outputRule + ctx, ctx2, ctx3 *genall.GenerationContext + out *outputRule genDir = filepath.Join("testdata", "gen") ) @@ -53,7 +53,10 @@ var _ = Describe("CRD Generation proper defaulting", func() { Expect(pkgs).To(HaveLen(1)) pkgs2, err := loader.LoadRoots("./...") Expect(err).NotTo(HaveOccurred()) - Expect(pkgs2).To(HaveLen(2)) + Expect(pkgs2).To(HaveLen(3)) + pkgs3, err := loader.LoadRoots("./iface") + Expect(err).NotTo(HaveOccurred()) + Expect(pkgs3).To(HaveLen(1)) By("setup up the context") reg := &markers.Registry{} @@ -73,6 +76,12 @@ var _ = Describe("CRD Generation proper defaulting", func() { Checker: &loader.TypeChecker{}, OutputRule: out, } + ctx3 = &genall.GenerationContext{ + Collector: &markers.Collector{Registry: reg}, + Roots: pkgs3, + Checker: &loader.TypeChecker{}, + OutputRule: out, + } }) It("should fail to generate v1beta1 CRDs", func() { @@ -106,13 +115,15 @@ var _ = Describe("CRD Generation proper defaulting", func() { Expect(gen.Generate(ctx2)).NotTo(HaveOccurred()) By("loading the desired YAMLs") + expectedFileIfaces, err := os.ReadFile(filepath.Join(genDir, "iface", "iface.example.com_kindwithifaces.yaml")) + Expect(err).NotTo(HaveOccurred()) expectedFileFoos, err := os.ReadFile(filepath.Join(genDir, "bar.example.com_foos.yaml")) Expect(err).NotTo(HaveOccurred()) expectedFileZoos, err := os.ReadFile(filepath.Join(genDir, "zoo", "bar.example.com_zoos.yaml")) Expect(err).NotTo(HaveOccurred()) - By("comparing the two, output must be deterministic because groupKinds are sorted") - expectedOut := string(expectedFileFoos) + string(expectedFileZoos) + By("comparing the three, output must be deterministic because groupKinds are sorted") + expectedOut := string(expectedFileFoos) + string(expectedFileIfaces) + string(expectedFileZoos) Expect(out.buf.String()).To(Equal(expectedOut), cmp.Diff(out.buf.String(), expectedOut)) }) @@ -169,6 +180,26 @@ var _ = Describe("CRD Generation proper defaulting", func() { By("comparing the two") Expect(out.buf.String()).To(Equal(string(expectedFile)), cmp.Diff(out.buf.String(), string(expectedFile))) }) + + It("should gracefully error on interface types", func() { + gen := &crd.Generator{} + err := gen.Generate(ctx3) + Expect(err).NotTo(HaveOccurred()) + + wd, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + matches := 0 + for _, pkg := range ctx3.Roots { + for _, pkgError := range pkg.Errors { + posRel, err := filepath.Rel(filepath.Join(wd, genDir), pkgError.Pos) + Expect(err).NotTo(HaveOccurred()) + Expect(posRel).To(Equal("iface/iface_types.go:32:6")) + Expect(pkgError.Msg).To(Equal("cannot generate schema for any")) + matches++ + } + } + Expect(matches).To(Equal(1)) + }) }) type outputRule struct { diff --git a/pkg/crd/schema.go b/pkg/crd/schema.go index efb09b7c9..082e3320a 100644 --- a/pkg/crd/schema.go +++ b/pkg/crd/schema.go @@ -270,8 +270,9 @@ func localNamedToSchema(ctx *schemaContext, ident *ast.Ident) *apiext.JSONSchema if aliasInfo, isAlias := typeInfo.(*types.Alias); isAlias { typeInfo = aliasInfo.Rhs() } - if basicInfo, isBasic := typeInfo.(*types.Basic); isBasic { - typ, fmt, err := builtinToType(basicInfo, ctx.allowDangerousTypes) + switch typeInfo := typeInfo.(type) { + case *types.Basic: + typ, fmt, err := builtinToType(typeInfo, ctx.allowDangerousTypes) if err != nil { ctx.pkg.AddError(loader.ErrFromNode(err, ident)) } @@ -280,7 +281,7 @@ func localNamedToSchema(ctx *schemaContext, ident *ast.Ident) *apiext.JSONSchema // > For gotypesalias=1, alias declarations produce an Alias type. // > Otherwise, the alias information is only in the type name, which // > points directly to the actual (aliased) type. - if basicInfo.Name() != ident.Name { + if typeInfo.Name() != ident.Name { ctx.requestSchema("", ident.Name) link := TypeRefLink("", ident.Name) return &apiext.JSONSchemaProps{ @@ -293,19 +294,24 @@ func localNamedToSchema(ctx *schemaContext, ident *ast.Ident) *apiext.JSONSchema Type: typ, Format: fmt, } - } - // NB(directxman12): if there are dot imports, this might be an external reference, - // so use typechecking info to get the actual object - typeNameInfo := typeInfo.(interface{ Obj() *types.TypeName }).Obj() - pkg := typeNameInfo.Pkg() - pkgPath := loader.NonVendorPath(pkg.Path()) - if pkg == ctx.pkg.Types { - pkgPath = "" - } - ctx.requestSchema(pkgPath, typeNameInfo.Name()) - link := TypeRefLink(pkgPath, typeNameInfo.Name()) - return &apiext.JSONSchemaProps{ - Ref: &link, + case interface{ Obj() *types.TypeName }: + // NB(directxman12): if there are dot imports, this might be an external reference, + // so use typechecking info to get the actual object + typeNameInfo := typeInfo.Obj() + pkg := typeNameInfo.Pkg() + pkgPath := loader.NonVendorPath(pkg.Path()) + if pkg == ctx.pkg.Types { + pkgPath = "" + } + ctx.requestSchema(pkgPath, typeNameInfo.Name()) + link := TypeRefLink(pkgPath, typeNameInfo.Name()) + return &apiext.JSONSchemaProps{ + Ref: &link, + } + default: + // This happens for type any, and other scenarios. + ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("cannot generate schema for %s", ident.Name), ident)) + return &apiext.JSONSchemaProps{} } } diff --git a/pkg/crd/testdata/gen/iface/iface.example.com_kindwithifaces.yaml b/pkg/crd/testdata/gen/iface/iface.example.com_kindwithifaces.yaml new file mode 100644 index 000000000..8474d1435 --- /dev/null +++ b/pkg/crd/testdata/gen/iface/iface.example.com_kindwithifaces.yaml @@ -0,0 +1,46 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (devel) + name: kindwithifaces.iface.example.com +spec: + group: iface.example.com + names: + kind: KindWithIFace + listKind: KindWithIFaceList + plural: kindwithifaces + singular: kindwithiface + scope: Namespaced + versions: + - name: iface + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + bar: {} + type: object + required: + - metadata + type: object + served: true + storage: true diff --git a/pkg/crd/testdata/gen/iface/iface_types.go b/pkg/crd/testdata/gen/iface/iface_types.go new file mode 100644 index 000000000..d1de2dc51 --- /dev/null +++ b/pkg/crd/testdata/gen/iface/iface_types.go @@ -0,0 +1,33 @@ +/* + +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. +*/ + +//go:generate ../../../../../.run-controller-gen.sh crd:crdVersions=v1 paths=. output:dir=. + +// +groupName=iface.example.com +package iface + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type KindWithIFace struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + Spec KindWithIFaceSpec `json:"spec,omitempty"` +} + +type KindWithIFaceSpec struct { + Bar any `json:"bar,omitempty"` +}