Skip to content

Commit 05161c9

Browse files
committed
Cross-module support
This patch updates the controller-gen package loader to support loading packages across Go module boundaries.
1 parent cb13ac5 commit 05161c9

File tree

13 files changed

+508
-7
lines changed

13 files changed

+508
-7
lines changed

pkg/loader/loader.go

Lines changed: 218 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2019 The Kubernetes Authors.
2+
Copyright 2019-2022 The Kubernetes Authors.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -25,9 +25,12 @@ import (
2525
"go/types"
2626
"io/ioutil"
2727
"os"
28+
"path/filepath"
29+
"strings"
2830
"sync"
2931

3032
"golang.org/x/tools/go/packages"
33+
"k8s.io/apimachinery/pkg/util/sets"
3134
)
3235

3336
// Much of this is strongly inspired by the contents of go/packages,
@@ -329,7 +332,7 @@ func LoadRoots(roots ...string) ([]*Package, error) {
329332
//
330333
// This is generally only useful for use in testing when you need to modify
331334
// loading settings to load from a fake location.
332-
func LoadRootsWithConfig(cfg *packages.Config, roots ...string) ([]*Package, error) {
335+
func LoadRootsWithConfig(cfg *packages.Config, roots ...string) (pkgs []*Package, retErr error) {
333336
l := &loader{
334337
cfg: cfg,
335338
packages: make(map[*packages.Package]*Package),
@@ -341,13 +344,221 @@ func LoadRootsWithConfig(cfg *packages.Config, roots ...string) ([]*Package, err
341344
// put our build flags first so that callers can override them
342345
l.cfg.BuildFlags = append([]string{"-tags", "ignore_autogenerated"}, l.cfg.BuildFlags...)
343346

344-
rawPkgs, err := packages.Load(l.cfg, roots...)
345-
if err != nil {
346-
return nil, err
347+
// ensure the cfg.Dir field is reset to its original value upon
348+
// returning from this function. it should honestly be fine if it is
349+
// not given most callers will not send in the cfg parameter directly,
350+
// as it's largely for testing, but still, let's be good stewards.
351+
defer func(d string) {
352+
cfg.Dir = d
353+
}(cfg.Dir)
354+
355+
// ensure cfg.Dir is absolute. it just makes the rest of the code easier
356+
// to debug when there are any issues
357+
if cfg.Dir != "" && !filepath.IsAbs(cfg.Dir) {
358+
ap, err := filepath.Abs(cfg.Dir)
359+
if err != nil {
360+
return nil, err
361+
}
362+
cfg.Dir = ap
347363
}
348364

349-
for _, rawPkg := range rawPkgs {
350-
l.Roots = append(l.Roots, l.packageFor(rawPkg))
365+
// loadRoots is a helper function used in three locations below.
366+
loadRoots := func(uniqueRoots *sets.String, roots ...string) error {
367+
// load the root and gets its raw packages
368+
rawPkgs, err := packages.Load(l.cfg, roots...)
369+
if err != nil {
370+
return err
371+
}
372+
373+
// get the package path for each raw package
374+
for _, rawPkg := range rawPkgs {
375+
pkg := l.packageFor(rawPkg)
376+
377+
// if a set was provided to prevent duplicate packages, use it,
378+
// otherwise append to the package list
379+
if uniqueRoots == nil {
380+
l.Roots = append(l.Roots, pkg)
381+
} else if !uniqueRoots.Has(pkg.ID) {
382+
l.Roots = append(l.Roots, pkg)
383+
uniqueRoots.Insert(pkg.ID)
384+
}
385+
}
386+
387+
return nil
388+
}
389+
390+
// if no roots were provided then load the current package and return early
391+
if len(roots) == 0 {
392+
if err := loadRoots(nil); err != nil {
393+
return nil, err
394+
}
395+
return l.Roots, nil
396+
}
397+
398+
// store the value of cfg.Dir so we can use it later if it is non-empty.
399+
// we need to store it now as the value of cfg.Dir will be updated by
400+
// a loop below
401+
cfgDir := cfg.Dir
402+
403+
// uniqueRoots is used to keep track of the discovered packages to be nice
404+
// and try and prevent packages from showing up twice when nested module
405+
// support is enabled. there is not harm that comes from this per se, but
406+
// it makes testing easier when a known number of modules can be asserted
407+
uniqueRoots := sets.String{}
408+
409+
// isFSRoots is used to keep track of whether a root is a filesystem path
410+
// or a package/module name. the logic to determine this is based on the
411+
// output from "go help packages". The "go" command determines if a path
412+
// is a filesystem path based on whether it is:
413+
//
414+
// * absolute
415+
// * begins with "."
416+
// * begins with ".."
417+
//
418+
// otherwise the path is considered to be a package. this set is a list of
419+
// the indices from the roots slice that are filesystem paths
420+
isFSRoots := sets.Int{}
421+
422+
// addNestedGoModulesToRoots is given to filepath.WalkDir
423+
// and adds the directory part of p to the list of roots
424+
// IFF p is the path to a file named "go.mod"
425+
addNestedGoModulesToRoots := func(
426+
p string,
427+
d os.DirEntry,
428+
e error) error {
429+
430+
if e != nil {
431+
return e
432+
}
433+
if !d.IsDir() && filepath.Base(p) == "go.mod" {
434+
roots = append(roots, filepath.Join(filepath.Dir(p), "..."))
435+
isFSRoots.Insert(len(roots) - 1)
436+
}
437+
return nil
438+
}
439+
440+
// the basic loader logic is as follows:
441+
//
442+
// 1. iterate over the list of provided roots
443+
//
444+
// 2. if a root uses the nested path syntax, ex. ..., then walk the
445+
// root's descendants to search for any any nested Go modules, and if
446+
// found, load them to the list of roots
447+
//
448+
// 3. iterate over the list of updated roots
449+
//
450+
// 4. update the loader config's Dir property to be the directory element
451+
// of the current root
452+
//
453+
// 5. execute the loader on the base element of the current root, which
454+
// will be either "./." or "./..."
455+
//
456+
// the following range operation is parts 1-2
457+
for i := range roots {
458+
r := roots[i]
459+
460+
//fmt.Printf("1.processing root=%s\n", r)
461+
462+
// based on the logic from "go help packages", a path is a package/mod
463+
// name if it is absolute or begins with "." or "..". If it is none of
464+
// those things then go ahead and skip to the next iteration of the loop
465+
if !filepath.IsAbs(r) &&
466+
!strings.HasPrefix(r, ".") &&
467+
!strings.HasPrefix(r, "..") {
468+
469+
continue
470+
}
471+
472+
//fmt.Printf("1.processing root as file=%s\n", r)
473+
474+
// this is a filesytem path
475+
isFSRoots.Insert(i)
476+
477+
// clean up the root
478+
r = filepath.Clean(r)
479+
480+
// get the absolute path of the root
481+
if !filepath.IsAbs(r) {
482+
483+
// if the initial value of cfg.Dir was non-empty then use it when
484+
// building the absolute path to this root. otherwise use the
485+
// filepath.Abs function to get the absolute path of the root based
486+
// on the working directory
487+
if cfgDir != "" {
488+
r = filepath.Join(cfgDir, r)
489+
} else {
490+
ar, err := filepath.Abs(r)
491+
if err != nil {
492+
return nil, err
493+
}
494+
r = ar
495+
}
496+
}
497+
498+
// update the root to be an absolute path
499+
roots[i] = r
500+
501+
b, d := filepath.Base(r), filepath.Dir(r)
502+
503+
// if the base element is "..." then it means nested traversal is
504+
// activated. this can be passed directly to the loader. however, if
505+
// specified we also want to traverse the path manually to determine if
506+
// there are any nested Go modules we want to add to the list of roots
507+
// to process
508+
if b == "..." {
509+
if err := filepath.WalkDir(
510+
d,
511+
addNestedGoModulesToRoots); err != nil {
512+
513+
return nil, err
514+
}
515+
}
516+
}
517+
518+
// this range operation is parts 3-5 from above.
519+
for i := range roots {
520+
r := roots[i]
521+
522+
//fmt.Printf("2.processing root=%s\n", r)
523+
524+
// if the root is not a filesystem path then just load it directly
525+
// and skip to the next iteration of the loop
526+
if !isFSRoots.Has(i) {
527+
l.cfg.Dir = cfgDir
528+
if err := loadRoots(&uniqueRoots, r); err != nil {
529+
return nil, err
530+
}
531+
//fmt.Printf("2.skipping root=%s\n", r)
532+
continue
533+
}
534+
535+
b, d := filepath.Base(r), filepath.Dir(r)
536+
537+
// we want the base part of the path to be either "..." or ".", except
538+
// Go's filepath utilities clean paths during manipulation, removing the
539+
// ".". thus, if not "...", let's update the path components so that:
540+
//
541+
// d = r
542+
// b = "."
543+
if b != "..." {
544+
d = r
545+
b = "."
546+
}
547+
548+
// update the loader configuration's Dir field to the directory part of
549+
// the root
550+
l.cfg.Dir = d
551+
552+
// update the root to be "./..." or "./."
553+
// (with OS-specific filepath separator). please note filepath.Join
554+
// would clean up the trailing "." character that we want preserved,
555+
// hence the more manual path concatenation logic
556+
r = fmt.Sprintf(".%s%s", string(filepath.Separator), b)
557+
558+
// load the root
559+
if err := loadRoots(&uniqueRoots, r); err != nil {
560+
return nil, err
561+
}
351562
}
352563

353564
return l.Roots, nil

pkg/loader/loader_suite_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package loader_test
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo"
23+
. "github.com/onsi/gomega"
24+
)
25+
26+
func TestLoader(t *testing.T) {
27+
RegisterFailHandler(Fail)
28+
RunSpecs(t, "Loader Patching Suite")
29+
}

0 commit comments

Comments
 (0)