importverifier.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. /*
  2. Copyright 2017 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package main
  14. import (
  15. "bytes"
  16. "encoding/json"
  17. "fmt"
  18. "io"
  19. "io/ioutil"
  20. "log"
  21. "os"
  22. "os/exec"
  23. "path/filepath"
  24. "strings"
  25. "gopkg.in/yaml.v2"
  26. )
  27. // Package is a subset of cmd/go.Package
  28. type Package struct {
  29. Dir string `yaml:",omitempty"` // directory containing package sources
  30. ImportPath string `yaml:",omitempty"` // import path of package in dir
  31. Imports []string `yaml:",omitempty"` // import paths used by this package
  32. TestImports []string `yaml:",omitempty"` // imports from TestGoFiles
  33. XTestImports []string `yaml:",omitempty"` // imports from XTestGoFiles
  34. }
  35. // ImportRestriction describes a set of allowable import
  36. // trees for a tree of source code
  37. type ImportRestriction struct {
  38. // BaseDir is the root of the package tree that is
  39. // restricted by this configuration, given as a
  40. // relative path from the root of the repository
  41. BaseDir string `yaml:"baseImportPath"`
  42. // IgnoredSubTrees are roots of sub-trees of the
  43. // BaseDir for which we do not want to enforce
  44. // any import restrictions whatsoever, given as
  45. // relative paths from the root of the repository
  46. IgnoredSubTrees []string `yaml:"ignoredSubTrees,omitempty"`
  47. // AllowedImports are roots of package trees that
  48. // are allowed to be imported from the BaseDir,
  49. // given as paths that would be used in a Go
  50. // import statement
  51. AllowedImports []string `yaml:"allowedImports"`
  52. // ExcludeTests will skip checking test dependencies.
  53. ExcludeTests bool `yaml:"excludeTests"`
  54. }
  55. // ForbiddenImportsFor determines all of the forbidden
  56. // imports for a package given the import restrictions
  57. func (i *ImportRestriction) ForbiddenImportsFor(pkg Package) ([]string, error) {
  58. if restricted, err := i.isRestrictedDir(pkg.Dir); err != nil {
  59. return []string{}, err
  60. } else if !restricted {
  61. return []string{}, nil
  62. }
  63. return i.forbiddenImportsFor(pkg), nil
  64. }
  65. // isRestrictedDir determines if the source directory has
  66. // any restrictions placed on it by this configuration.
  67. // A path will be restricted if:
  68. // - it falls under the base import path
  69. // - it does not fall under any of the ignored sub-trees
  70. func (i *ImportRestriction) isRestrictedDir(dir string) (bool, error) {
  71. if under, err := isPathUnder(i.BaseDir, dir); err != nil {
  72. return false, err
  73. } else if !under {
  74. return false, nil
  75. }
  76. for _, ignored := range i.IgnoredSubTrees {
  77. if under, err := isPathUnder(ignored, dir); err != nil {
  78. return false, err
  79. } else if under {
  80. return false, nil
  81. }
  82. }
  83. return true, nil
  84. }
  85. // isPathUnder determines if path is under base
  86. func isPathUnder(base, path string) (bool, error) {
  87. absBase, err := filepath.Abs(base)
  88. if err != nil {
  89. return false, err
  90. }
  91. absPath, err := filepath.Abs(path)
  92. if err != nil {
  93. return false, err
  94. }
  95. relPath, err := filepath.Rel(absBase, absPath)
  96. if err != nil {
  97. return false, err
  98. }
  99. // if path is below base, the relative path
  100. // from base to path will not start with `../`
  101. return !strings.HasPrefix(relPath, ".."), nil
  102. }
  103. // forbiddenImportsFor determines all of the forbidden
  104. // imports for a package given the import restrictions
  105. // and returns a deduplicated list of them
  106. func (i *ImportRestriction) forbiddenImportsFor(pkg Package) []string {
  107. forbiddenImportSet := map[string]struct{}{}
  108. imports := pkg.Imports
  109. if !i.ExcludeTests {
  110. imports = append(imports, append(pkg.TestImports, pkg.XTestImports...)...)
  111. }
  112. for _, imp := range imports {
  113. path := extractVendorPath(imp)
  114. if i.isForbidden(path) {
  115. forbiddenImportSet[path] = struct{}{}
  116. }
  117. }
  118. var forbiddenImports []string
  119. for imp := range forbiddenImportSet {
  120. forbiddenImports = append(forbiddenImports, imp)
  121. }
  122. return forbiddenImports
  123. }
  124. // extractVendorPath removes a vendor prefix if one exists
  125. func extractVendorPath(path string) string {
  126. vendorPath := "/vendor/"
  127. if !strings.Contains(path, vendorPath) {
  128. return path
  129. }
  130. return path[strings.Index(path, vendorPath)+len(vendorPath):]
  131. }
  132. // isForbidden determines if an import is forbidden,
  133. // which is true when the import is:
  134. // - of a package under the rootPackage
  135. // - is not of the base import path or a sub-package of it
  136. // - is not of an allowed path or a sub-package of one
  137. func (i *ImportRestriction) isForbidden(imp string) bool {
  138. importsBelowRoot := strings.HasPrefix(imp, rootPackage)
  139. importsBelowBase := strings.HasPrefix(imp, i.BaseDir)
  140. importsAllowed := false
  141. for _, allowed := range i.AllowedImports {
  142. exactlyImportsAllowed := imp == allowed
  143. importsBelowAllowed := strings.HasPrefix(imp, fmt.Sprintf("%s/", allowed))
  144. importsAllowed = importsAllowed || (importsBelowAllowed || exactlyImportsAllowed)
  145. }
  146. return importsBelowRoot && !importsBelowBase && !importsAllowed
  147. }
  148. var rootPackage string
  149. func main() {
  150. if len(os.Args) != 3 {
  151. log.Fatalf("Usage: %s ROOT RESTRICTIONS.yaml", os.Args[0])
  152. }
  153. rootPackage = os.Args[1]
  154. configFile := os.Args[2]
  155. importRestrictions, err := loadImportRestrictions(configFile)
  156. if err != nil {
  157. log.Fatalf("Failed to load import restrictions: %v", err)
  158. }
  159. foundForbiddenImports := false
  160. for _, restriction := range importRestrictions {
  161. log.Printf("Inspecting imports under %s...\n", restriction.BaseDir)
  162. packages, err := resolvePackageTree(restriction.BaseDir)
  163. if err != nil {
  164. log.Fatalf("Failed to resolve package tree: %v", err)
  165. } else if len(packages) == 0 {
  166. log.Fatalf("Found no packages under tree %s", restriction.BaseDir)
  167. }
  168. log.Printf("- validating imports for %d packages in the tree", len(packages))
  169. restrictionViolated := false
  170. for _, pkg := range packages {
  171. if forbidden, err := restriction.ForbiddenImportsFor(pkg); err != nil {
  172. log.Fatalf("-- failed to validate imports: %v", err)
  173. } else if len(forbidden) != 0 {
  174. logForbiddenPackages(pkg.ImportPath, forbidden)
  175. restrictionViolated = true
  176. }
  177. }
  178. if restrictionViolated {
  179. foundForbiddenImports = true
  180. log.Println("- FAIL")
  181. } else {
  182. log.Println("- OK")
  183. }
  184. }
  185. if foundForbiddenImports {
  186. os.Exit(1)
  187. }
  188. }
  189. func loadImportRestrictions(configFile string) ([]ImportRestriction, error) {
  190. config, err := ioutil.ReadFile(configFile)
  191. if err != nil {
  192. return nil, fmt.Errorf("failed to load configuration from %s: %v", configFile, err)
  193. }
  194. var importRestrictions []ImportRestriction
  195. if err := yaml.Unmarshal(config, &importRestrictions); err != nil {
  196. return nil, fmt.Errorf("failed to unmarshal from %s: %v", configFile, err)
  197. }
  198. return importRestrictions, nil
  199. }
  200. func resolvePackageTree(treeBase string) ([]Package, error) {
  201. cmd := "go"
  202. args := []string{"list", "-json", fmt.Sprintf("%s...", treeBase)}
  203. stdout, err := exec.Command(cmd, args...).Output()
  204. if err != nil {
  205. var message string
  206. if ee, ok := err.(*exec.ExitError); ok {
  207. message = fmt.Sprintf("%v\n%v", ee, string(ee.Stderr))
  208. } else {
  209. message = fmt.Sprintf("%v", err)
  210. }
  211. return nil, fmt.Errorf("failed to run `%s %s`: %v", cmd, strings.Join(args, " "), message)
  212. }
  213. packages, err := decodePackages(bytes.NewReader(stdout))
  214. if err != nil {
  215. return nil, fmt.Errorf("failed to decode packages: %v", err)
  216. }
  217. return packages, nil
  218. }
  219. func decodePackages(r io.Reader) ([]Package, error) {
  220. // `go list -json` concatenates package definitions
  221. // instead of emitting a single valid JSON, so we
  222. // need to stream the output to decode it into the
  223. // data we are looking for instead of just using a
  224. // simple JSON decoder on stdout
  225. var packages []Package
  226. decoder := json.NewDecoder(r)
  227. for decoder.More() {
  228. var pkg Package
  229. if err := decoder.Decode(&pkg); err != nil {
  230. return nil, fmt.Errorf("invalid package: %v", err)
  231. }
  232. packages = append(packages, pkg)
  233. }
  234. return packages, nil
  235. }
  236. func logForbiddenPackages(base string, forbidden []string) {
  237. log.Printf("-- found forbidden imports for %s:\n", base)
  238. for _, forbiddenPackage := range forbidden {
  239. log.Printf("--- %s\n", forbiddenPackage)
  240. }
  241. }