Skip to content

[WIP] 🏃 External k8s.io References Checker #229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions hack/external-objects/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
External Imports Checker
========================

This tool knows how to check for external imports, to make sure that we
don't accidentally break ourselves because of Kubernetes. It's also
a general framework for doing this kind of thing, so it could be adapted
to general type signature checking, etc.

Usage
-----

### Printing all external references

This will print out all external references, plus their type signatures
(non-exported parts excluded on structs). You can diff this against the
output from a previous commit to check for differences.

```bash
$ go run ./*.go --root ../.. find
```

### Investigating a new reference

This will print out the "top-level" objects (functions, interfaces,
variables, etc) that eventually reference the given external type, as well
as the intermediate types to get there.

For instance, if controller-runtime method `Foo` references external
struct `Bar` that includes the target type `Baz`, it'll show the chain
from `Foo` to `Bar` to `Baz`.

```bash
$ go run ./*.go --root ../.. what-refs "k8s.io/some/other/package#SomeType"
```
60 changes: 60 additions & 0 deletions hack/external-objects/filter/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package filter

import (
"path"
"strings"

"sigs.k8s.io/controller-runtime/hack/external-objects/locate"
)

// PackageInfoResolver knows how to resolve package info for a
// particular path.
type PackageInfoResolver interface {
// PackageInfoFor resolves the package information for the
// given package path (can be a filesystem path) retrieved
// from a types.Package object imported by the source of this
// resolver. It may return nil for paths from other sources.
PackageInfoFor(path string) *locate.PackageInfo

// FetchPackageInfoFor resolves the package information for the
// the given path (like PackageInfoFor), but may also attempt to
// load basic package info if the given package info hasn't been
// cached.
FetchPackageInfoFor(path string) (*locate.PackageInfo, error)
}

// FilterEnvironment represents commonly required information for executing filters
type FilterEnvironment struct {
// RootPackage is the base package to which all filtered packages and objects belong
RootPackage *locate.PackageInfo

// Loader can be used to locate import path information
Loader PackageInfoResolver

// VendorImportPrefix is the import path prefix
// for the vendor directory (including the root package).
// If empty, it will be ignored.
VendorImportPrefix string
}

func NewFilterEnvironment(rootPackage *locate.PackageInfo, loader PackageInfoResolver, isProject bool) *FilterEnvironment {
vendorPrefix := ""
if isProject {
vendorPrefix = path.Join(rootPackage.BuildInfo.ImportPath, "vendor") + "/"
}

return &FilterEnvironment{
RootPackage: rootPackage,
Loader: loader,
VendorImportPrefix: vendorPrefix,
}
}

// StripVendor strips the vendor prefix from the given path, if set and applicable.
func (e *FilterEnvironment) StripVendor(importPath string) string {
if e.VendorImportPrefix != "" {
return strings.TrimPrefix(importPath, e.VendorImportPrefix)
}

return importPath
}
76 changes: 76 additions & 0 deletions hack/external-objects/filter/external.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package filter

import (
"go/types"
"strings"
"fmt"

"sigs.k8s.io/controller-runtime/hack/external-objects/process"
)

// FindExternalReferences returns all external references in the
// type signature of the given object, and all referenced types,
// in public types/fields/etc.
func FindExternalReferences(obj types.Object, env *FilterEnvironment) []*types.TypeName {
externalRefsRaw := process.FilterMapTypesInObject(obj, ExternalReferencesFilter(env))
externalRefs := make([]*types.TypeName, len(externalRefsRaw))
for i, ref := range externalRefsRaw {
externalRefs[i] = ref.(*types.TypeName)
}

return externalRefs
}

// ExternalReferencesFind returns a FilterMapFunc that finds all references to packages outside
// the given "root" package (and subpackages). The returned objects are *types.TypeName objects.
// The loader is used to figure out actual import paths for packages.
func ExternalReferencesFilter(env *FilterEnvironment) process.FilterMapFunc {
return func(typ types.Type) interface{} {
if typ == nil {
// we don't care about the end of a given type
return nil
}
ref := GetExternalReference(env, typ)
// avoid a present-but-nil-valued interface{} object
if ref == nil {
return nil
}
return ref
}
}

