mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-11-06 05:17:08 +00:00
The nil check inside the referencer loop tested the map that the code above had just created, so it never ran. An unwanted module whose only referencers are main modules listing it as an indirect dependency got no status.unwantedReferences key at all; google/btree, reached through the go.mod-less peterbourgon/diskv v2, was invisible this way until diskv v3.0.1 gave it a visible referencer. Check for the key instead, so the next module in that situation shows up as an empty entry. Signed-off-by: Davanum Srinivas <davanum@gmail.com>
432 lines
14 KiB
Go
432 lines
14 KiB
Go
/*
|
|
Copyright 2021 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/google/go-cmp/cmp" //nolint:depguard
|
|
)
|
|
|
|
type Unwanted struct {
|
|
// things we want to stop referencing
|
|
Spec UnwantedSpec `json:"spec"`
|
|
// status of our unwanted dependencies
|
|
Status UnwantedStatus `json:"status"`
|
|
}
|
|
|
|
type UnwantedSpec struct {
|
|
// module names we don't want to depend on, mapped to an optional message about why
|
|
UnwantedModules map[string]string `json:"unwantedModules"`
|
|
// module names that should never be updated from their current version, mapped to a struct with version and reason
|
|
PinnedModules map[string]PinnedModule `json:"pinnedModules"`
|
|
}
|
|
|
|
type PinnedModule struct {
|
|
Version string `json:"Version"`
|
|
Reason string `json:"Reason"`
|
|
}
|
|
|
|
type UnwantedStatus struct {
|
|
// references to modules in the spec.unwantedModules list, based on `go mod graph` content.
|
|
// eliminating things from this list is good, and sometimes requires working with upstreams to do so.
|
|
UnwantedReferences map[string][]string `json:"unwantedReferences"`
|
|
// list of modules in the spec.unwantedModules list which are vendored
|
|
UnwantedVendored []string `json:"unwantedVendored"`
|
|
}
|
|
|
|
// runCommand runs the cmd and returns the combined stdout and stderr, or an
|
|
// error if the command failed.
|
|
func runCommand(cmd ...string) (string, error) {
|
|
return runCommandInDir("", cmd)
|
|
}
|
|
|
|
func runCommandInDir(dir string, cmd []string) (string, error) {
|
|
c := exec.Command(cmd[0], cmd[1:]...)
|
|
c.Dir = dir
|
|
output, err := c.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to run %q: %s (%s)", strings.Join(cmd, " "), err, output)
|
|
}
|
|
return string(output), nil
|
|
}
|
|
|
|
func readFile(path string) (string, error) {
|
|
content, err := os.ReadFile(path)
|
|
// Convert []byte to string and print to screen
|
|
return string(content), err
|
|
}
|
|
|
|
func moduleInSlice(a module, list []module, matchVersion bool) bool {
|
|
for _, b := range list {
|
|
if b == a {
|
|
return true
|
|
}
|
|
if !matchVersion && b.name == a.name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// converts `go mod graph` output modStr into a map of from->[]to references and the main module
|
|
func convertToMap(modStr string) ([]module, map[module][]module) {
|
|
var (
|
|
mainModulesList = []module{}
|
|
mainModules = map[module]bool{}
|
|
)
|
|
modMap := make(map[module][]module)
|
|
for _, line := range strings.Split(modStr, "\n") {
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
deps := strings.Split(line, " ")
|
|
if len(deps) == 2 {
|
|
first := parseModule(deps[0])
|
|
second := parseModule(deps[1])
|
|
if first.version == "" || first.version == "v0.0.0" {
|
|
if !mainModules[first] {
|
|
mainModules[first] = true
|
|
mainModulesList = append(mainModulesList, first)
|
|
}
|
|
}
|
|
modMap[first] = append(modMap[first], second)
|
|
} else {
|
|
// skip invalid line
|
|
log.Printf("!!!invalid line in mod.graph: %s", line)
|
|
continue
|
|
}
|
|
}
|
|
return mainModulesList, modMap
|
|
}
|
|
|
|
// difference returns a-b and b-a as sorted lists
|
|
func difference(a, b []string) ([]string, []string) {
|
|
aMinusB := map[string]bool{}
|
|
bMinusA := map[string]bool{}
|
|
for _, dependency := range a {
|
|
aMinusB[dependency] = true
|
|
}
|
|
for _, dependency := range b {
|
|
if _, found := aMinusB[dependency]; found {
|
|
delete(aMinusB, dependency)
|
|
} else {
|
|
bMinusA[dependency] = true
|
|
}
|
|
}
|
|
aMinusBList := []string{}
|
|
bMinusAList := []string{}
|
|
for dependency := range aMinusB {
|
|
aMinusBList = append(aMinusBList, dependency)
|
|
}
|
|
for dependency := range bMinusA {
|
|
bMinusAList = append(bMinusAList, dependency)
|
|
}
|
|
sort.Strings(aMinusBList)
|
|
sort.Strings(bMinusAList)
|
|
return aMinusBList, bMinusAList
|
|
}
|
|
|
|
type module struct {
|
|
name string
|
|
version string
|
|
}
|
|
|
|
func (m module) String() string {
|
|
if len(m.version) == 0 {
|
|
return m.name
|
|
}
|
|
return m.name + "@" + m.version
|
|
}
|
|
|
|
func parseModule(s string) module {
|
|
if !strings.Contains(s, "@") {
|
|
return module{name: s}
|
|
}
|
|
parts := strings.SplitN(s, "@", 2)
|
|
return module{name: parts[0], version: parts[1]}
|
|
}
|
|
|
|
// option1: dependencyverifier dependencies.json
|
|
// it will run `go mod graph` and check it.
|
|
func main() {
|
|
var modeGraphStr string
|
|
var err error
|
|
if len(os.Args) == 2 {
|
|
// run `go mod graph`
|
|
modeGraphStr, err = runCommand("go", "mod", "graph")
|
|
if err != nil {
|
|
log.Fatalf("Error running 'go mod graph': %s", err)
|
|
}
|
|
} else {
|
|
log.Fatalf("Usage: %s dependencies.json", os.Args[0])
|
|
}
|
|
|
|
dependenciesJSONPath := string(os.Args[1])
|
|
dependencies, err := readFile(dependenciesJSONPath)
|
|
if err != nil {
|
|
log.Fatalf("Error reading dependencies file %s: %s", dependencies, err)
|
|
}
|
|
|
|
// load Unwanted from json
|
|
configFromFile := &Unwanted{}
|
|
decoder := json.NewDecoder(bytes.NewBuffer([]byte(dependencies)))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(configFromFile); err != nil {
|
|
log.Fatalf("Error reading dependencies file %s: %s", dependenciesJSONPath, err)
|
|
}
|
|
|
|
// convert from `go mod graph` to main module and map of from->[]to references
|
|
mainModules, moduleGraph := convertToMap(modeGraphStr)
|
|
|
|
directDependencies := map[string]map[string]bool{}
|
|
for _, mainModule := range mainModules {
|
|
dir := ""
|
|
if mainModule.name != "k8s.io/kubernetes" {
|
|
dir = "staging/src/" + mainModule.name
|
|
}
|
|
listOutput, err := runCommandInDir(dir, []string{"go", "list", "-m", "-f", "{{if not .Indirect}}{{if not .Main}}{{.Path}}{{end}}{{end}}", "all"})
|
|
if err != nil {
|
|
log.Fatalf("Error running 'go list' for %s: %s", mainModule.name, err)
|
|
}
|
|
directDependencies[mainModule.name] = map[string]bool{}
|
|
for _, directDependency := range strings.Split(listOutput, "\n") {
|
|
directDependencies[mainModule.name][directDependency] = true
|
|
}
|
|
}
|
|
|
|
// gather the effective versions by looking at the versions required by the main modules
|
|
effectiveVersions := map[string]module{}
|
|
for _, mainModule := range mainModules {
|
|
for _, override := range moduleGraph[mainModule] {
|
|
if _, ok := effectiveVersions[override.name]; !ok {
|
|
effectiveVersions[override.name] = override
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for pinned modules that have been updated
|
|
pinnedModuleViolations := map[string][]string{}
|
|
if len(configFromFile.Spec.PinnedModules) > 0 {
|
|
// Get the current versions of pinned modules
|
|
for pinnedModule, pinnedInfo := range configFromFile.Spec.PinnedModules {
|
|
// Check if the module is in effectiveVersions
|
|
if effectiveModule, ok := effectiveVersions[pinnedModule]; ok {
|
|
// Compare with the pinned version from the JSON file
|
|
if effectiveModule.version != pinnedInfo.Version {
|
|
pinnedModuleViolations[pinnedModule] = []string{
|
|
fmt.Sprintf("Pinned version: %s", pinnedInfo.Version),
|
|
fmt.Sprintf("Attempted update to: %s", effectiveModule.version),
|
|
fmt.Sprintf("Reason for pinning: %s", pinnedInfo.Reason),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
unwantedToReferencers := map[string][]module{}
|
|
for _, mainModule := range mainModules {
|
|
// visit to find unwanted modules still referenced from the main module
|
|
visit(func(m module, via []module) {
|
|
if _, unwanted := configFromFile.Spec.UnwantedModules[m.name]; unwanted {
|
|
// this is unwanted, store what is referencing it
|
|
referencer := via[len(via)-1]
|
|
if !moduleInSlice(referencer, unwantedToReferencers[m.name], false) {
|
|
// // uncomment to get a detailed tree of the path that referenced the unwanted dependency
|
|
//
|
|
// i := 0
|
|
// for _, v := range via {
|
|
// if v.version != "" && v.version != "v0.0.0" {
|
|
// fmt.Println(strings.Repeat(" ", i), v)
|
|
// i++
|
|
// }
|
|
// }
|
|
// if i > 0 {
|
|
// fmt.Println(strings.Repeat(" ", i+1), m)
|
|
// fmt.Println()
|
|
// }
|
|
unwantedToReferencers[m.name] = append(unwantedToReferencers[m.name], referencer)
|
|
}
|
|
}
|
|
}, mainModule, moduleGraph, effectiveVersions)
|
|
}
|
|
|
|
config := &Unwanted{}
|
|
config.Spec.UnwantedModules = configFromFile.Spec.UnwantedModules
|
|
for unwanted := range unwantedToReferencers {
|
|
if config.Status.UnwantedReferences == nil {
|
|
config.Status.UnwantedReferences = map[string][]string{}
|
|
}
|
|
sort.Slice(unwantedToReferencers[unwanted], func(i, j int) bool {
|
|
ri := unwantedToReferencers[unwanted][i]
|
|
rj := unwantedToReferencers[unwanted][j]
|
|
if ri.name != rj.name {
|
|
return ri.name < rj.name
|
|
}
|
|
return ri.version < rj.version
|
|
})
|
|
for _, referencer := range unwantedToReferencers[unwanted] {
|
|
// make sure any reference at all shows up as a non-nil status,
|
|
// even when every referencer is a main module that only lists the
|
|
// unwanted module as an indirect dependency
|
|
if _, ok := config.Status.UnwantedReferences[unwanted]; !ok {
|
|
config.Status.UnwantedReferences[unwanted] = []string{}
|
|
}
|
|
// record specific names of versioned referents
|
|
if referencer.version != "" && referencer.version != "v0.0.0" {
|
|
config.Status.UnwantedReferences[unwanted] = append(config.Status.UnwantedReferences[unwanted], referencer.name)
|
|
} else if directDependencies[referencer.name][unwanted] {
|
|
config.Status.UnwantedReferences[unwanted] = append(config.Status.UnwantedReferences[unwanted], referencer.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
vendorModulesTxt, err := os.ReadFile("vendor/modules.txt")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
vendoredModules := map[string]bool{}
|
|
for _, l := range strings.Split(string(vendorModulesTxt), "\n") {
|
|
parts := strings.Split(l, " ")
|
|
if len(parts) == 3 && parts[0] == "#" && strings.HasPrefix(parts[2], "v") {
|
|
vendoredModules[parts[1]] = true
|
|
}
|
|
}
|
|
config.Status.UnwantedVendored = []string{}
|
|
for unwanted := range configFromFile.Spec.UnwantedModules {
|
|
if vendoredModules[unwanted] {
|
|
config.Status.UnwantedVendored = append(config.Status.UnwantedVendored, unwanted)
|
|
}
|
|
}
|
|
sort.Strings(config.Status.UnwantedVendored)
|
|
|
|
needUpdate := false
|
|
|
|
// Compare unwanted list from unwanted-dependencies.json with current status from `go mod graph`
|
|
expected, err := json.MarshalIndent(configFromFile.Status, "", " ")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
actual, err := json.MarshalIndent(config.Status, "", " ")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if !bytes.Equal(expected, actual) {
|
|
log.Printf("Expected status of\n%s", string(expected))
|
|
log.Printf("Got status of\n%s", string(actual))
|
|
needUpdate = true
|
|
log.Print("Status diff:\n", cmp.Diff(expected, actual))
|
|
}
|
|
for expectedRef, expectedFrom := range configFromFile.Status.UnwantedReferences {
|
|
actualFrom, ok := config.Status.UnwantedReferences[expectedRef]
|
|
if !ok {
|
|
// disappeared entirely
|
|
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)
|
|
needUpdate = true
|
|
continue
|
|
}
|
|
removedReferences, unwantedReferences := difference(expectedFrom, actualFrom)
|
|
if len(removedReferences) > 0 {
|
|
log.Printf("Good news! Unwanted module %q dropped the following dependants:", expectedRef)
|
|
for _, reference := range removedReferences {
|
|
log.Printf(" %s", reference)
|
|
}
|
|
log.Printf("!!! Remove those from status.unwantedReferences[%q] in %s to ensure they don't get reintroduced.", expectedRef, dependenciesJSONPath)
|
|
needUpdate = true
|
|
}
|
|
if len(unwantedReferences) > 0 {
|
|
log.Printf("Unwanted module %q marked in %s is referenced by new dependants:", expectedRef, dependenciesJSONPath)
|
|
for _, reference := range unwantedReferences {
|
|
log.Printf(" %s", reference)
|
|
}
|
|
log.Printf("!!! Avoid updating referencing modules to versions that reintroduce use of unwanted dependencies\n")
|
|
needUpdate = true
|
|
}
|
|
}
|
|
for actualRef, actualFrom := range config.Status.UnwantedReferences {
|
|
if _, expected := configFromFile.Status.UnwantedReferences[actualRef]; expected {
|
|
// expected, already ensured referencers were equal in the first loop
|
|
continue
|
|
}
|
|
log.Printf("Unwanted module %q marked in %s is referenced", actualRef, dependenciesJSONPath)
|
|
for _, reference := range actualFrom {
|
|
log.Printf(" %s", reference)
|
|
}
|
|
log.Printf("!!! Avoid updating referencing modules to versions that reintroduce use of unwanted dependencies\n")
|
|
needUpdate = true
|
|
}
|
|
|
|
removedVendored, addedVendored := difference(configFromFile.Status.UnwantedVendored, config.Status.UnwantedVendored)
|
|
if len(removedVendored) > 0 {
|
|
log.Printf("Good news! Unwanted modules are no longer vendered: %q", removedVendored)
|
|
log.Printf("!!! Remove those from status.unwantedVendored in %s to ensure they don't get reintroduced.", dependenciesJSONPath)
|
|
needUpdate = true
|
|
}
|
|
if len(addedVendored) > 0 {
|
|
log.Printf("Unwanted modules are newly vendored: %q", addedVendored)
|
|
log.Printf("!!! Avoid updates that increase vendoring of unwanted dependencies\n")
|
|
needUpdate = true
|
|
}
|
|
|
|
if needUpdate {
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Check if there are any pinned module violations
|
|
if len(pinnedModuleViolations) > 0 {
|
|
log.Printf("ERROR: The following pinned modules have been updated:")
|
|
for module, details := range pinnedModuleViolations {
|
|
log.Printf("Module: %s", module)
|
|
for _, detail := range details {
|
|
log.Printf(" %s", detail)
|
|
}
|
|
}
|
|
log.Printf("Pinned modules must not be updated. Please revert these changes.")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func visit(visitor func(m module, via []module), main module, references map[module][]module, effectiveVersions map[string]module) {
|
|
doVisit(visitor, main, nil, map[module]bool{}, references, effectiveVersions)
|
|
}
|
|
|
|
func doVisit(visitor func(m module, via []module), from module, via []module, visited map[module]bool, references map[module][]module, effectiveVersions map[string]module) {
|
|
visitor(from, via)
|
|
via = append(via, from)
|
|
if visited[from] {
|
|
return
|
|
}
|
|
for _, to := range references[from] {
|
|
// switch to the effective version of this dependency
|
|
if override, ok := effectiveVersions[to.name]; ok {
|
|
to = override
|
|
}
|
|
// recurse unless we've already visited this module in this traversal
|
|
if !moduleInSlice(to, via, false) {
|
|
doVisit(visitor, to, via, visited, references, effectiveVersions)
|
|
}
|
|
}
|
|
visited[from] = true
|
|
}
|