mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-11-03 23:40:03 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			422 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			422 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2018 The Kubernetes Authors.
 | 
						|
 | 
						|
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.
 | 
						|
*/
 | 
						|
 | 
						|
// do a fast type check of kubernetes code, for all platforms.
 | 
						|
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"flag"
 | 
						|
	"fmt"
 | 
						|
	"go/ast"
 | 
						|
	"go/build"
 | 
						|
	"go/parser"
 | 
						|
	"go/token"
 | 
						|
	"io"
 | 
						|
	"log"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"sort"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
	"sync/atomic"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"golang.org/x/crypto/ssh/terminal"
 | 
						|
	// TODO(rmmh): remove this when golang/go#23712 is fixed, and the
 | 
						|
	// fix is the current minimum Go version to build Kubernetes.
 | 
						|
	"k8s.io/kubernetes/test/typecheck/srcimporter"
 | 
						|
	"k8s.io/kubernetes/third_party/forked/golang/go/types"
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	verbose   = flag.Bool("verbose", false, "print more information")
 | 
						|
	cross     = flag.Bool("cross", true, "build for all platforms")
 | 
						|
	platforms = flag.String("platform", "", "comma-separated list of platforms to typecheck")
 | 
						|
	timings   = flag.Bool("time", false, "output times taken for each phase")
 | 
						|
	defuses   = flag.Bool("defuse", false, "output defs/uses")
 | 
						|
	serial    = flag.Bool("serial", false, "don't type check platforms in parallel")
 | 
						|
 | 
						|
	isTerminal = terminal.IsTerminal(int(os.Stdout.Fd()))
 | 
						|
	logPrefix  = ""
 | 
						|
 | 
						|
	// When processed in order, windows and darwin are early to make
 | 
						|
	// interesting OS-based errors happen earlier.
 | 
						|
	crossPlatforms = []string{
 | 
						|
		"linux/amd64", "windows/386",
 | 
						|
		"darwin/amd64", "linux/arm",
 | 
						|
		"linux/386", "windows/amd64",
 | 
						|
		"linux/arm64", "linux/ppc64le",
 | 
						|
		"linux/s390x", "darwin/386",
 | 
						|
	}
 | 
						|
	darwinPlatString  = "darwin/386,darwin/amd64"
 | 
						|
	windowsPlatString = "windows/386,windows/amd64"
 | 
						|
)
 | 
						|
 | 
						|
type analyzer struct {
 | 
						|
	fset      *token.FileSet // positions are relative to fset
 | 
						|
	conf      types.Config
 | 
						|
	ctx       build.Context
 | 
						|
	failed    bool
 | 
						|
	platform  string
 | 
						|
	donePaths map[string]interface{}
 | 
						|
	errors    []string
 | 
						|
}
 | 
						|
 | 
						|
func newAnalyzer(platform string) *analyzer {
 | 
						|
	ctx := build.Default
 | 
						|
	platSplit := strings.Split(platform, "/")
 | 
						|
	ctx.GOOS, ctx.GOARCH = platSplit[0], platSplit[1]
 | 
						|
	ctx.CgoEnabled = true
 | 
						|
 | 
						|
	a := &analyzer{
 | 
						|
		platform:  platform,
 | 
						|
		fset:      token.NewFileSet(),
 | 
						|
		ctx:       ctx,
 | 
						|
		donePaths: make(map[string]interface{}),
 | 
						|
	}
 | 
						|
	a.conf = types.Config{
 | 
						|
		FakeImportC: true,
 | 
						|
		Error:       a.handleError,
 | 
						|
		Sizes:       types.SizesFor("gc", a.ctx.GOARCH),
 | 
						|
	}
 | 
						|
 | 
						|
	a.conf.Importer = srcimporter.New(
 | 
						|
		&a.ctx, a.fset, make(map[string]*types.Package))
 | 
						|
 | 
						|
	if *verbose {
 | 
						|
		fmt.Printf("context: %#v\n", ctx)
 | 
						|
	}
 | 
						|
 | 
						|
	return a
 | 
						|
}
 | 
						|
 | 
						|
