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