123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- /*
- Copyright 2017 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.
- */
- package main
- import (
- "bytes"
- "encoding/json"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "gopkg.in/yaml.v2"
- )
- // Package is a subset of cmd/go.Package
- type Package struct {
- Dir string `yaml:",omitempty"` // directory containing package sources
- ImportPath string `yaml:",omitempty"` // import path of package in dir
- Imports []string `yaml:",omitempty"` // import paths used by this package
- TestImports []string `yaml:",omitempty"` // imports from TestGoFiles
- XTestImports []string `yaml:",omitempty"` // imports from XTestGoFiles
- }
- // ImportRestriction describes a set of allowable import
- // trees for a tree of source code
- type ImportRestriction struct {
- // BaseDir is the root of the package tree that is
- // restricted by this configuration, given as a
- // relative path from the root of the repository
- BaseDir string `yaml:"baseImportPath"`
- // IgnoredSubTrees are roots of sub-trees of the
- // BaseDir for which we do not want to enforce
- // any import restrictions whatsoever, given as
- // relative paths from the root of the repository
- IgnoredSubTrees []string `yaml:"ignoredSubTrees,omitempty"`
- // AllowedImports are roots of package trees that
- // are allowed to be imported from the BaseDir,
- // given as paths that would be used in a Go
- // import statement
- AllowedImports []string `yaml:"allowedImports"`
- // ExcludeTests will skip checking test dependencies.
- ExcludeTests bool `yaml:"excludeTests"`
- }
- // ForbiddenImportsFor determines all of the forbidden
- // imports for a package given the import restrictions
- func (i *ImportRestriction) ForbiddenImportsFor(pkg Package) ([]string, error) {
- if restricted, err := i.isRestrictedDir(pkg.Dir); err != nil {
- return []string{}, err
- } else if !restricted {
- return []string{}, nil
- }
- return i.forbiddenImportsFor(pkg), nil
- }
- // isRestrictedDir determines if the source directory has
- // any restrictions placed on it by this configuration.
- // A path will be restricted if:
- // - it falls under the base import path
- // - it does not fall under any of the ignored sub-trees
- func (i *ImportRestriction) isRestrictedDir(dir string) (bool, error) {
- if under, err := isPathUnder(i.BaseDir, dir); err != nil {
- return false, err
- } else if !under {
- return false, nil
- }
- for _, ignored := range i.IgnoredSubTrees {
- if under, err := isPathUnder(ignored, dir); err != nil {
- return false, err
- } else if under {
- return false, nil
- }
- }
- return true, nil
- }
- // isPathUnder determines if path is under base
- func isPathUnder(base, path string) (bool, error) {
- absBase, err := filepath.Abs(base)
- if err != nil {
- return false, err
- }
- absPath, err := filepath.Abs(path)
- if err != nil {
- return false, err
- }
- relPath, err := filepath.Rel(absBase, absPath)
- if err != nil {
- return false, err
- }
- // if path is below base, the relative path
- // from base to path will not start with `../`
- return !strings.HasPrefix(relPath, ".."), nil
- }
- // forbiddenImportsFor determines all of the forbidden
- // imports for a package given the import restrictions
- // and returns a deduplicated list of them
- func (i *ImportRestriction) forbiddenImportsFor(pkg Package) []string {
- forbiddenImportSet := map[string]struct{}{}
- imports := pkg.Imports
- if !i.ExcludeTests {
- imports = append(imports, append(pkg.TestImports, pkg.XTestImports...)...)
- }
- for _, imp := range imports {
- path := extractVendorPath(imp)
- if i.isForbidden(path) {
- forbiddenImportSet[path] = struct{}{}
- }
- }
- var forbiddenImports []string
- for imp := range forbiddenImportSet {
- forbiddenImports = append(forbiddenImports, imp)
- }
- return forbiddenImports
- }
- // extractVendorPath removes a vendor prefix if one exists
- func extractVendorPath(path string) string {
- vendorPath := "/vendor/"
- if !strings.Contains(path, vendorPath) {
- return path
- }
- return path[strings.Index(path, vendorPath)+len(vendorPath):]
- }
- // isForbidden determines if an import is forbidden,
- // which is true when the import is:
- // - of a package under the rootPackage
- // - is not of the base import path or a sub-package of it
- // - is not of an allowed path or a sub-package of one
- func (i *ImportRestriction) isForbidden(imp string) bool {
- importsBelowRoot := strings.HasPrefix(imp, rootPackage)
- importsBelowBase := strings.HasPrefix(imp, i.BaseDir)
- importsAllowed := false
- for _, allowed := range i.AllowedImports {
- exactlyImportsAllowed := imp == allowed
- importsBelowAllowed := strings.HasPrefix(imp, fmt.Sprintf("%s/", allowed))
- importsAllowed = importsAllowed || (importsBelowAllowed || exactlyImportsAllowed)
- }
- return importsBelowRoot && !importsBelowBase && !importsAllowed
- }
- var rootPackage string
- func main() {
- if len(os.Args) != 3 {
- log.Fatalf("Usage: %s ROOT RESTRICTIONS.yaml", os.Args[0])
- }
- rootPackage = os.Args[1]
- configFile := os.Args[2]
- importRestrictions, err := loadImportRestrictions(configFile)
- if err != nil {
- log.Fatalf("Failed to load import restrictions: %v", err)
- }
- foundForbiddenImports := false
- for _, restriction := range importRestrictions {
- log.Printf("Inspecting imports under %s...\n", restriction.BaseDir)
- packages, err := resolvePackageTree(restriction.BaseDir)
- if err != nil {
- log.Fatalf("Failed to resolve package tree: %v", err)
- } else if len(packages) == 0 {
- log.Fatalf("Found no packages under tree %s", restriction.BaseDir)
- }
- log.Printf("- validating imports for %d packages in the tree", len(packages))
- restrictionViolated := false
- for _, pkg := range packages {
- if forbidden, err := restriction.ForbiddenImportsFor(pkg); err != nil {
- log.Fatalf("-- failed to validate imports: %v", err)
- } else if len(forbidden) != 0 {
- logForbiddenPackages(pkg.ImportPath, forbidden)
- restrictionViolated = true
- }
- }
- if restrictionViolated {
- foundForbiddenImports = true
- log.Println("- FAIL")
- } else {
- log.Println("- OK")
- }
- }
- if foundForbiddenImports {
- os.Exit(1)
- }
- }
- func loadImportRestrictions(configFile string) ([]ImportRestriction, error) {
- config, err := ioutil.ReadFile(configFile)
- if err != nil {
- return nil, fmt.Errorf("failed to load configuration from %s: %v", configFile, err)
- }
- var importRestrictions []ImportRestriction
- if err := yaml.Unmarshal(config, &importRestrictions); err != nil {
- return nil, fmt.Errorf("failed to unmarshal from %s: %v", configFile, err)
- }
- return importRestrictions, nil
- }
- func resolvePackageTree(treeBase string) ([]Package, error) {
- cmd := "go"
- args := []string{"list", "-json", fmt.Sprintf("%s...", treeBase)}
- stdout, err := exec.Command(cmd, args...).Output()
- if err != nil {
- var message string
- if ee, ok := err.(*exec.ExitError); ok {
- message = fmt.Sprintf("%v\n%v", ee, string(ee.Stderr))
- } else {
- message = fmt.Sprintf("%v", err)
- }
- return nil, fmt.Errorf("failed to run `%s %s`: %v", cmd, strings.Join(args, " "), message)
- }
- packages, err := decodePackages(bytes.NewReader(stdout))
- if err != nil {
- return nil, fmt.Errorf("failed to decode packages: %v", err)
- }
- return packages, nil
- }
- func decodePackages(r io.Reader) ([]Package, error) {
- // `go list -json` concatenates package definitions
- // instead of emitting a single valid JSON, so we
- // need to stream the output to decode it into the
- // data we are looking for instead of just using a
- // simple JSON decoder on stdout
- var packages []Package
- decoder := json.NewDecoder(r)
- for decoder.More() {
- var pkg Package
- if err := decoder.Decode(&pkg); err != nil {
- return nil, fmt.Errorf("invalid package: %v", err)
- }
- packages = append(packages, pkg)
- }
- return packages, nil
- }
- func logForbiddenPackages(base string, forbidden []string) {
- log.Printf("-- found forbidden imports for %s:\n", base)
- for _, forbiddenPackage := range forbidden {
- log.Printf("--- %s\n", forbiddenPackage)
- }
- }
|