// GetExternalReference checks the given type to see if it's a named type, and if so,
// if that name refers to a package outside the given root package's package hierarchy.
// If so, it returns that name, otherwise returning nil. The given file loader is used
// to produce actual import paths for any type (since the package info might have relative
// filesystem paths, depending on where we're called from). Packages in the GoRoot
// (the Go standard library) are skipped.
func GetExternalReference(env *FilterEnvironment, typ types.Type) *types.TypeName {
namedType, isNamedType := typ.(*types.Named)
if !isNamedType {
return nil
}

typeName := namedType.Obj()

if typeName.Pkg() == nil {
// a built-in type like "error"
return nil
}

packageInfo, err := env.Loader.FetchPackageInfoFor(typeName.Pkg().Path())
if err != nil {
panic(fmt.Sprintf("unknown package %s while proccessing %s: %v", typeName.Pkg().Path(), typ, err))
return nil
}
if packageInfo.BuildInfo.Goroot {
// skip builtins
return nil
}
actualPackagePath := env.StripVendor(packageInfo.BuildInfo.ImportPath)
if strings.HasPrefix(actualPackagePath, env.RootPackage.BuildInfo.ImportPath) {
return nil
}

return typeName
}
82 changes: 82 additions & 0 deletions hack/external-objects/findall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/types"
"sort"
"strings"

"sigs.k8s.io/controller-runtime/hack/external-objects/filter"
"sigs.k8s.io/controller-runtime/hack/external-objects/locate"
)

var (
useSignatures = flag.Bool("with-sig", true, "print public type signatures when finding all references")
)

func findAll(loader *locate.FileLoader, allExternalRefs refsMap, filterEnv *filter.FilterEnvironment) {
// sort by package name, then type name
sortedRefs := make([]*types.TypeName, 0, len(allExternalRefs))
for ref := range allExternalRefs {
sortedRefs = append(sortedRefs, ref)
}
sort.Slice(sortedRefs, func(i, j int) bool {
refI := sortedRefs[i]
refJ := sortedRefs[j]
pkgInfoI := loader.PackageInfoFor(refI.Pkg().Path())
pkgInfoJ := loader.PackageInfoFor(refJ.Pkg().Path())
if pkgInfoI.BuildInfo.ImportPath == pkgInfoJ.BuildInfo.ImportPath {
return refI.Name() < refJ.Name()
}

return pkgInfoI.BuildInfo.ImportPath < pkgInfoJ.BuildInfo.ImportPath
})

// print all the sorted, deduped external types
for _, ref := range sortedRefs {
pkgInfo := loader.PackageInfoFor(ref.Pkg().Path())
importPath := filterEnv.StripVendor(pkgInfo.BuildInfo.ImportPath)
if *skipNonKube && !strings.HasPrefix(importPath, "k8s.io/") {
continue
}
fmt.Printf("%q.%s", importPath, ref.Name())
if *useSignatures {
fmt.Printf(" -- %s", typeSig(ref.Type().Underlying()))
}
fmt.Printf("\n")
}
}

func typeSig(ref types.Type) string {
// like types.TypeString, but only exported fields for structs (and no struct tags)
switch typ := ref.(type) {
case *types.Struct:
var buff bytes.Buffer
buff.WriteString("struct{")
for i := 0; i < typ.NumFields(); i++ {
field := typ.Field(i)
if !field.Embedded() && !ast.IsExported(field.Name()) {
continue
}
if i > 0 {
buff.WriteString("; ")
}
if !field.Embedded() {
if !ast.IsExported(field.Name()) {
continue
}
buff.WriteString(field.Name())
buff.WriteByte(' ')
}
buff.WriteString(typeSig(field.Type()))
}
buff.WriteString("}")
return buff.String()
default:
return ref.String()
}

}
Loading