Skip to content

Commit e81ea43

Browse files
authored
Merge pull request #493 from DirectXMan12/bug/deterministic-type-checking
🐛 Don't allow generators to interfere with eachother's type checking
2 parents 6c9ddb1 + 2f4e044 commit e81ea43

File tree

13 files changed

+315
-153
lines changed

13 files changed

+315
-153
lines changed

pkg/crd/gen.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package crd
1818

1919
import (
2020
"fmt"
21+
"go/ast"
2122
"go/types"
2223

2324
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -84,6 +85,9 @@ type Generator struct {
8485
CRDVersions []string `marker:"crdVersions,optional"`
8586
}
8687

88+
func (Generator) CheckFilter() loader.NodeFilter {
89+
return filterTypesForCRDs
90+
}
8791
func (Generator) RegisterMarkers(into *markers.Registry) error {
8892
return crdmarkers.Register(into)
8993
}
@@ -319,3 +323,22 @@ func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) map[schema.GroupKi
319323

320324
return kubeKinds
321325
}
326+
327+
// filterTypesForCRDs filters out all nodes that aren't used in CRD generation,
328+
// like interfaces and struct fields without JSON tag.
329+
func filterTypesForCRDs(node ast.Node) bool {
330+
switch node := node.(type) {
331+
case *ast.InterfaceType:
332+
// skip interfaces, we never care about references in them
333+
return false
334+
case *ast.StructType:
335+
return true
336+
case *ast.Field:
337+
_, hasTag := loader.ParseAstTag(node.Tag).Lookup("json")
338+
// fields without JSON tags mean we have custom serialization,
339+
// so only visit fields with tags.
340+
return hasTag
341+
default:
342+
return true
343+
}
344+
}

pkg/crd/parser.go

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package crd
1818

1919
import (
2020
"fmt"
21-
"go/ast"
2221

2322
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2423
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -216,7 +215,7 @@ func (p *Parser) AddPackage(pkg *loader.Package) {
216215
return
217216
}
218217
p.indexTypes(pkg)
219-
p.Checker.Check(pkg, filterTypesForCRDs)
218+
p.Checker.Check(pkg)
220219
p.packages[pkg] = struct{}{}
221220
}
222221

@@ -236,22 +235,3 @@ func (p *Parser) NeedPackage(pkg *loader.Package) {
236235
}
237236
p.AddPackage(pkg)
238237
}
239-
240-
// filterTypesForCRDs filters out all nodes that aren't used in CRD generation,
241-
// like interfaces and struct fields without JSON tag.
242-
func filterTypesForCRDs(node ast.Node) bool {
243-
switch node := node.(type) {
244-
case *ast.InterfaceType:
245-
// skip interfaces, we never care about references in them
246-
return false
247-
case *ast.StructType:
248-
return true
249-
case *ast.Field:
250-
_, hasTag := loader.ParseAstTag(node.Tag).Lookup("json")
251-
// fields without JSON tags mean we have custom serialization,
252-
// so only visit fields with tags.
253-
return hasTag
254-
default:
255-
return true
256-
}
257-
}

pkg/crd/schema_visitor.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ type SchemaVisitor interface {
2727
// this visitor will be called again with `nil` to indicate that
2828
// all children have been visited. If a nil visitor is returned,
2929
// children are not visited.
30+
//
31+
// It is *NOT* safe to save references to the given schema.
32+
// Make deepcopies if you need to keep things around beyond
33+
// the lifetime of the call.
3034
Visit(schema *apiext.JSONSchemaProps) SchemaVisitor
3135
}
3236