func (a *analyzer) handleError(err error) {
 | 
						|
	if e, ok := err.(types.Error); ok {
 | 
						|
		// useful for some ignores:
 | 
						|
		// path := e.Fset.Position(e.Pos).String()
 | 
						|
		ignore := false
 | 
						|
		// TODO(rmmh): read ignores from a file, so this code can
 | 
						|
		// be Kubernetes-agnostic. Unused ignores should be treated as
 | 
						|
		// errors, to ensure coverage isn't overly broad.
 | 
						|
		if strings.Contains(e.Msg, "GetOpenAPIDefinitions") {
 | 
						|
			// TODO(rmmh): figure out why this happens.
 | 
						|
			// cmd/kube-apiserver/app/server.go:392:70
 | 
						|
			// test/integration/framework/master_utils.go:131:84
 | 
						|
			ignore = true
 | 
						|
		}
 | 
						|
		if ignore {
 | 
						|
			if *verbose {
 | 
						|
				fmt.Println("ignoring error:", err)
 | 
						|
			}
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
	a.errors = append(a.errors, err.Error())
 | 
						|
	if *serial {
 | 
						|
		fmt.Fprintf(os.Stderr, "%sERROR(%s) %s\n", logPrefix, a.platform, err)
 | 
						|
	}
 | 
						|
	a.failed = true
 | 
						|
}
 | 
						|
 | 
						|
func (a *analyzer) dumpAndResetErrors() []string {
 | 
						|
	es := a.errors
 | 
						|
	a.errors = nil
 | 
						|
	return es
 | 
						|
}
 | 
						|
 | 
						|
// collect extracts test metadata from a file.
 | 
						|
func (a *analyzer) collect(dir string) {
 | 
						|
	if _, ok := a.donePaths[dir]; ok {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	a.donePaths[dir] = nil
 | 
						|
 | 
						|
	// Create the AST by parsing src.
 | 
						|
	fs, err := parser.ParseDir(a.fset, dir, nil, parser.AllErrors)
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		fmt.Println(logPrefix+"ERROR(syntax)", err)
 | 
						|
		a.failed = true
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if len(fs) > 1 && *verbose {
 | 
						|
		fmt.Println("multiple packages in dir:", dir)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, p := range fs {
 | 
						|
		// returns first error, but a.handleError deals with it
 | 
						|
		files := a.filterFiles(p.Files)
 | 
						|
		if *verbose {
 | 
						|
			fmt.Printf("path: %s package: %s files: ", dir, p.Name)
 | 
						|
			for _, f := range files {
 | 
						|
				fname := filepath.Base(a.fset.File(f.Pos()).Name())
 | 
						|
				fmt.Printf("%s ", fname)
 | 
						|
			}
 | 
						|
			fmt.Printf("\n")
 | 
						|
		}
 | 
						|
		a.typeCheck(dir, files)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// filterFiles restricts a list of files to only those that should be built by
 | 
						|
// the current platform. This includes both build suffixes (_windows.go) and build
 | 
						|
// tags ("// +build !linux" at the beginning).
 | 
						|
func (a *analyzer) filterFiles(fs map[string]*ast.File) []*ast.File {
 | 
						|
	files := []*ast.File{}
 | 
						|
	for _, f := range fs {
 | 
						|
		fpath := a.fset.File(f.Pos()).Name()
 | 
						|
		dir, name := filepath.Split(fpath)
 | 
						|
		matches, err := a.ctx.MatchFile(dir, name)
 | 
						|
		if err != nil {
 | 
						|
			fmt.Fprintf(os.Stderr, "%sERROR reading %s: %s\n", logPrefix, fpath, err)
 | 
						|
			a.failed = true
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if matches {
 | 
						|
			files = append(files, f)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return files
 | 
						|
}
 | 
						|
 | 
						|
func (a *analyzer) typeCheck(dir string, files []*ast.File) error {
 | 
						|
	info := types.Info{
 | 
						|
		Defs: make(map[*ast.Ident]types.Object),
 | 
						|
		Uses: make(map[*ast.Ident]types.Object),
 | 
						|
	}
 | 
						|
 | 
						|
	// NOTE: this type check does a *recursive* import, but srcimporter
 | 
						|
	// doesn't do a full type check (ignores function bodies)-- this has
 | 
						|
	// some additional overhead.
 | 
						|
	//
 | 
						|
	// This means that we need to ensure that typeCheck runs on all
 | 
						|
	// code we will be compiling.
 | 
						|
	//
 | 
						|
	// TODO(rmmh): Customize our forked srcimporter to do this better.
 | 
						|
	pkg, err := a.conf.Check(dir, a.fset, files, &info)
 | 
						|
	if err != nil {
 | 
						|
		return err // type error
 | 
						|
	}
 | 
						|
 | 
						|
	// A significant fraction of vendored code only compiles on Linux,
 | 
						|
	// but it's only imported by code that has build-guards for Linux.
 | 
						|
	// Track vendored code to type-check it in a second pass.
 | 
						|
	for _, imp := range pkg.Imports() {
 | 
						|
		if strings.HasPrefix(imp.Path(), "k8s.io/kubernetes/vendor/") {
 | 
						|
			vendorPath := imp.Path()[len("k8s.io/kubernetes/"):]
 | 
						|
			if *verbose {
 | 
						|
				fmt.Println("recursively checking vendor path:", vendorPath)
 | 
						|
			}
 | 
						|
			a.collect(vendorPath)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if *defuses {
 | 
						|
		for id, obj := range info.Defs {
 | 
						|
			fmt.Printf("%s: %q defines %v\n",
 | 
						|
				a.fset.Position(id.Pos()), id.Name, obj)
 | 
						|
		}
 | 
						|
		for id, obj := range info.Uses {
 | 
						|
			fmt.Printf("%s: %q uses %v\n",
 | 
						|
				a.fset.Position(id.Pos()), id.Name, obj)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
type collector struct {
 | 
						|
	dirs []string
 | 
						|
}
 | 
						|
 | 
						|
// handlePath walks the filesystem recursively, collecting directories,
 | 
						|
// ignoring some unneeded directories (hidden/vendored) that are handled
 | 
						|
// specially later.
 | 
						|
func (c *collector) handlePath(path string, info os.FileInfo, err error) error {
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	if info.IsDir() {
 | 
						|
		// Ignore hidden directories (.git, .cache, etc)
 | 
						|
		if len(path) > 1 && path[0] == '.' ||
 | 
						|
			// Staging code is symlinked from vendor/k8s.io, and uses import
 | 
						|
			// paths as if it were inside of vendor/. It fails typechecking
 | 
						|
			// inside of staging/, but works when typechecked as part of vendor/.
 | 
						|
			path == "staging" ||
 | 
						|
			// OS-specific vendor code tends to be imported by OS-specific
 | 
						|
			// packages. We recursively typecheck imported vendored packages for
 | 
						|
			// each OS, but don't typecheck everything for every OS.
 | 
						|
			path == "vendor" ||
 | 
						|
			path == "_output" ||
 | 
						|
			// This is a weird one. /testdata/ is *mostly* ignored by Go,
 | 
						|
			// and this translates to kubernetes/vendor not working.
 | 
						|
			// edit/record.go doesn't compile without gopkg.in/yaml.v2
 | 
						|
			// in $GOSRC/$GOROOT (both typecheck and the shell script).
 | 
						|
			path == "pkg/kubectl/cmd/testdata/edit" {
 | 
						|
			return filepath.SkipDir
 | 
						|
		}
 | 
						|
		c.dirs = append(c.dirs, path)
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
type analyzerResult struct {
 | 
						|
	platform string
 | 
						|
	dir      string
 | 
						|
	errors   []string
 | 
						|
}
 | 
						|
 | 
						|
func dedupeErrors(out io.Writer, results chan analyzerResult, nDirs, nPlatforms int) {
 | 
						|
	pkgRes := make(map[string][]analyzerResult)
 | 
						|
	for done := 0; done < nDirs; {
 | 
						|
		res := <-results
 | 
						|
		pkgRes[res.dir] = append(pkgRes[res.dir], res)
 | 
						|
		if len(pkgRes[res.dir]) != nPlatforms {
 | 
						|
			continue // expect more results for dir
 | 
						|
		}
 | 
						|
		done++
 | 
						|
		// Collect list of platforms for each error
 | 
						|
		errPlats := map[string][]string{}
 | 
						|
		for _, res := range pkgRes[res.dir] {
 | 
						|
			for _, err := range res.errors {
 | 
						|
				errPlats[err] = append(errPlats[err], res.platform)
 | 
						|
			}
 | 
						|
		}
 | 
						|
		// Print each error (in the same order!) once.
 | 
						|
		for _, res := range pkgRes[res.dir] {
 | 
						|
			for _, err := range res.errors {
 | 
						|
				if errPlats[err] == nil {
 | 
						|
					continue // already printed
 | 
						|
				}
 | 
						|
				sort.Strings(errPlats[err])
 | 
						|
				plats := strings.Join(errPlats[err], ",")
 | 
						|
				if len(errPlats[err]) == len(crossPlatforms) {
 | 
						|
					plats = "all"
 | 
						|
				} else if plats == darwinPlatString {
 | 
						|
					plats = "darwin"
 | 
						|
				} else if plats == windowsPlatString {
 | 
						|
					plats = "windows"
 | 
						|
				}
 | 
						|
				fmt.Fprintf(out, "%sERROR(%s) %s\n", logPrefix, plats, err)
 | 
						|
				delete(errPlats, err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
		delete(pkgRes, res.dir)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func main() {
 | 
						|
	flag.Parse()
 | 
						|
	args := flag.Args()
 | 
						|
 | 
						|
	if *verbose {
 | 
						|
		*serial = true // to avoid confusing interleaved logs
 | 
						|
	}
 | 
						|
 | 
						|
	if len(args) == 0 {
 | 
						|
		args = append(args, ".")
 | 
						|
	}
 | 
						|
 | 
						|
	c := collector{}
 | 
						|
	for _, arg := range args {
 | 
						|
		err := filepath.Walk(arg, c.handlePath)
 | 
						|
		if err != nil {
 | 
						|
			log.Fatalf("Error walking: %v", err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	sort.Strings(c.dirs)
 | 
						|
 | 
						|
	ps := crossPlatforms[:]
 | 
						|
	if *platforms != "" {
 | 
						|
		ps = strings.Split(*platforms, ",")
 | 
						|
	} else if !*cross {
 | 
						|
		ps = ps[:1]
 | 
						|
	}
 | 
						|
 | 
						|
	fmt.Println("type-checking: ", strings.Join(ps, ", "))
 | 
						|
 | 
						|
	var wg sync.WaitGroup
 | 
						|
	var processedDirs int64
 | 
						|
	var currentWork int64 // (dir_index << 8) | platform_index
 | 
						|
	statuses := make([]int, len(ps))
 | 
						|
	var results chan analyzerResult
 | 
						|
	if !*serial {
 | 
						|
		results = make(chan analyzerResult)
 | 
						|
		wg.Add(1)
 | 
						|
		go func() {
 | 
						|
			dedupeErrors(os.Stderr, results, len(c.dirs), len(ps))
 | 
						|
			wg.Done()
 | 
						|
		}()
 | 
						|
	}
 | 
						|
	for i, p := range ps {
 | 
						|
		wg.Add(1)
 | 
						|
		fn := func(i int, p string) {
 | 
						|
			start := time.Now()
 | 
						|
			a := newAnalyzer(p)
 | 
						|
			for n, dir := range c.dirs {
 | 
						|
				a.collect(dir)
 | 
						|
				atomic.AddInt64(&processedDirs, 1)
 | 
						|
				atomic.StoreInt64(¤tWork, int64(n<<8|i))
 | 
						|
				if results != nil {
 | 
						|
					results <- analyzerResult{p, dir, a.dumpAndResetErrors()}
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if a.failed {
 | 
						|
				statuses[i] = 1
 | 
						|
			}
 | 
						|
			if *timings {
 | 
						|
				fmt.Printf("%s took %.1fs\n", p, time.Since(start).Seconds())
 | 
						|
			}
 | 
						|
			wg.Done()
 | 
						|
		}
 | 
						|
		if *serial {
 | 
						|
			fn(i, p)
 | 
						|
		} else {
 | 
						|
			go fn(i, p)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if isTerminal {
 | 
						|
		logPrefix = "\r" // clear status bar when printing
 | 
						|
		// Display a status bar so devs can estimate completion times.
 | 
						|
		wg.Add(1)
 | 
						|
		go func() {
 | 
						|
			total := len(ps) * len(c.dirs)
 | 
						|
			for proc := 0; ; proc = int(atomic.LoadInt64(&processedDirs)) {
 | 
						|
				work := atomic.LoadInt64(¤tWork)
 | 
						|
				dir := c.dirs[work>>8]
 | 
						|
				platform := ps[work&0xFF]
 | 
						|
				if len(dir) > 80 {
 | 
						|
					dir = dir[:80]
 | 
						|
				}
 | 
						|
				fmt.Printf("\r%d/%d \033[2m%-13s\033[0m %-80s", proc, total, platform, dir)
 | 
						|
				if proc == total {
 | 
						|
					fmt.Println()
 | 
						|
					break
 | 
						|
				}
 | 
						|
				time.Sleep(50 * time.Millisecond)
 | 
						|
			}
 | 
						|
			wg.Done()
 | 
						|
		}()
 | 
						|
	}
 | 
						|
	wg.Wait()
 | 
						|
	for _, status := range statuses {
 | 
						|
		if status != 0 {
 | 
						|
			os.Exit(status)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |