1
16
17 package main
18
19 import (
20 "bytes"
21 "encoding/json"
22 "fmt"
23 "io/ioutil"
24 "log"
25 "os"
26 "os/exec"
27 "sort"
28 "strings"
29
30 "github.com/google/go-cmp/cmp"
31 )
32
33 type Unwanted struct {
34
35 Spec UnwantedSpec `json:"spec"`
36
37 Status UnwantedStatus `json:"status"`
38 }
39
40 type UnwantedSpec struct {
41
42 UnwantedModules map[string]string `json:"unwantedModules"`
43 }
44
45 type UnwantedStatus struct {
46
47
48 UnwantedReferences map[string][]string `json:"unwantedReferences"`
49
50 UnwantedVendored []string `json:"unwantedVendored"`
51 }
52
53
54
55 func runCommand(cmd ...string) (string, error) {
56 return runCommandInDir("", cmd)
57 }
58
59 func runCommandInDir(dir string, cmd []string) (string, error) {
60 c := exec.Command(cmd[0], cmd[1:]...)
61 c.Dir = dir
62 output, err := c.CombinedOutput()
63 if err != nil {
64 return "", fmt.Errorf("failed to run %q: %s (%s)", strings.Join(cmd, " "), err, output)
65 }
66 return string(output), nil
67 }
68
69 func readFile(path string) (string, error) {
70 content, err := os.ReadFile(path)
71
72 return string(content), err
73 }
74
75 func moduleInSlice(a module, list []module, matchVersion bool) bool {
76 for _, b := range list {
77 if b == a {
78 return true
79 }
80 if !matchVersion && b.name == a.name {
81 return true
82 }
83 }
84 return false
85 }
86
87
88 func convertToMap(modStr string) ([]module, map[module][]module) {
89 var (
90 mainModulesList = []module{}
91 mainModules = map[module]bool{}
92 )
93 modMap := make(map[module][]module)
94 for _, line := range strings.Split(modStr, "\n") {
95 if len(line) == 0 {
96 continue
97 }
98 deps := strings.Split(line, " ")
99 if len(deps) == 2 {
100 first := parseModule(deps[0])
101 second := parseModule(deps[1])
102 if first.version == "" || first.version == "v0.0.0" {
103 if !mainModules[first] {
104 mainModules[first] = true
105 mainModulesList = append(mainModulesList, first)
106 }
107 }
108 modMap[first] = append(modMap[first], second)
109 } else {
110
111 log.Printf("!!!invalid line in mod.graph: %s", line)
112 continue
113 }
114 }
115 return mainModulesList, modMap
116 }
117
118
119 func difference(a, b []string) ([]string, []string) {
120 aMinusB := map[string]bool{}
121 bMinusA := map[string]bool{}
122 for _, dependency := range a {
123 aMinusB[dependency] = true
124 }
125 for _, dependency := range b {
126 if _, found := aMinusB[dependency]; found {
127 delete(aMinusB, dependency)
128 } else {
129 bMinusA[dependency] = true
130 }
131 }
132 aMinusBList := []string{}
133 bMinusAList := []string{}
134 for dependency := range aMinusB {
135 aMinusBList = append(aMinusBList, dependency)
136 }
137 for dependency := range bMinusA {
138 bMinusAList = append(bMinusAList, dependency)
139 }
140 sort.Strings(aMinusBList)
141 sort.Strings(bMinusAList)
142 return aMinusBList, bMinusAList
143 }
144
145 type module struct {
146 name string
147 version string
148 }
149
150 func (m module) String() string {
151 if len(m.version) == 0 {
152 return m.name
153 }
154 return m.name + "@" + m.version
155 }
156
157 func parseModule(s string) module {
158 if !strings.Contains(s, "@") {
159 return module{name: s}
160 }
161 parts := strings.SplitN(s, "@", 2)
162 return module{name: parts[0], version: parts[1]}
163 }
164
165
166
167 func main() {
168 var modeGraphStr string
169 var err error
170 if len(os.Args) == 2 {
171
172 modeGraphStr, err = runCommand("go", "mod", "graph")
173 if err != nil {
174 log.Fatalf("Error running 'go mod graph': %s", err)
175 }
176 } else {
177 log.Fatalf("Usage: %s dependencies.json", os.Args[0])
178 }
179
180 dependenciesJSONPath := string(os.Args[1])
181 dependencies, err := readFile(dependenciesJSONPath)
182 if err != nil {
183 log.Fatalf("Error reading dependencies file %s: %s", dependencies, err)
184 }
185
186
187 configFromFile := &Unwanted{}
188 decoder := json.NewDecoder(bytes.NewBuffer([]byte(dependencies)))
189 decoder.DisallowUnknownFields()
190 if err := decoder.Decode(configFromFile); err != nil {
191 log.Fatalf("Error reading dependencies file %s: %s", dependenciesJSONPath, err)
192 }
193
194
195 mainModules, moduleGraph := convertToMap(modeGraphStr)
196
197 directDependencies := map[string]map[string]bool{}
198 for _, mainModule := range mainModules {
199 dir := ""
200 if mainModule.name != "k8s.io/kubernetes" {
201 dir = "staging/src/" + mainModule.name
202 }
203 listOutput, err := runCommandInDir(dir, []string{"go", "list", "-m", "-f", "{{if not .Indirect}}{{if not .Main}}{{.Path}}{{end}}{{end}}", "all"})
204 if err != nil {
205 log.Fatalf("Error running 'go list' for %s: %s", mainModule.name, err)
206 }
207 directDependencies[mainModule.name] = map[string]bool{}
208 for _, directDependency := range strings.Split(listOutput, "\n") {
209 directDependencies[mainModule.name][directDependency] = true
210 }
211 }
212
213
214 effectiveVersions := map[string]module{}
215 for _, mainModule := range mainModules {
216 for _, override := range moduleGraph[mainModule] {
217 if _, ok := effectiveVersions[override.name]; !ok {
218 effectiveVersions[override.name] = override
219 }
220 }
221 }
222
223 unwantedToReferencers := map[string][]module{}
224 for _, mainModule := range mainModules {
225
226 visit(func(m module, via []module) {
227 if _, unwanted := configFromFile.Spec.UnwantedModules[m.name]; unwanted {
228
229 referencer := via[len(via)-1]
230 if !moduleInSlice(referencer, unwantedToReferencers[m.name], false) {
231
232
233
234
235
236
237
238
239
240
241
242
243
244 unwantedToReferencers[m.name] = append(unwantedToReferencers[m.name], referencer)
245 }
246 }
247 }, mainModule, moduleGraph, effectiveVersions)
248 }
249
250 config := &Unwanted{}
251 config.Spec.UnwantedModules = configFromFile.Spec.UnwantedModules
252 for unwanted := range unwantedToReferencers {
253 if config.Status.UnwantedReferences == nil {
254 config.Status.UnwantedReferences = map[string][]string{}
255 }
256 sort.Slice(unwantedToReferencers[unwanted], func(i, j int) bool {
257 ri := unwantedToReferencers[unwanted][i]
258 rj := unwantedToReferencers[unwanted][j]
259 if ri.name != rj.name {
260 return ri.name < rj.name
261 }
262 return ri.version < rj.version
263 })
264 for _, referencer := range unwantedToReferencers[unwanted] {
265
266 if config.Status.UnwantedReferences == nil {
267 config.Status.UnwantedReferences[unwanted] = []string{}
268 }
269
270 if referencer.version != "" && referencer.version != "v0.0.0" {
271 config.Status.UnwantedReferences[unwanted] = append(config.Status.UnwantedReferences[unwanted], referencer.name)
272 } else if directDependencies[referencer.name][unwanted] {
273 config.Status.UnwantedReferences[unwanted] = append(config.Status.UnwantedReferences[unwanted], referencer.name)
274 }
275 }
276 }
277
278 vendorModulesTxt, err := ioutil.ReadFile("vendor/modules.txt")
279 if err != nil {
280 log.Fatal(err)
281 }
282 vendoredModules := map[string]bool{}
283 for _, l := range strings.Split(string(vendorModulesTxt), "\n") {
284 parts := strings.Split(l, " ")
285 if len(parts) == 3 && parts[0] == "#" && strings.HasPrefix(parts[2], "v") {
286 vendoredModules[parts[1]] = true
287 }
288 }
289 config.Status.UnwantedVendored = []string{}
290 for unwanted := range configFromFile.Spec.UnwantedModules {
291 if vendoredModules[unwanted] {
292 config.Status.UnwantedVendored = append(config.Status.UnwantedVendored, unwanted)
293 }
294 }
295 sort.Strings(config.Status.UnwantedVendored)
296
297 needUpdate := false
298
299
300 expected, err := json.MarshalIndent(configFromFile.Status, "", " ")
301 if err != nil {
302 log.Fatal(err)
303 }
304 actual, err := json.MarshalIndent(config.Status, "", " ")
305 if err != nil {
306 log.Fatal(err)
307 }
308 if !bytes.Equal(expected, actual) {
309 log.Printf("Expected status of\n%s", string(expected))
310 log.Printf("Got status of\n%s", string(actual))
311 needUpdate = true
312 log.Print("Status diff:\n", cmp.Diff(expected, actual))
313 }
314 for expectedRef, expectedFrom := range configFromFile.Status.UnwantedReferences {
315 actualFrom, ok := config.Status.UnwantedReferences[expectedRef]
316 if !ok {
317
318 log.Printf("Good news! Unwanted dependency %q is no longer referenced. Remove status.unwantedReferences[%q] in %s to ensure it doesn't get reintroduced.", expectedRef, expectedRef, dependenciesJSONPath)
319 needUpdate = true
320 continue
321 }
322 removedReferences, unwantedReferences := difference(expectedFrom, actualFrom)
323 if len(removedReferences) > 0 {
324 log.Printf("Good news! Unwanted module %q dropped the following dependants:", expectedRef)
325 for _, reference := range removedReferences {
326 log.Printf(" %s", reference)
327 }
328 log.Printf("!!! Remove those from status.unwantedReferences[%q] in %s to ensure they don't get reintroduced.", expectedRef, dependenciesJSONPath)
329 needUpdate = true
330 }
331 if len(unwantedReferences) > 0 {
332 log.Printf("Unwanted module %q marked in %s is referenced by new dependants:", expectedRef, dependenciesJSONPath)
333 for _, reference := range unwantedReferences {
334 log.Printf(" %s", reference)
335 }
336 log.Printf("!!! Avoid updating referencing modules to versions that reintroduce use of unwanted dependencies\n")
337 needUpdate = true
338 }
339 }
340 for actualRef, actualFrom := range config.Status.UnwantedReferences {
341 if _, expected := configFromFile.Status.UnwantedReferences[actualRef]; expected {
342
343 continue
344 }
345 log.Printf("Unwanted module %q marked in %s is referenced", actualRef, dependenciesJSONPath)
346 for _, reference := range actualFrom {
347 log.Printf(" %s", reference)
348 }
349 log.Printf("!!! Avoid updating referencing modules to versions that reintroduce use of unwanted dependencies\n")
350 needUpdate = true
351 }
352
353 removedVendored, addedVendored := difference(configFromFile.Status.UnwantedVendored, config.Status.UnwantedVendored)
354 if len(removedVendored) > 0 {
355 log.Printf("Good news! Unwanted modules are no longer vendered: %q", removedVendored)
356 log.Printf("!!! Remove those from status.unwantedVendored in %s to ensure they don't get reintroduced.", dependenciesJSONPath)
357 needUpdate = true
358 }
359 if len(addedVendored) > 0 {
360 log.Printf("Unwanted modules are newly vendored: %q", addedVendored)
361 log.Printf("!!! Avoid updates that increase vendoring of unwanted dependencies\n")
362 needUpdate = true
363 }
364
365 if needUpdate {
366 os.Exit(1)
367 }
368 }
369
370 func visit(visitor func(m module, via []module), main module, references map[module][]module, effectiveVersions map[string]module) {
371 doVisit(visitor, main, nil, map[module]bool{}, references, effectiveVersions)
372 }
373
374 func doVisit(visitor func(m module, via []module), from module, via []module, visited map[module]bool, references map[module][]module, effectiveVersions map[string]module) {
375 visitor(from, via)
376 via = append(via, from)
377 if visited[from] {
378 return
379 }
380 for _, to := range references[from] {
381
382 if override, ok := effectiveVersions[to.name]; ok {
383 to = override
384 }
385
386 if !moduleInSlice(to, via, false) {
387 doVisit(visitor, to, via, visited, references, effectiveVersions)
388 }
389 }
390 visited[from] = true
391 }
392
View as plain text