Skip to content

Commit 346ab99

Browse files
committed
tools/cloud-dep-check: add linter enforcing per-cloud binary independence
Each cilium-operator-* binary is intended to ship only the cloud-provider SDKs that match its build tags: - cilium-operator-generic : no cloud SDKs - cilium-operator-aws : aws-sdk-go-v2 / smithy-go only - cilium-operator-azure : azure-sdk-for-go / AzureAD only - cilium-operator-alibabacloud: alibaba-cloud-sdk-go only Without an automated check, it is easy to accidentally re-introduce a cross-cloud dependency by importing a package from a non-tagged file. Add a small Go program at tools/cloud-dep-check that, for each operator binary configuration, runs 'go list -deps -tags <tags> ./operator' and fails if any forbidden cloud-SDK package shows up in the dependency closure. The combined cilium-operator binary is allowed to contain all three SDKs. The check is wired into 'make custom-lint' so it runs as part of the existing 'Custom linters' step in .github/workflows/lint-go.yaml, and no new GitHub Actions workflow is needed. It can also be invoked directly: make -C tools/cloud-dep-check check Signed-off-by: André Martins <andre@cilium.io>
1 parent 92e1a4f commit 346ab99

5 files changed

Lines changed: 234 additions & 1 deletion

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,9 @@ custom-lint: ## Run extra local linters
410410
$(ECHO_CHECK) metricslint
411411
$(QUIET)$(MAKE) -C tools/metricslint
412412
$(QUIET)tools/metricslint/metricslint ./...
413+
$(ECHO_CHECK) cloud-dep-check
414+
$(QUIET)$(MAKE) -C tools/cloud-dep-check
415+
$(QUIET)tools/cloud-dep-check/cilium-cloud-dep-check -root .
413416

414417
golangci-lint: ## Run golangci-lint
415418
ifneq (,$(findstring $(GOLANGCILINT_WANT_VERSION:v%=%),$(GOLANGCILINT_VERSION)))

