Skip to content

Commit

Permalink
Merge pull request #2774 from alexandremahdhaoui/sync_go_mod_kubernet…
Browse files Browse the repository at this point in the history
…es_kubernetes

🌱 verify go modules are in sync with upstream k/k
  • Loading branch information
k8s-ci-robot authored Apr 29, 2024
2 parents 7743591 + 105349a commit f6b23b1
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 3 deletions.
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
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)
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)
}

0 comments on commit f6b23b1

Please sign in to comment.