Skip to content

Commit 846b2c1

Browse files
🌱 verify go modules are in sync with upstream k/k
This commit addresses issues were go modules aren't in sync with upstream k/k by adding these changes: - add `tools/cmd/gomodcheck/main.go` to: - Parse and compares k/k dependencies to controller-runtime's ones. - If any version diffs is found, returns a payload describing the diffs and exit 1. - The user may exclude packages by passing them as arguments. - extend the `verify-modules` make target with `gomodcheck`.
1 parent 67b27f2 commit 846b2c1

File tree

3 files changed

+225
-1
lines changed

3 files changed

+225
-1
lines changed

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ GOLANGCI_LINT_PKG := github.com/golangci/golangci-lint/cmd/golangci-lint
9292
$(GOLANGCI_LINT): # Build golangci-lint from tools folder.
9393
GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(GOLANGCI_LINT_PKG) $(GOLANGCI_LINT_BIN) $(GOLANGCI_LINT_VER)
9494

95+
GO_MOD_CHECK_DIR := $(abspath ./hack/tools/cmd/gomodcheck)
96+
GO_MOD_CHECK := $(abspath $(TOOLS_BIN_DIR)/gomodcheck)
97+
GO_MOD_CHECK_IGNORE := $(abspath ./hack/.gomodcheck.yaml)
98+
$(GO_MOD_CHECK): # Build gomodcheck
99+
go build -C $(GO_MOD_CHECK_DIR) -o $(GO_MOD_CHECK)
100+
95101
## --------------------------------------
96102
## Linting
97103
## --------------------------------------
@@ -134,11 +140,12 @@ clean-bin: ## Remove all generated binaries.
134140
rm -rf hack/tools/bin
135141

136142
.PHONY: verify-modules
137-
verify-modules: modules ## Verify go modules are up to date
143+
verify-modules: modules $(GO_MOD_CHECK) ## Verify go modules are up to date
138144
@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 \
139145
git diff; \
140146
echo "go module files are out of date, please run 'make modules'"; exit 1; \
141147
fi
148+
$(GO_MOD_CHECK) $(GO_MOD_CHECK_IGNORE)
142149

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

hack/.gomodcheck.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
upstreamRefs:
2+
- k8s.io/api
3+
- k8s.io/apiextensions-apiserver
4+
- k8s.io/apimachinery
5+
- k8s.io/apiserver
6+
- k8s.io/client-go
7+
- k8s.io/component-base
8+
- k8s.io/klog/v2
9+
# k8s.io/utils -> conflicts with k/k deps
10+
11+
excludedModules:
12+
# --- test dependencies:
13+
- github.com/onsi/ginkgo/v2
14+
- github.com/onsi/gomega

