k8s.io/kubernetes@v1.29.3/test/instrumentation/main.go (about) 1 /* 2 Copyright 2019 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 "bufio" 21 "flag" 22 "fmt" 23 "go/ast" 24 "go/parser" 25 "go/token" 26 "os" 27 "path/filepath" 28 "sort" 29 "strings" 30 31 "gopkg.in/yaml.v2" 32 ) 33 34 const ( 35 kubeMetricImportPath = `"k8s.io/component-base/metrics"` 36 // Should equal to final directory name of kubeMetricImportPath 37 kubeMetricsDefaultImportName = "metrics" 38 39 kubeURLRoot = "k8s.io/kubernetes/" 40 ) 41 42 var ( 43 // env configs 44 GOROOT string = os.Getenv("GOROOT") 45 GOOS string = os.Getenv("GOOS") 46 KUBE_ROOT string = os.Getenv("KUBE_ROOT") 47 ALL_STABILITY_CLASSES bool 48 ) 49 50 func main() { 51 52 flag.BoolVar(&ALL_STABILITY_CLASSES, "allstabilityclasses", false, "use this flag to enable all stability classes") 53 flag.Parse() 54 if len(flag.Args()) < 1 { 55 fmt.Fprintf(os.Stderr, "USAGE: %s <DIR or FILE or '-'> [...]\n", os.Args[0]) 56 os.Exit(64) 57 } 58 stableMetricNames := map[string]struct{}{} 59 stableMetrics := []metric{} 60 errors := []error{} 61 62 addStdin := false 63 for _, arg := range flag.Args() { 64 if arg == "-" { 65 addStdin = true 66 continue 67 } 68 ms, es := searchPathForStableMetrics(arg) 69 for _, m := range ms { 70 if _, ok := stableMetricNames[m.Name]; !ok { 71 stableMetrics = append(stableMetrics, m) 72 } 73 stableMetricNames[m.Name] = struct{}{} 74 } 75 errors = append(errors, es...) 76 } 77 if addStdin { 78 scanner := bufio.NewScanner(os.Stdin) 79 scanner.Split(bufio.ScanLines) 80 for scanner.Scan() { 81 arg := scanner.Text() 82 ms, es := searchPathForStableMetrics(arg) 83 stableMetrics = append(stableMetrics, ms...) 84 errors = append(errors, es...) 85 } 86 } 87 88 for _, err := range errors { 89 fmt.Fprintf(os.Stderr, "%s\n", err) 90 } 91 if len(errors) != 0 { 92 os.Exit(1) 93 } 94 if len(stableMetrics) == 0 { 95 os.Exit(0) 96 } 97 for i, m := range stableMetrics { 98 if m.StabilityLevel == "" { 99 m.StabilityLevel = "ALPHA" 100 } 101 stableMetrics[i] = m 102 } 103 sort.Sort(byFQName(stableMetrics)) 104 data, err := yaml.Marshal(stableMetrics) 105 if err != nil { 106 fmt.Fprintf(os.Stderr, "%s\n", err) 107 os.Exit(1) 108 } 109 110 fmt.Print(string(data)) 111 } 112 113 func searchPathForStableMetrics(path string) ([]metric, []error) { 114 metrics := []metric{} 115 errors := []error{} 116 err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 117 if strings.HasPrefix(path, "vendor") { 118 return filepath.SkipDir 119 } 120 if !strings.HasSuffix(path, ".go") { 121 return nil 122 } 123 ms, es := searchFileForStableMetrics(path, nil) 124 errors = append(errors, es...) 125 metrics = append(metrics, ms...) 126 return nil 127 }) 128 if err != nil { 129 errors = append(errors, err) 130 } 131 return metrics, errors 132 } 133 134 // Pass either only filename of existing file or src including source code in any format and a filename that it comes from 135 func searchFileForStableMetrics(filename string, src interface{}) ([]metric, []error) { 136 fileset := token.NewFileSet() 137 tree, err := parser.ParseFile(fileset, filename, src, parser.AllErrors) 138 if err != nil { 139 return []metric{}, []error{err} 140 } 141 metricsImportName, err := getLocalNameOfImportedPackage(tree, kubeMetricImportPath, kubeMetricsDefaultImportName) 142 if err != nil { 143 return []metric{}, addFileInformationToErrors([]error{err}, fileset) 144 } 145 if metricsImportName == "" { 146 return []metric{}, []error{} 147 } 148 variables := globalVariableDeclarations(tree) 149 150 variables, err = importedGlobalVariableDeclaration(variables, tree.Imports) 151 if err != nil { 152 return []metric{}, addFileInformationToErrors([]error{err}, fileset) 153 } 154 155 stableMetricsFunctionCalls, errors := findStableMetricDeclaration(tree, metricsImportName) 156 metrics, es := decodeMetricCalls(stableMetricsFunctionCalls, metricsImportName, variables) 157 errors = append(errors, es...) 158 return metrics, addFileInformationToErrors(errors, fileset) 159 } 160 161 func getLocalNameOfImportedPackage(tree *ast.File, importPath, defaultImportName string) (string, error) { 162 var importName string 163 for _, im := range tree.Imports { 164 if im.Path.Value == importPath { 165 if im.Name == nil { 166 importName = defaultImportName 167 } else { 168 if im.Name.Name == "." { 169 return "", newDecodeErrorf(im, errImport) 170 } 171 importName = im.Name.Name 172 } 173 } 174 } 175 return importName, nil 176 } 177 178 func addFileInformationToErrors(es []error, fileset *token.FileSet) []error { 179 for i := range es { 180 if de, ok := es[i].(*decodeError); ok { 181 es[i] = de.errorWithFileInformation(fileset) 182 } 183 } 184 return es 185 } 186 187 func globalVariableDeclarations(tree *ast.File) map[string]ast.Expr { 188 consts := make(map[string]ast.Expr) 189 for _, d := range tree.Decls { 190 if gd, ok := d.(*ast.GenDecl); ok && (gd.Tok == token.CONST || gd.Tok == token.VAR) { 191 for _, spec := range gd.Specs { 192 if vspec, ok := spec.(*ast.ValueSpec); ok { 193 for _, name := range vspec.Names { 194 for _, value := range vspec.Values { 195 consts[name.Name] = value 196 } 197 } 198 } 199 } 200 } 201 } 202 return consts 203 } 204 205 func localImportPath(importExpr string) (string, error) { 206 // parse directory path 207 var pathPrefix string 208 if strings.Contains(importExpr, kubeURLRoot) { 209 // search k/k local checkout 210 pathPrefix = KUBE_ROOT 211 importExpr = strings.Replace(importExpr, kubeURLRoot, "", 1) 212 } else if strings.Contains(importExpr, "k8s.io/klog/v2") || strings.Contains(importExpr, "k8s.io/util") { 213 pathPrefix = strings.Join([]string{KUBE_ROOT, "vendor"}, string(os.PathSeparator)) 214 } else if strings.Contains(importExpr, "k8s.io/") { 215 // search k/k/staging local checkout 216 pathPrefix = strings.Join([]string{KUBE_ROOT, "staging", "src"}, string(os.PathSeparator)) 217 } else if strings.Contains(importExpr, ".") { 218 // not stdlib -> prefix with GOMODCACHE 219 // pathPrefix = strings.Join([]string{KUBE_ROOT, "vendor"}, string(os.PathSeparator)) 220 221 // this requires implementing SIV, skip for now 222 return "", fmt.Errorf("unable to handle general, non STL imports for metric analysis. import path: %s", importExpr) 223 } else { 224 // stdlib -> prefix with GOROOT 225 pathPrefix = strings.Join([]string{GOROOT, "src"}, string(os.PathSeparator)) 226 } // ToDo: support non go mod 227 228 crossPlatformImportExpr := strings.Replace(importExpr, "/", string(os.PathSeparator), -1) 229 importDirectory := strings.Join([]string{pathPrefix, strings.Trim(crossPlatformImportExpr, "\"")}, string(os.PathSeparator)) 230 231 return importDirectory, nil 232 } 233 234 func importedGlobalVariableDeclaration(localVariables map[string]ast.Expr, imports []*ast.ImportSpec) (map[string]ast.Expr, error) { 235 for _, im := range imports { 236 // get imported label 237 var importAlias string 238 if im.Name == nil { 239 pathSegments := strings.Split(im.Path.Value, "/") 240 importAlias = strings.Trim(pathSegments[len(pathSegments)-1], "\"") 241 } else { 242 importAlias = im.Name.String() 243 } 244 245 // find local path on disk for listed import 246 importDirectory, err := localImportPath(im.Path.Value) 247 if err != nil { 248 // uncomment the below log line if you want to start using non k8s/non stl libs for resolving const/var in metric definitions 249 // fmt.Fprint(os.Stderr, err.Error() + "\n") 250 continue 251 } 252 253 files, err := os.ReadDir(importDirectory) 254 if err != nil { 255 fmt.Fprintf(os.Stderr, "failed to read import path directory %s with error %s, skipping\n", importDirectory, err) 256 continue 257 } 258 259 for _, file := range files { 260 if file.IsDir() { 261 // do not grab constants from subpackages 262 continue 263 } 264 265 if strings.Contains(file.Name(), "_test") { 266 // do not parse test files 267 continue 268 } 269 270 if !strings.HasSuffix(file.Name(), ".go") { 271 // not a go code file, do not attempt to parse 272 continue 273 } 274 275 fileset := token.NewFileSet() 276 tree, err := parser.ParseFile(fileset, strings.Join([]string{importDirectory, file.Name()}, string(os.PathSeparator)), nil, parser.AllErrors) 277 if err != nil { 278 return nil, fmt.Errorf("failed to parse path %s with error %w", im.Path.Value, err) 279 } 280 281 // pass parsed filepath into globalVariableDeclarations 282 variables := globalVariableDeclarations(tree) 283 284 // add returned map into supplied map and prepend import label to all keys 285 for k, v := range variables { 286 importK := strings.Join([]string{importAlias, k}, ".") 287 if _, ok := localVariables[importK]; !ok { 288 localVariables[importK] = v 289 } else { 290 // cross-platform file that gets included in the correct OS build via OS build tags 291 // use whatever matches GOOS 292 293 if strings.Contains(file.Name(), GOOS) { 294 // assume at some point we will find the correct OS version of this file 295 // if we are running on an OS that does not have an OS specific file for something then we will include a constant we shouldn't 296 // TODO: should we include/exclude based on the build tags? 297 localVariables[importK] = v 298 } 299 300 } 301 } 302 } 303 304 } 305 306 return localVariables, nil 307 }