@@ -102,6 +106,8 @@ func (w schemaWalker) walkSchema(schema *apiext.JSONSchemaProps) {
102106
// walkMap walks over values of the given map, saving changes to them.
103107
func (w schemaWalker) walkMap(defs map[string]apiext.JSONSchemaProps) {
104108
for name, def := range defs {
109+
// this is iter var reference is because we immediately preseve it below
110+
//nolint:gosec
105111
w.walkSchema(&def)
106112
// make sure the edits actually go through since we can't
107113
// take a reference to the value in the map

pkg/deepcopy/deepcopy_integration_test.go

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,52 @@ limitations under the License.
1717
package deepcopy_test
1818

1919
import (
20+
"io"
2021
"io/ioutil"
2122
"os"
23+
"sort"
2224

2325
"github.com/google/go-cmp/cmp"
2426
. "github.com/onsi/ginkgo"
2527
. "github.com/onsi/gomega"
26-
"golang.org/x/tools/go/packages"
2728

29+
"sigs.k8s.io/controller-tools/pkg/crd"
2830
"sigs.k8s.io/controller-tools/pkg/deepcopy"
31+
"sigs.k8s.io/controller-tools/pkg/genall"
2932
"sigs.k8s.io/controller-tools/pkg/loader"
3033
"sigs.k8s.io/controller-tools/pkg/markers"
3134
)
3235

33-
func packageErrors(pkg *loader.Package, filterKinds ...packages.ErrorKind) error {
34-
toSkip := make(map[packages.ErrorKind]struct{})
35-
for _, errKind := range filterKinds {
36-
toSkip[errKind] = struct{}{}
36+
type outputToMap map[string]*outputFile
37+
38+
// Open implements genall.OutputRule.
39+
func (m outputToMap) Open(_ *loader.Package, path string) (io.WriteCloser, error) {
40+
if _, ok := m[path]; !ok {
41+
m[path] = &outputFile{}
3742
}
38-
var outErr error
39-
packages.Visit([]*packages.Package{pkg.Package}, nil, func(pkgRaw *packages.Package) {
40-
for _, err := range pkgRaw.Errors {
41-
if _, skip := toSkip[err.Kind]; skip {
42-
continue
43-
}
44-
outErr = err
45-
}
46-
})
47-
return outErr
43+
return m[path], nil
44+
}
45+
46+
func (m outputToMap) fileList() []string {
47+
ret := make([]string, 0, len(m))
48+
for path := range m {
49+
ret = append(ret, path)
50+
}
51+
sort.Strings(ret)
52+
return ret
53+
}
54+
55+
type outputFile struct {
56+
contents []byte
57+
}
58+
59+
func (o *outputFile) Write(p []byte) (int, error) {
60+
o.contents = append(o.contents, p...)
61+
return len(p), nil
62+
}
63+
64+
func (o *outputFile) Close() error {
65+
return nil
4866
}
4967

5068
var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func() {
@@ -55,35 +73,37 @@ var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func
5573
Expect(os.Chdir("./testdata")).To(Succeed()) // go modules are directory-sensitive
5674
defer func() { Expect(os.Chdir(cwd)).To(Succeed()) }()
5775

58-
By("loading the roots")
59-
pkgs, err := loader.LoadRoots(".")
60-
Expect(err).NotTo(HaveOccurred())
61-
Expect(pkgs).To(HaveLen(1))
62-
cronJobPkg := pkgs[0]
76+
output := make(outputToMap)
6377

64-
By("setting up the generation context")
65-
collector := &markers.Collector{Registry: &markers.Registry{}}
66-
Expect(deepcopy.Generator{}.RegisterMarkers(collector.Registry)).To(Succeed())
67-
checker := &loader.TypeChecker{}
68-
ctx := &deepcopy.ObjectGenCtx{
69-
Collector: collector,
70-
Checker: checker,
71-
}
72-
73-
By("requesting that types be generated")
74-
outContents := ctx.GenerateForPackage(cronJobPkg)
78+
By("initializing the runtime")
79+
optionsRegistry := &markers.Registry{}
80+
Expect(optionsRegistry.Register(markers.Must(markers.MakeDefinition("crd", markers.DescribesPackage, crd.Generator{})))).To(Succeed())
81+
Expect(optionsRegistry.Register(markers.Must(markers.MakeDefinition("object", markers.DescribesPackage, deepcopy.Generator{})))).To(Succeed())
82+
rt, err := genall.FromOptions(optionsRegistry, []string{
83+
"crd", // Run another generator first to make sure they don't interfere; see also: the comment on cronjob_types.go:UntypedBlob
84+
"object",
85+
})
86+
Expect(err).NotTo(HaveOccurred())
87+
rt.OutputRules = genall.OutputRules{Default: output}
7588

76-
By("checking that no errors occurred along the way (expect for type errors)")
77-
Expect(packageErrors(cronJobPkg, packages.TypeError)).NotTo(HaveOccurred())
89+
By("running the generator and checking for errors")
90+
hadErrs := rt.Run()
7891

7992
By("checking that we got output contents")
93+
Expect(output.fileList()).To(ContainElement("zz_generated.deepcopy.go")) // Don't use HaveKey--the output is too verbose to be usable
94+
outFile := output["zz_generated.deepcopy.go"]
95+
Expect(outFile).NotTo(BeNil())
96+
outContents := outFile.contents
8097
Expect(outContents).NotTo(BeNil())
8198

8299
By("loading the desired code")
83100
expectedFile, err := ioutil.ReadFile("zz_generated.deepcopy.go")
84101
Expect(err).NotTo(HaveOccurred())
85102

86103
By("comparing the two")
87-
Expect(outContents).To(Equal(expectedFile), "generated code not as expected, check pkg/deepcopy/testdata/README.md for more details.\n\nDiff:\n\n%s", cmp.Diff(outContents, expectedFile))
104+
Expect(string(outContents)).To(Equal(string(expectedFile)), "generated code not as expected, check pkg/deepcopy/testdata/README.md for more details.\n\nDiff:\n\n%s", cmp.Diff(outContents, expectedFile))
105+
106+
By("checking for errors")
107+
Expect(hadErrs).To(BeFalse())
88108
})
89109
})

pkg/deepcopy/gen.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ type Generator struct {
5858
Year string `marker:",optional"`
5959
}
6060

61+
func (Generator) CheckFilter() loader.NodeFilter {
62+
return func(node ast.Node) bool {
63+
// ignore interfaces
64+
_, isIface := node.(*ast.InterfaceType)
65+
return !isIface
66+
}
67+
}
68+
6169
func (Generator) RegisterMarkers(into *markers.Registry) error {
6270
if err := markers.RegisterAll(into,
6371
enablePkgMarker, legacyEnablePkgMarker, enableTypeMarker,
@@ -144,7 +152,7 @@ func (d Generator) Generate(ctx *genall.GenerationContext) error {
144152
}
145153

146154
for _, root := range ctx.Roots {
147-
outContents := objGenCtx.GenerateForPackage(root)
155+
outContents := objGenCtx.generateForPackage(root)
148156
if outContents == nil {
149157
continue
150158
}
@@ -186,21 +194,17 @@ import (
186194

187195
}
188196

189-
// GenerateForPackage generates DeepCopy and runtime.Object implementations for
197+
// generateForPackage generates DeepCopy and runtime.Object implementations for
190198
// types in the given package, writing the formatted result to given writer.
191199
// May return nil if source could not be generated.
192-
func (ctx *ObjectGenCtx) GenerateForPackage(root *loader.Package) []byte {
200+
func (ctx *ObjectGenCtx) generateForPackage(root *loader.Package) []byte {
193201
allTypes, err := enabledOnPackage(ctx.Collector, root)
194202
if err != nil {
195203
root.AddError(err)
196204
return nil
197205
}
198206

199-
ctx.Checker.Check(root, func(node ast.Node) bool {
200-
// ignore interfaces
201-
_, isIface := node.(*ast.InterfaceType)
202-
return !isIface
203-
})
207+
ctx.Checker.Check(root)
204208

205209
root.NeedTypesInfo()
206210

0 commit comments

Comments
 (0)