contrib/scripts/check-fipsonly.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ EXCLUDED_DIRS=(
1616
"tools/alignchecker"
1717
"tools/api-flaggen"
1818
"tools/complexity-diff"
19+
"tools/cloud-dep-check"
1920
"tools/crdcheck"
2021
"tools/crdlistgen"
2122
"tools/dev-doctor"

tools/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
include ../Makefile.defs
55

6-
SUBDIRS := alignchecker mount slogloggercheck metricslint feature-helm-generator
6+
SUBDIRS := alignchecker mount slogloggercheck metricslint feature-helm-generator cloud-dep-check
77

88
.PHONY: all $(SUBDIRS) clean install
99

tools/cloud-dep-check/Makefile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright Authors of Cilium
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
include ../../Makefile.defs
5+
6+
TARGET := cilium-cloud-dep-check
7+
8+
.PHONY: all $(TARGET) clean install check
9+
10+
all: $(TARGET)
11+
12+
$(TARGET):
13+
@$(ECHO_GO)
14+
$(QUIET)$(GO_BUILD) -o $@
15+
16+
# `make check` builds the linter and runs it against the cilium repository
17+
# root, verifying that each cilium-operator-* binary variant only contains
18+
# its own cloud-provider SDK dependencies.
19+
check: $(TARGET)
20+
$(QUIET)./$(TARGET) -root ../..
21+
22+
clean:
23+
@$(ECHO_CLEAN)
24+
-$(QUIET)rm -f $(TARGET)
25+
$(QUIET)$(GO_CLEAN)
26+
27+
install:
28+
$(QUIET)$(INSTALL) -m 0755 -d $(DESTDIR)$(BINDIR)
29+
$(QUIET)$(INSTALL) -m 0755 $(TARGET) $(DESTDIR)$(BINDIR)
30+

tools/cloud-dep-check/main.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Authors of Cilium
3+
4+
// cilium-cloud-dep-check verifies that each operator binary variant only
5+
// contains the cloud-provider SDKs that match its build tags.
6+
//
7+
// It enforces independence across the cilium-operator-* binaries:
8+
//
9+
// - cilium-operator-generic : must not contain AWS, Azure, or AlibabaCloud SDKs
10+
// - cilium-operator-aws : must not contain Azure or AlibabaCloud SDKs
11+
// - cilium-operator-azure : must not contain AWS or AlibabaCloud SDKs
12+
// - cilium-operator-alibabacloud: must not contain AWS or Azure SDKs
13+
//
14+
// The check is implemented by invoking `go list -deps -tags <tags> ./operator`
15+
// from the cilium repository root and matching the resulting import paths
16+
// against the forbidden module prefixes for that binary.
17+
//
18+
// Usage:
19+
//
20+
// cilium-cloud-dep-check [-root <repo-root>] [-target <pkg>]
21+
//
22+
// Exit code is non-zero if any forbidden dependency is found.
23+
package main
24+
25+
import (
26+
"flag"
27+
"fmt"
28+
"os"
29+
"os/exec"
30+
"path/filepath"
31+
"slices"
32+
"sort"
33+
"strings"
34+
)
35+
36+
// cloudSDKs maps a cloud-provider name to the list of import-path prefixes
37+
// owned by that provider's SDK. A package is considered to belong to the
38+
// provider if its import path starts with any of the listed prefixes.
39+
var cloudSDKs = map[string][]string{
40+
"aws": {
41+
"github.com/aws/aws-sdk-go-v2",
42+
"github.com/aws/smithy-go",
43+
},
44+
"azure": {
45+
"github.com/Azure/azure-sdk-for-go",
46+
"github.com/AzureAD/",
47+
},
48+
"alibabacloud": {
49+
"github.com/aliyun/alibaba-cloud-sdk-go",
50+
},
51+
}
52+
53+
// binary describes one cilium-operator binary variant.
54+
type binary struct {
55+
name string // human-readable binary name
56+
buildTags []string // -tags value passed to `go list`
57+
allowed []string // cloud SDKs allowed to appear in this binary
58+
}
59+
60+
// binaries is the canonical list of cross-checked binary configurations.
61+
// Keep this in sync with operator/Makefile.
62+
var binaries = []binary{
63+
{
64+
name: "cilium-operator-generic",
65+
buildTags: []string{"ipam_provider_operator"},
66+
allowed: nil,
67+
},
68+
{
69+
name: "cilium-operator-aws",
70+
buildTags: []string{"ipam_provider_aws"},
71+
allowed: []string{"aws"},
72+
},
73+
{
74+
name: "cilium-operator-azure",
75+
buildTags: []string{"ipam_provider_azure"},
76+
allowed: []string{"azure"},
77+
},
78+
{
79+
name: "cilium-operator-alibabacloud",
80+
buildTags: []string{"ipam_provider_alibabacloud"},
81+
allowed: []string{"alibabacloud"},
82+
},
83+
{
84+
name: "cilium-operator (combined)",
85+
buildTags: []string{
86+
"ipam_provider_aws",
87+
"ipam_provider_azure",
88+
"ipam_provider_operator",
89+
"ipam_provider_alibabacloud",
90+
},
91+
allowed: []string{"aws", "azure", "alibabacloud"},
92+
},
93+
}
94+
95+
// listDeps runs `go list -deps -tags <tags> <target>` and returns the unique
96+
// list of import paths.
97+
func listDeps(repoRoot, target string, tags []string) ([]string, error) {
98+
args := []string{"list", "-deps"}
99+
if len(tags) > 0 {
100+
args = append(args, "-tags", strings.Join(tags, ","))
101+
}
102+
args = append(args, target)
103+
cmd := exec.Command("go", args...)
104+
cmd.Dir = repoRoot
105+
cmd.Stderr = os.Stderr
106+
out, err := cmd.Output()
107+
if err != nil {
108+
return nil, fmt.Errorf("go list failed: %w", err)
109+
}
110+
deps := strings.Split(strings.TrimSpace(string(out)), "\n")
111+
sort.Strings(deps)
112+
return deps, nil
113+
}
114+
115+
// classify returns the cloud name if pkg belongs to a known cloud SDK, or "".
116+
func classify(pkg string) string {
117+
for cloud, prefixes := range cloudSDKs {
118+
for _, p := range prefixes {
119+
// Strip a trailing slash for prefix-only matches like "AzureAD/".
120+
trimmed := strings.TrimSuffix(p, "/")
121+
if pkg == trimmed || strings.HasPrefix(pkg, trimmed+"/") {
122+
return cloud
123+
}
124+
}
125+
}
126+
return ""
127+
}
128+
129+
// check verifies a single binary configuration. It returns the list of
130+
// forbidden packages found in its dependency closure.
131+
func check(repoRoot, target string, b binary) (map[string][]string, error) {
132+
deps, err := listDeps(repoRoot, target, b.buildTags)
133+
if err != nil {
134+
return nil, err
135+
}
136+
forbidden := map[string][]string{}
137+
for _, d := range deps {
138+
cloud := classify(d)
139+
if cloud == "" {
140+
continue
141+
}
142+
if slices.Contains(b.allowed, cloud) {
143+
continue
144+
}
145+
forbidden[cloud] = append(forbidden[cloud], d)
146+
}
147+
return forbidden, nil
148+
}
149+
150+
func main() {
151+
var (
152+
repoRoot string
153+
target string
154+
)
155+
flag.StringVar(&repoRoot, "root", ".", "path to the cilium repository root")
156+
flag.StringVar(&target, "target", "./operator", "go package to analyze")
157+
flag.Parse()
158+
159+
abs, err := filepath.Abs(repoRoot)
160+
if err != nil {
161+
fmt.Fprintf(os.Stderr, "cannot resolve repo root %q: %v\n", repoRoot, err)
162+
os.Exit(2)
163+
}
164+
165+
failures := 0
166+
for _, b := range binaries {
167+
fmt.Printf("==> checking %s (tags: %s)\n", b.name, strings.Join(b.buildTags, ","))
168+
forbidden, err := check(abs, target, b)
169+
if err != nil {
170+
fmt.Fprintf(os.Stderr, " ERROR: %v\n", err)
171+
failures++
172+
continue
173+
}
174+
if len(forbidden) == 0 {
175+
fmt.Println(" OK")
176+
continue
177+
}
178+
failures++
179+
// Stable iteration order for output.
180+
clouds := make([]string, 0, len(forbidden))
181+
for k := range forbidden {
182+
clouds = append(clouds, k)
183+
}
184+
sort.Strings(clouds)
185+
for _, cloud := range clouds {
186+
pkgs := forbidden[cloud]
187+
fmt.Printf(" FAIL: forbidden %s SDK packages found (%d):\n", cloud, len(pkgs))
188+
for _, p := range pkgs {
189+
fmt.Printf(" - %s\n", p)
190+
}
191+
}
192+
}
193+
194+
if failures > 0 {
195+
fmt.Fprintf(os.Stderr, "\ncloud dependency check failed for %d binary configuration(s)\n", failures)
196+
os.Exit(1)
197+
}
198+
fmt.Println("\nall binary configurations are independent")
199+
}

0 commit comments

Comments
 (0)