hack/tools/cmd/gomodcheck/main.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"regexp"
9+
"strings"
10+
11+
"go.uber.org/zap"
12+
"sigs.k8s.io/yaml"
13+
)
14+
15+
const (
16+
modFile = "./go.mod"
17+
)
18+
19+
type config struct {
20+
UpstreamRefs []string `yaml:"upstreamRefs"`
21+
ExcludedModules []string `yaml:"excludedModules"`
22+
}
23+
24+
type upstream struct {
25+
Ref string `json:"ref"`
26+
Version string `json:"version"`
27+
}
28+
29+
// representation of an out of sync module
30+
type oosMod struct {
31+
Name string `json:"name"`
32+
Version string `json:"version"`
33+
Upstreams []upstream `json:"upstreams"`
34+
}
35+
36+
func main() {
37+
l, _ := zap.NewProduction()
38+
logger := l.Sugar()
39+
40+
if len(os.Args) < 2 {
41+
fmt.Printf("USAGE: %s [PATH_TO_CONFIG_FILE]\n", os.Args[0])
42+
os.Exit(1)
43+
}
44+
45+
// --- 1. parse config
46+
b, err := os.ReadFile(os.Args[1])
47+
if err != nil {
48+
logger.Fatal(err.Error())
49+
}
50+
51+
cfg := new(config)
52+
if err := yaml.Unmarshal(b, cfg); err != nil {
53+
logger.Fatal(err.Error())
54+
}
55+
56+
excludedMods := make(map[string]any)
57+
for _, mod := range cfg.ExcludedModules {
58+
excludedMods[mod] = nil
59+
}
60+
61+
// --- 2. project mods
62+
deps, err := parseModFile()
63+
if err != nil {
64+
logger.Fatal(err.Error())
65+
}
66+
67+
// --- 3. upstream mods (holding upstream refs)
68+
upstreamModGraph, err := getUpstreamModGraph(cfg.UpstreamRefs)
69+
if err != nil {
70+
logger.Fatal(err.Error())
71+
}
72+
73+
oosMods := make([]oosMod, 0)
74+
75+
// --- 4. validate
76+
// for each module in our project,
77+
// if it matches an upstream module,
78+
// then for each upstream module,
79+
// if project module version doesn't match upstream version,
80+
// then we add the version and the ref to the list of out of sync modules.
81+
for mod, version := range deps {
82+
if _, ok := excludedMods[mod]; ok {
83+
logger.Infof("skipped excluded module: %s", mod)
84+
continue
85+
}
86+
87+
if versionToRef, ok := upstreamModGraph[mod]; ok {
88+
upstreams := make([]upstream, 0)
89+
90+
for upstreamVersion, upstreamRef := range versionToRef {
91+
if version != upstreamVersion {
92+
upstreams = append(upstreams, upstream{
93+
Ref: upstreamRef,
94+
Version: upstreamVersion,
95+
})
96+
}
97+
}
98+
99+
if len(upstreams) > 0 {
100+
oosMods = append(oosMods, oosMod{
101+
Name: mod,
102+
Version: version,
103+
Upstreams: upstreams,
104+
})
105+
}
106+
}
107+
}
108+
109+
if len(oosMods) == 0 {
110+
fmt.Println("Success! 🎉")
111+
os.Exit(0)
112+
}
113+
114+
b, err = json.MarshalIndent(map[string]any{"outOfSyncModules": oosMods}, "", " ")
115+
if err != nil {
116+
panic(err)
117+
}
118+
119+
fmt.Println(string(b))
120+
os.Exit(1)
121+
}
122+
123+
var (
124+
cleanMods = regexp.MustCompile(`\t| *//.*`)
125+
modDelimStart = regexp.MustCompile(`^require.*`)
126+
modDelimEnd = ")"
127+
)
128+
129+
func parseModFile() (map[string]string, error) {
130+
b, err := os.ReadFile(modFile)
131+
if err != nil {
132+
return nil, err
133+
}
134+
135+
in := string(cleanMods.ReplaceAll(b, []byte("")))
136+
out := make(map[string]string)
137+
138+
start := false
139+
for _, s := range strings.Split(in, "\n") {
140+
switch {
141+
case modDelimStart.MatchString(s) && !start:
142+
start = true
143+
case s == modDelimEnd:
144+
return out, nil
145+
case start:
146+
kv := strings.SplitN(s, " ", 2)
147+
if len(kv) < 2 {
148+
return nil, fmt.Errorf("unexpected format for module: %q", s)
149+
}
150+
151+
out[kv[0]] = kv[1]
152+
}
153+
}
154+
155+
return out, nil
156+
}
157+
158+
func getUpstreamModGraph(upstreamRefs []string) (map[string]map[string]string, error) {
159+
b, err := exec.Command("go", "mod", "graph").Output()
160+
if err != nil {
161+
return nil, err
162+
}
163+
164+
graph := string(b)
165+
o1Refs := make(map[string]bool)
166+
for _, upstreamRef := range upstreamRefs {
167+
o1Refs[upstreamRef] = false
168+
}
169+
170+
modToVersionToUpstreamRef := make(map[string]map[string]string)
171+
172+
for _, line := range strings.Split(graph, "\n") {
173+
upstreamRef := strings.SplitN(line, "@", 2)[0]
174+
if _, ok := o1Refs[upstreamRef]; ok {
175+
o1Refs[upstreamRef] = true
176+
kv := strings.SplitN(strings.SplitN(line, " ", 2)[1], "@", 2)
177+
name := kv[0]
178+
version := kv[1]
179+
180+
if m, ok := modToVersionToUpstreamRef[kv[0]]; ok {
181+
m[version] = upstreamRef
182+
} else {
183+
versionToRef := map[string]string{version: upstreamRef}
184+
modToVersionToUpstreamRef[name] = versionToRef
185+
}
186+
}
187+
}
188+
189+
notFound := ""
190+
for ref, found := range o1Refs {
191+
if !found {
192+
notFound = fmt.Sprintf("%s%s, ", notFound, ref)
193+
}
194+
}
195+
196+
if notFound != "" {
197+
return nil, fmt.Errorf("cannot verify modules;"+
198+
"the following specified upstream module cannot be found in go.mod: [ %s ]",
199+
strings.TrimSuffix(notFound, ", "))
200+
}
201+
202+
return modToVersionToUpstreamRef, nil
203+
}

0 commit comments

Comments
 (0)