1
16
17 package main
18
19 import (
20 "bufio"
21 "encoding/json"
22 "flag"
23 "fmt"
24 "go/ast"
25 "go/parser"
26 "go/token"
27 "os"
28 "os/exec"
29 "path/filepath"
30 "sort"
31 "strconv"
32 "strings"
33
34 "gopkg.in/yaml.v2"
35 )
36
37 const (
38 kubeMetricImportPath = `"k8s.io/component-base/metrics"`
39
40 kubeMetricsDefaultImportName = "metrics"
41 )
42
43 var (
44
45 GOOS string = findGOOS()
46 ALL_STABILITY_CLASSES bool
47 )
48
49 func findGOOS() string {
50 cmd := exec.Command("go", "env", "GOOS")
51 out, err := cmd.CombinedOutput()
52 if err != nil {
53 panic(fmt.Sprintf("running `go env` failed: %v\n\n%s", err, string(out)))
54 }
55 if len(out) == 0 {
56 panic("empty result from `go env GOOS`")
57 }
58 return string(out)
59 }
60
61 func main() {
62
63 flag.BoolVar(&ALL_STABILITY_CLASSES, "allstabilityclasses", false, "use this flag to enable all stability classes")
64 flag.Parse()
65 if len(flag.Args()) < 1 {
66 fmt.Fprintf(os.Stderr, "USAGE: %s <DIR or FILE or '-'> [...]\n", os.Args[0])
67 os.Exit(64)
68 }
69 stableMetricNames := map[string]struct{}{}
70 stableMetrics := []metric{}
71 errors := []error{}
72
73 addStdin := false
74 for _, arg := range flag.Args() {
75 if arg == "-" {
76 addStdin = true
77 continue
78 }
79 ms, es := searchPathForStableMetrics(arg)
80 for _, m := range ms {
81 if _, ok := stableMetricNames[m.Name]; !ok {
82 stableMetrics = append(stableMetrics, m)
83 }
84 stableMetricNames[m.Name] = struct{}{}
85 }
86 errors = append(errors, es...)
87 }
88 if addStdin {
89 scanner := bufio.NewScanner(os.Stdin)
90 scanner.Split(bufio.ScanLines)
91 for scanner.Scan() {
92 arg := scanner.Text()
93 ms, es := searchPathForStableMetrics(arg)
94 stableMetrics = append(stableMetrics, ms...)
95 errors = append(errors, es...)
96 }
97 }
98
99 for _, err := range errors {
100 fmt.Fprintf(os.Stderr, "%s\n", err)
101 }
102 if len(errors) != 0 {
103 os.Exit(1)
104 }
105 if len(stableMetrics) == 0 {
106 os.Exit(0)
107 }
108 for i, m := range stableMetrics {
109 if m.StabilityLevel == "" {
110 m.StabilityLevel = "ALPHA"
111 }
112 stableMetrics[i] = m
113 }
114 sort.Sort(byFQName(stableMetrics))
115 data, err := yaml.Marshal(stableMetrics)
116 if err != nil {
117 fmt.Fprintf(os.Stderr, "%s\n", err)
118 os.Exit(1)
119 }
120
121 fmt.Print(string(data))
122 }
123
124 func searchPathForStableMetrics(path string) ([]metric, []error) {
125 metrics := []metric{}
126 errors := []error{}
127 err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
128 if strings.HasPrefix(path, "vendor") {
129 return filepath.SkipDir
130 }
131 if !strings.HasSuffix(path, ".go") {
132 return nil
133 }
134 ms, es := searchFileForStableMetrics(path, nil)
135 errors = append(errors, es...)
136 metrics = append(metrics, ms...)
137 return nil
138 })
139 if err != nil {
140 errors = append(errors, err)
141 }
142 return metrics, errors
143 }
144
145
146 func searchFileForStableMetrics(filename string, src interface{}) ([]metric, []error) {
147 fileset := token.NewFileSet()
148 tree, err := parser.ParseFile(fileset, filename, src, parser.AllErrors)
149 if err != nil {
150 return []metric{}, []error{err}
151 }
152 metricsImportName, err := getLocalNameOfImportedPackage(tree, kubeMetricImportPath, kubeMetricsDefaultImportName)
153 if err != nil {
154 return []metric{}, addFileInformationToErrors([]error{err}, fileset)
155 }
156 if metricsImportName == "" {
157 return []metric{}, []error{}
158 }
159 variables := globalVariableDeclarations(tree)
160
161 variables, err = importedGlobalVariableDeclaration(variables, tree.Imports)
162 if err != nil {
163 return []metric{}, addFileInformationToErrors([]error{err}, fileset)
164 }
165
166 stableMetricsFunctionCalls, errors := findStableMetricDeclaration(tree, metricsImportName)
167 metrics, es := decodeMetricCalls(stableMetricsFunctionCalls, metricsImportName, variables)
168 errors = append(errors, es...)
169 return metrics, addFileInformationToErrors(errors, fileset)
170 }
171
172 func getLocalNameOfImportedPackage(tree *ast.File, importPath, defaultImportName string) (string, error) {
173 var importName string
174 for _, im := range tree.Imports {
175 if im.Path.Value == importPath {
176 if im.Name == nil {
177 importName = defaultImportName
178 } else {
179 if im.Name.Name == "." {
180 return "", newDecodeErrorf(im, errImport)
181 }
182 importName = im.Name.Name
183 }
184 }
185 }
186 return importName, nil
187 }
188
189 func addFileInformationToErrors(es []error, fileset *token.FileSet) []error {
190 for i := range es {
191 if de, ok := es[i].(*decodeError); ok {
192 es[i] = de.errorWithFileInformation(fileset)
193 }
194 }
195 return es
196 }
197
198 func globalVariableDeclarations(tree *ast.File) map[string]ast.Expr {
199 consts := make(map[string]ast.Expr)
200 for _, d := range tree.Decls {
201 if gd, ok := d.(*ast.GenDecl); ok && (gd.Tok == token.CONST || gd.Tok == token.VAR) {
202 for _, spec := range gd.Specs {
203 if vspec, ok := spec.(*ast.ValueSpec); ok {
204 for _, name := range vspec.Names {
205 for _, value := range vspec.Values {
206 consts[name.Name] = value
207 }
208 }
209 }
210 }
211 }
212 }
213 return consts
214 }
215
216 func findPkgDir(pkg string) (string, error) {
217
218 cmd := exec.Command("go", "list", "-find", "-json=Dir", pkg)
219 out, err := cmd.CombinedOutput()
220 if err != nil {
221 return "", fmt.Errorf("running `go list` failed: %w\n\n%s", err, string(out))
222 }
223 result := struct {
224 Dir string
225 }{}
226 if err := json.Unmarshal(out, &result); err != nil {
227 return "", fmt.Errorf("json unmarshal of `go list` failed: %w", err)
228 }
229 if result.Dir != "" {
230 return result.Dir, nil
231 }
232
233 return "", fmt.Errorf("empty respose from `go list`")
234 }
235
236 func importedGlobalVariableDeclaration(localVariables map[string]ast.Expr, imports []*ast.ImportSpec) (map[string]ast.Expr, error) {
237 for _, im := range imports {
238
239 var importAlias string
240 if im.Name == nil {
241 pathSegments := strings.Split(im.Path.Value, "/")
242 importAlias = strings.Trim(pathSegments[len(pathSegments)-1], "\"")
243 } else {
244 importAlias = im.Name.String()
245 }
246
247
248 pkg, err := strconv.Unquote(im.Path.Value)
249 if err != nil {
250 return nil, fmt.Errorf("can't handle import '%s': %w", im.Path.Value, err)
251 }
252 importDirectory, err := findPkgDir(pkg)
253 if err != nil {
254 return nil, fmt.Errorf("can't find import '%s': %w", im.Path.Value, err)
255 }
256
257 files, err := os.ReadDir(importDirectory)
258 if err != nil {
259 return nil, fmt.Errorf("failed to read import directory %s: %w", importDirectory, err)
260 }
261
262 for _, file := range files {
263 if file.IsDir() {
264
265 continue
266 }
267
268 if strings.Contains(file.Name(), "_test") {
269
270 continue
271 }
272
273 if !strings.HasSuffix(file.Name(), ".go") {
274
275 continue
276 }
277
278 fileset := token.NewFileSet()
279 tree, err := parser.ParseFile(fileset, strings.Join([]string{importDirectory, file.Name()}, string(os.PathSeparator)), nil, parser.AllErrors)
280 if err != nil {
281 return nil, fmt.Errorf("failed to parse path %s with error %w", im.Path.Value, err)
282 }
283
284
285 variables := globalVariableDeclarations(tree)
286
287
288 for k, v := range variables {
289 importK := strings.Join([]string{importAlias, k}, ".")
290 if _, ok := localVariables[importK]; !ok {
291 localVariables[importK] = v
292 } else {
293
294
295
296 if strings.Contains(file.Name(), GOOS) {
297
298
299
300 localVariables[importK] = v
301 }
302
303 }
304 }
305 }
306
307 }
308
309 return localVariables, nil
310 }
311
View as plain text