blob: c73dad68c2a5fc23c28a2f05cf53f8a034263ee3 [file] [log] [blame]
/*
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)
}
}