Skip to content

Commit 1d27a91

Browse files
committed
fix(applycheck): drop false-positive blockers on virtual-link names
BondConfig.name, VLANConfig.name, and BridgeConfig.name describe *virtual links being created* by the apply, not references to existing links. The walker emitted them as Phase 1 link refs anyway, so every legitimate 'add a new bond / vlan / bridge to a node' apply tripped its own pre-flight gate with a 'declared link bond99 not found' blocker on the doc that was supposed to create bond99 in the first place. Operators would have had to pass --skip-resource-validation on every such apply, defeating the gate. New contract per kind: - LinkConfig.name -> emitted (override case dominates; the operator is usually tweaking ens5 / eth0 settings on a NIC that already exists, and a typo here is the most common Phase-1 catch). - VLANConfig.link -> emitted (parent must exist), .name skipped. - BondConfig.links[] -> emitted (slaves must exist), .name skipped. - BridgeConfig.ports[] -> emitted (ports must exist), .name skipped. Pinned with explicit assertion: VLANConfig.name (eth0.4000 in the fixture) MUST NOT surface as a ref. Verified on dev17 OCI cluster: adding 'BondConfig{name: bond99, links: [ghost0, ens5]}' now blocks only on ghost0 (the bad slave), not on bond99 (the new bond being created). Refs: #172 Signed-off-by: Aleksei Sviridkin <f@lex.la>
1 parent 491c6f7 commit 1d27a91

2 files changed

Lines changed: 42 additions & 13 deletions

File tree

pkg/applycheck/refs.go

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,17 @@ type multidocHandler func(refs []Ref, doc map[string]any, basePath string) []Ref
165165

166166
//nolint:gochecknoglobals // dispatch table for multidoc kinds; static after init.
167167
var multidocHandlers = map[string]multidocHandler{
168+
// LinkConfig.name is emitted: the typical case is an override of an
169+
// existing physical NIC (ens5 settings, MTU on eth0), where the
170+
// operator wants validation to catch a typoed interface name. The
171+
// rarer "create-a-fresh-virtual-link" case is covered by BondConfig/
172+
// BridgeConfig/VLANConfig below, which intentionally do NOT validate
173+
// their own .name field — those names describe a virtual link being
174+
// created by the apply, not an existing one to reference.
168175
"LinkConfig": handleNameOnly,
169-
"BondConfig": handleNamePlusList("links"),
170-
"VLANConfig": handleNamePlusParent,
171-
"BridgeConfig": handleNamePlusList("ports"),
176+
"BondConfig": handleListOnly("links"),
177+
"VLANConfig": handleParentOnly,
178+
"BridgeConfig": handleListOnly("ports"),
172179
"Layer2VIPConfig": handleLayer2VIP,
173180
"UserVolumeConfig": handleUserVolume,
174181
}
@@ -189,22 +196,26 @@ func handleNameOnly(refs []Ref, doc map[string]any, basePath string) []Ref {
189196
return appendNameRef(refs, doc, basePath)
190197
}
191198

192-
func handleNamePlusList(listKey string) multidocHandler {
199+
// handleListOnly emits only the list-valued slaves/ports of the doc,
200+
// not the doc's own .name. Used for BondConfig (its .name describes a
201+
// virtual bond being created by the apply; the .links[] members are
202+
// pre-existing physical NICs that must be present).
203+
func handleListOnly(listKey string) multidocHandler {
193204
return func(refs []Ref, doc map[string]any, basePath string) []Ref {
194-
refs = appendNameRef(refs, doc, basePath)
195-
196205
return appendListRefs(refs, doc, listKey, basePath+"."+listKey)
197206
}
198207
}
199208

200-
func handleNamePlusParent(refs []Ref, doc map[string]any, basePath string) []Ref {
201-
refs = appendNameRef(refs, doc, basePath)
202-
203-
if parent, ok := doc["link"].(string); ok && parent != "" {
204-
refs = append(refs, Ref{Kind: RefKindLink, Name: parent, Source: basePath + ".link"})
209+
// handleParentOnly emits only the parent reference of a VLAN doc, not
210+
// its own .name. The .name is the VLAN tag's child link name (a new
211+
// virtual link being created); the .link is the parent that must exist.
212+
func handleParentOnly(refs []Ref, doc map[string]any, basePath string) []Ref {
213+
parent, ok := doc["link"].(string)
214+
if !ok || parent == "" {
215+
return refs
205216
}
206217

207-
return refs
218+
return append(refs, Ref{Kind: RefKindLink, Name: parent, Source: basePath + ".link"})
208219
}
209220

210221
func handleLayer2VIP(refs []Ref, doc map[string]any, basePath string) []Ref {

pkg/applycheck/refs_test.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,31 @@ func TestWalkRefs_v1_12_ExtractsLinksFromMultidocAndDiskSelector(t *testing.T) {
125125
t.Fatalf("WalkRefs error: %v", err)
126126
}
127127

128-
wantLinks := []string{"eth0", "eth0.4000", "bond0", "eth1", "eth2", "br0", "eth3"}
128+
// Walker emits only references to *existing* links, not names of
129+
// virtual links being created by the apply. So:
130+
// - LinkConfig.name -> emitted (override of an existing physical NIC)
131+
// - VLANConfig.link -> emitted (the parent link must exist)
132+
// - VLANConfig.name -> NOT emitted (the VLAN child is being created)
133+
// - BondConfig.links[] -> emitted (slave NICs must exist)
134+
// - BondConfig.name -> NOT emitted (the bond is being created)
135+
// - BridgeConfig.ports[] -> emitted (port NICs must exist)
136+
// - BridgeConfig.name -> NOT emitted (the bridge is being created)
137+
wantLinks := []string{"eth0" /* LinkConfig.name + VLANConfig.link parent */, "eth1", "eth2" /* BondConfig.links */, "eth3" /* BridgeConfig.ports */}
129138
for _, name := range wantLinks {
130139
if _, ok := findRef(refs, applycheck.RefKindLink, name); !ok {
131140
t.Errorf("expected link ref for %q, got refs=%+v", name, refs)
132141
}
133142
}
134143

144+
// Names of virtual links being created must NOT surface (bond0 is
145+
// a BondConfig.name + a BridgeConfig.ports[] entry; the .name path
146+
// should not appear, only the .ports[] one. eth0.4000 is a
147+
// VLANConfig.name with no other references — should not appear at
148+
// all).
149+
if _, ok := findRef(refs, applycheck.RefKindLink, "eth0.4000"); ok {
150+
t.Errorf("VLANConfig.name (eth0.4000) leaked as a ref; only the .link parent should validate")
151+
}
152+
135153
// Layer2VIPConfig.link is a distinct ref site. Use a name that no other
136154
// document mentions (bond1) so the assertion proves handleLayer2VIP
137155
// actually emitted the ref, rather than incidentally picking it up

0 commit comments

Comments
 (0)