diff --git a/.gomodcheck.ignore b/.gomodcheck.ignore deleted file mode 100644 index aa1479c9a2..0000000000 --- a/.gomodcheck.ignore +++ /dev/null @@ -1,9 +0,0 @@ -github.com/onsi/gomega -github.com/onsi/ginkgo/v2 - -k8s.io/api -k8s.io/apimachinery -k8s.io/apiextensions-apiserver -k8s.io/apiserver -k8s.io/client-go -k8s.io/component-base diff --git a/Makefile b/Makefile index 66b1b0bfb1..c491c5c918 100644 --- a/Makefile +++ b/Makefile @@ -92,9 +92,9 @@ GOLANGCI_LINT_PKG := github.com/golangci/golangci-lint/cmd/golangci-lint $(GOLANGCI_LINT): # Build golangci-lint from tools folder. GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(GOLANGCI_LINT_PKG) $(GOLANGCI_LINT_BIN) $(GOLANGCI_LINT_VER) -GO_MOD_CHECK_DIR := $(abspath ./tools/cmd/gomodcheck) +GO_MOD_CHECK_DIR := $(abspath ./hack/tools/cmd/gomodcheck) GO_MOD_CHECK := $(abspath $(TOOLS_BIN_DIR)/gomodcheck) -GO_MOD_CHECK_IGNORE := $(abspath ./.gomodcheck.ignore) +GO_MOD_CHECK_IGNORE := $(abspath ./hack/.gomodcheck.yaml) $(GO_MOD_CHECK): # Build gomodcheck go build -C $(GO_MOD_CHECK_DIR) -o $(GO_MOD_CHECK) @@ -145,7 +145,7 @@ verify-modules: modules $(GO_MOD_CHECK) ## Verify go modules are up to date git diff; \ echo "go module files are out of date, please run 'make modules'"; exit 1; \ fi - cat $(GO_MOD_CHECK_IGNORE) | xargs $(GO_MOD_CHECK) + $(GO_MOD_CHECK) $(GO_MOD_CHECK_IGNORE) APIDIFF_OLD_COMMIT ?= $(shell git rev-parse origin/main) diff --git a/hack/.gomodcheck.yaml b/hack/.gomodcheck.yaml new file mode 100644 index 0000000000..2d96847a66 --- /dev/null +++ b/hack/.gomodcheck.yaml @@ -0,0 +1,14 @@ +upstreamRefs: + - k8s.io/api + - k8s.io/apiextensions-apiserver + - k8s.io/apimachinery + - k8s.io/apiserver + - k8s.io/client-go + - k8s.io/component-base + - k8s.io/klog/v2 + - k8s.io/utils + +excludedModules: + # --- test dependencies: + - github.com/onsi/ginkgo/v2 + - github.com/onsi/gomega diff --git a/hack/tools/cmd/gomodcheck/main.go b/hack/tools/cmd/gomodcheck/main.go index ae1ea0ce8a..9c32243976 100644 --- a/hack/tools/cmd/gomodcheck/main.go +++ b/hack/tools/cmd/gomodcheck/main.go @@ -3,70 +3,106 @@ package main import ( "encoding/json" "fmt" - "io" - "net/http" "os" + "os/exec" "regexp" "strings" - "sigs.k8s.io/controller-runtime/pkg/log/zap" + "go.uber.org/zap" + "sigs.k8s.io/yaml" ) const ( - kkModUrl = "https://raw.githubusercontent.com/kubernetes/kubernetes/master/go.mod" - modFile = "./go.mod" + modFile = "./go.mod" ) -var ( - cleanMods = regexp.MustCompile(`\t| *//.*`) - modDelimStart = regexp.MustCompile(`^require.*`) - modDelimEnd = ")" -) +type config struct { + UpstreamRefs []string `yaml:"upstreamRefs"` + ExcludedModules []string `yaml:"excludedModules"` +} + +type upstream struct { + Ref string `json:"ref"` + Version string `json:"version"` +} +// representation of an out of sync module type oosMod struct { - Name string `json:"name"` - Version string `json:"version"` - UpstreamVersion string `json:"upstreamVersion"` + Name string `json:"name"` + Version string `json:"version"` + Upstreams []upstream `json:"upstreams"` } func main() { - logger := zap.New() - excludedMods := getExcludedMods() + l, _ := zap.NewProduction() + logger := l.Sugar() - // 1. kk - resp, err := http.Get(kkModUrl) - if err != nil { - panic(err.Error()) + if len(os.Args) < 2 { + fmt.Printf("USAGE: %s [PATH_TO_CONFIG_FILE]\n", os.Args[0]) + os.Exit(1) } - b, err := io.ReadAll(resp.Body) + // --- 1. parse config + b, err := os.ReadFile(os.Args[1]) if err != nil { - panic(err.Error()) + logger.Fatal(err.Error()) + } + + cfg := new(config) + if err := yaml.Unmarshal(b, cfg); err != nil { + logger.Fatal(err.Error()) + } + + excludedMods := make(map[string]any) + for _, mod := range cfg.ExcludedModules { + excludedMods[mod] = nil } - kkMods := parseMod(b) + // --- 2. project mods + deps, err := parseModFile() + if err != nil { + logger.Fatal(err.Error()) + } - // 2. go.mod - b, err = os.ReadFile(modFile) + // --- 3. upstream mods (holding upstream refs) + upstreamModGraph, err := getUpstreamModGraph(cfg.UpstreamRefs) if err != nil { - return + logger.Fatal(err.Error()) } oosMods := make([]oosMod, 0) - mods := parseMod(b) - for k, v := range mods { - if _, ok := excludedMods[k]; ok { - logger.Info(fmt.Sprintf("skipped module: %s", k)) + // --- 4. validate + // for each module in our project, + // if it matches an upstream module, + // then for each upstream module, + // if project module version doesn't match upstream version, + // then we add the version and the ref to the list of out of sync modules. + for mod, version := range deps { + if _, ok := excludedMods[mod]; ok { + logger.Infof("skipped excluded module: %s", mod) continue } - if upstreamVersion, ok := kkMods[k]; ok && v != upstreamVersion { - oosMods = append(oosMods, oosMod{ - Name: k, - Version: v, - UpstreamVersion: upstreamVersion, - }) + if versionToRef, ok := upstreamModGraph[mod]; ok { + upstreams := make([]upstream, 0) + + for upstreamVersion, upstreamRef := range versionToRef { + if version != upstreamVersion { + upstreams = append(upstreams, upstream{ + Ref: upstreamRef, + Version: upstreamVersion, + }) + } + } + + if len(upstreams) > 0 { + oosMods = append(oosMods, oosMod{ + Name: mod, + Version: version, + Upstreams: upstreams, + }) + } } } @@ -84,7 +120,18 @@ func main() { os.Exit(1) } -func parseMod(b []byte) map[string]string { +var ( + cleanMods = regexp.MustCompile(`\t| *//.*`) + modDelimStart = regexp.MustCompile(`^require.*`) + modDelimEnd = ")" +) + +func parseModFile() (map[string]string, error) { + b, err := os.ReadFile(modFile) + if err != nil { + return nil, err + } + in := string(cleanMods.ReplaceAll(b, []byte(""))) out := make(map[string]string) @@ -94,26 +141,63 @@ func parseMod(b []byte) map[string]string { case modDelimStart.MatchString(s) && !start: start = true case s == modDelimEnd: - return out + return out, nil case start: kv := strings.SplitN(s, " ", 2) if len(kv) < 2 { - panic(fmt.Sprintf("unexpected format for module: %q", s)) + return nil, fmt.Errorf("unexpected format for module: %q", s) } out[kv[0]] = kv[1] } } - return out + return out, nil } -func getExcludedMods() map[string]any { - out := make(map[string]any) +func getUpstreamModGraph(upstreamRefs []string) (map[string]map[string]string, error) { + b, err := exec.Command("go", "mod", "graph").Output() + if err != nil { + return nil, err + } + + graph := string(b) + o1Refs := make(map[string]bool) + for _, upstreamRef := range upstreamRefs { + o1Refs[upstreamRef] = false + } + + modToVersionToUpstreamRef := make(map[string]map[string]string) + + for _, line := range strings.Split(graph, "\n") { + upstreamRef := strings.SplitN(line, "@", 2)[0] + if _, ok := o1Refs[upstreamRef]; ok { + o1Refs[upstreamRef] = true + kv := strings.SplitN(strings.SplitN(line, " ", 2)[1], "@", 2) + name := kv[0] + version := kv[1] + + if m, ok := modToVersionToUpstreamRef[kv[0]]; ok { + m[version] = upstreamRef + } else { + versionToRef := map[string]string{version: upstreamRef} + modToVersionToUpstreamRef[name] = versionToRef + } + } + } + + notFound := "" + for ref, found := range o1Refs { + if !found { + notFound = fmt.Sprintf("%s%s, ", notFound, ref) + } + } - for _, mod := range os.Args[1:] { - out[mod] = nil + if notFound != "" { + return nil, fmt.Errorf("cannot verify modules;"+ + "the following specified upstream module cannot be found in go.mod: [ %s ]", + strings.TrimSuffix(notFound, ", ")) } - return out + return modToVersionToUpstreamRef, nil }