Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🌱 verify go modules are in sync with upstream k/k #2774

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .gomodcheck.yaml
Original file line number Diff line number Diff line change
@@ -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 -> conflicts with k/k deps

excludedModules:
# --- test dependencies:
- github.com/onsi/ginkgo/v2
- github.com/onsi/gomega
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ 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 ./hack/tools/cmd/gomodcheck)
GO_MOD_CHECK := $(abspath $(TOOLS_BIN_DIR)/gomodcheck)
GO_MOD_CHECK_IGNORE := $(abspath .gomodcheck.yaml)
.PHONY: $(GO_MOD_CHECK)
$(GO_MOD_CHECK): # Build gomodcheck
alexandremahdhaoui marked this conversation as resolved.
Show resolved Hide resolved
go build -C $(GO_MOD_CHECK_DIR) -o $(GO_MOD_CHECK)

## --------------------------------------
## Linting
## --------------------------------------
Expand Down Expand Up @@ -130,16 +137,15 @@ clean-bin: ## Remove all generated binaries.
rm -rf hack/tools/bin

.PHONY: verify-modules
verify-modules: modules ## Verify go modules are up to date
verify-modules: modules $(GO_MOD_CHECK) ## Verify go modules are up to date
@if !(git diff --quiet HEAD -- go.sum go.mod $(TOOLS_DIR)/go.mod $(TOOLS_DIR)/go.sum $(ENVTEST_DIR)/go.mod $(ENVTEST_DIR)/go.sum $(SCRATCH_ENV_DIR)/go.sum); then \
git diff; \
echo "go module files are out of date, please run 'make modules'"; exit 1; \
fi
$(GO_MOD_CHECK) $(GO_MOD_CHECK_IGNORE)

APIDIFF_OLD_COMMIT ?= $(shell git rev-parse origin/main)

.PHONY: apidiff
verify-apidiff: $(GO_APIDIFF) ## Check for API differences
$(GO_APIDIFF) $(APIDIFF_OLD_COMMIT) --print-compatible


2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ require (
sigs.k8s.io/yaml v1.3.0
)

require golang.org/x/mod v0.15.0

require (
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6R
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
Expand Down
Empty file removed hack/tools/.keep
Empty file.
204 changes: 204 additions & 0 deletions hack/tools/cmd/gomodcheck/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package main

import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"

"go.uber.org/zap"
"golang.org/x/mod/modfile"
"sigs.k8s.io/yaml"
)

const (
modFile = "./go.mod"
)

type config struct {
UpstreamRefs []string `json:"upstreamRefs"`
ExcludedModules []string `json:"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"`
Upstreams []upstream `json:"upstreams"`
}

func main() {
l, _ := zap.NewProduction()
logger := l.Sugar()

if len(os.Args) < 2 {
fmt.Printf("USAGE: %s [PATH_TO_CONFIG_FILE]\n", os.Args[0])
os.Exit(1)
}

// --- 1. parse config
b, err := os.ReadFile(os.Args[1])
if err != nil {
fatal(err)
}

cfg := new(config)
if err := yaml.Unmarshal(b, cfg); err != nil {
fatal(err)
}

excludedMods := make(map[string]any)
for _, mod := range cfg.ExcludedModules {
excludedMods[mod] = nil
}

// --- 2. project mods
projectModules, err := modulesFromGoModFile()
if err != nil {
fatal(err)
}

// --- 3. upstream mods
upstreamModules, err := modulesFromUpstreamModGraph(cfg.UpstreamRefs)
if err != nil {
fatal(err)
}

oosMods := make([]oosMod, 0)

// --- 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 projectModules {
if _, ok := excludedMods[mod]; ok {
logger.Infof("skipped: %s", mod)
continue
}

if versionToRef, ok := upstreamModules[mod]; ok {
outOfSyncUpstream := make([]upstream, 0)

for upstreamVersion, upstreamRef := range versionToRef {
if version == upstreamVersion { // pass if version in sync.
continue
}

outOfSyncUpstream = append(outOfSyncUpstream, upstream{
Ref: upstreamRef,
Version: upstreamVersion,
})
}

if len(outOfSyncUpstream) == 0 { // pass if no out of sync upstreams.
continue
}

oosMods = append(oosMods, oosMod{
Name: mod,
Version: version,
Upstreams: outOfSyncUpstream,
})
}
}

if len(oosMods) == 0 {
fmt.Println("🎉 Success!")
os.Exit(0)
}

b, err = json.MarshalIndent(map[string]any{"outOfSyncModules": oosMods}, "", " ")
if err != nil {
fatal(err)
}

fmt.Println(string(b))
os.Exit(1)
}

func modulesFromGoModFile() (map[string]string, error) {
b, err := os.ReadFile(modFile)
sbueringer marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

f, err := modfile.Parse(modFile, b, nil)
if err != nil {
return nil, err
}

out := make(map[string]string)
for _, mod := range f.Require {
out[mod.Mod.Path] = mod.Mod.Version
}

return out, nil
}

func modulesFromUpstreamModGraph(upstreamRefList []string) (map[string]map[string]string, error) {
b, err := exec.Command("go", "mod", "graph").Output()
if err != nil {
return nil, err
}

graph := string(b)

// upstreamRefs is a set of user specified upstream modules.
// The set has 2 functions:
// 1. Check if `go mod graph` modules are one of the user specified upstream modules.
// 2. Mark if a user specified upstream module was found in the module graph.
// If a user specified upstream module is not found, gomodcheck will exit with an error.
upstreamRefs := make(map[string]bool)
for _, ref := range upstreamRefList {
upstreamRefs[ref] = false
}

modToVersionToUpstreamRef := make(map[string]map[string]string)
for _, line := range strings.Split(graph, "\n") {
ref := strings.SplitN(line, "@", 2)[0]

if _, ok := upstreamRefs[ref]; !ok {
continue
}

upstreamRefs[ref] = true // mark the ref as found

kv := strings.SplitN(strings.SplitN(line, " ", 2)[1], "@", 2)
name := kv[0]
version := kv[1]

if _, ok := modToVersionToUpstreamRef[name]; !ok {
modToVersionToUpstreamRef[name] = make(map[string]string)
}

modToVersionToUpstreamRef[name][version] = ref
}

notFoundErr := ""
for ref, found := range upstreamRefs {
if !found {
notFoundErr = fmt.Sprintf("%s%s, ", notFoundErr, ref)
}
}

if notFoundErr != "" {
return nil, fmt.Errorf("cannot verify modules: "+
"the following specified upstream module(s) cannot be found in go.mod: [ %s ]",
strings.TrimSuffix(notFoundErr, ", "))
}

return modToVersionToUpstreamRef, nil
}

func fatal(err error) {
fmt.Printf("❌ %s\n", err.Error())
os.Exit(1)
}