diff --git a/pkg/crd/gen_integration_test.go b/pkg/crd/gen_integration_test.go index 3358305fe..6dcf6b876 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 interface type any")) + matches++ + } + } + Expect(matches).To(Equal(1)) + }) }) type outputRule struct { diff --git a/pkg/crd/schema.go b/pkg/crd/schema.go index efb09b7c9..bb5de2898 100644 --- a/pkg/crd/schema.go +++ b/pkg/crd/schema.go @@ -294,6 +294,10 @@ func localNamedToSchema(ctx *schemaContext, ident *ast.Ident) *apiext.JSONSchema Format: fmt, } } + if _, isInterface := typeInfo.(*types.Interface); isInterface { + ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("cannot generate schema for interface type %s", ident.Name), ident)) + return &apiext.JSONSchemaProps{} + } // 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() 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"` +}