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  }