Skip to content

Commit 2dc5a44

Browse files
committed
fix(meta): derive VM role from pod owner for snapshot decisions
ShouldSnapshotVM under main-only parsed the VM name with a last-dash heuristic, so a toolbox named with a trailing -0 was misread as the main agent and snapshotted. Role now comes from the pod's CocoonSet owner via ExtractAgentSlot; the deprecated ExtractSlotFromVMName/InferRoleFromVMName pair is removed.
1 parent c3e35cc commit 2dc5a44

5 files changed

Lines changed: 88 additions & 68 deletions

File tree

meta/meta_test.go

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,6 @@ func TestVMNamingHelpers(t *testing.T) {
1414
if got := VMNameForPod("prod", "toolbox"); got != "vk-prod-toolbox" {
1515
t.Fatalf("pod vm name mismatch: got %q", got)
1616
}
17-
if got := ExtractSlotFromVMName("vk-prod-demo-2"); got != 2 {
18-
t.Fatalf("slot mismatch: got %d", got)
19-
}
20-
if got := ExtractSlotFromVMName("vk-prod-toolbox"); got != -1 {
21-
t.Fatalf("expected non-slot vm name to return -1, got %d", got)
22-
}
23-
}
24-
25-
func TestInferRoleFromVMName(t *testing.T) {
26-
if got := InferRoleFromVMName("vk-prod-demo-0"); got != RoleMain {
27-
t.Fatalf("expected role %q, got %q", RoleMain, got)
28-
}
29-
if got := InferRoleFromVMName("vk-prod-demo-3"); got != RoleSubAgent {
30-
t.Fatalf("expected role %q, got %q", RoleSubAgent, got)
31-
}
3217
}
3318

3419
func TestExtractAgentSlot(t *testing.T) {
@@ -54,10 +39,9 @@ func TestExtractAgentSlot(t *testing.T) {
5439
want: 3,
5540
},
5641
{
57-
// The legacy ExtractSlotFromVMName would mis-read this as
58-
// slot 2 because it splits at the last dash. ExtractAgentSlot
59-
// rejects it because the suffix after the agent prefix
60-
// contains a dash.
42+
// A naive last-dash split would misread this as slot 2;
43+
// ExtractAgentSlot rejects it because the suffix after the
44+
// agent prefix contains a dash.
6145
name: "toolbox with trailing digit is not an agent slot",
6246
ns: "prod",
6347
cocoonSet: "demo",
@@ -120,6 +104,50 @@ func TestInferRoleFromAgentSlot(t *testing.T) {
120104
}
121105
}
122106

107+
func TestRoleForPod(t *testing.T) {
108+
cocoonSetOwner := []metav1.OwnerReference{{Kind: KindCocoonSet, Name: "cs"}}
109+
cases := []struct {
110+
name string
111+
pod *corev1.Pod
112+
vmName string
113+
want string
114+
}{
115+
{
116+
name: "agent slot 0 is main",
117+
pod: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "ns", OwnerReferences: cocoonSetOwner}},
118+
vmName: "vk-ns-cs-0",
119+
want: RoleMain,
120+
},
121+
{
122+
name: "agent slot 2 is sub-agent",
123+
pod: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "ns", OwnerReferences: cocoonSetOwner}},
124+
vmName: "vk-ns-cs-2",
125+
want: RoleSubAgent,
126+
},
127+
{
128+
// Regression: toolbox "app-0" builds VM name "vk-ns-cs-app-0",
129+
// which a naive last-dash split would misread as agent slot 0.
130+
name: "toolbox named app-0 is not main",
131+
pod: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "ns", OwnerReferences: cocoonSetOwner}},
132+
vmName: "vk-ns-cs-app-0",
133+
want: RoleToolbox,
134+
},
135+
{
136+
name: "no CocoonSet owner is toolbox",
137+
pod: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "ns"}},
138+
vmName: "vk-ns-cs-0",
139+
want: RoleToolbox,
140+
},
141+
}
142+
for _, tt := range cases {
143+
t.Run(tt.name, func(t *testing.T) {
144+
if got := RoleForPod(tt.pod, tt.vmName); got != tt.want {
145+
t.Errorf("got %v, want %v", got, tt.want)
146+
}
147+
})
148+
}
149+
}
150+
123151
func TestConnectionType(t *testing.T) {
124152
cases := []struct {
125153
name string

meta/owner.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,18 @@ func HasCocoonTolerationKey(tolerations []corev1.Toleration) bool {
1818

1919
// IsOwnedByCocoonSet reports whether any owner reference is a CocoonSet.
2020
func IsOwnedByCocoonSet(ownerRefs []metav1.OwnerReference) bool {
21-
return slices.ContainsFunc(ownerRefs, func(ref metav1.OwnerReference) bool {
22-
return ref.Kind == KindCocoonSet
23-
})
21+
return CocoonSetOwnerName(ownerRefs) != ""
22+
}
23+
24+
// CocoonSetOwnerName returns the name of the CocoonSet owner reference, or
25+
// "" if none is present.
26+
func CocoonSetOwnerName(ownerRefs []metav1.OwnerReference) string {
27+
for _, ref := range ownerRefs {
28+
if ref.Kind == KindCocoonSet {
29+
return ref.Name
30+
}
31+
}
32+
return ""
2433
}
2534

2635
// OwnerDeploymentName extracts the deployment name from a ReplicaSet

meta/vmname.go

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package meta
33
import (
44
"strconv"
55
"strings"
6+
7+
corev1 "k8s.io/api/core/v1"
68
)
79

810
// VMNameForDeployment builds a deterministic VM name from a deployment and slot index.
@@ -56,32 +58,11 @@ func InferRoleFromAgentSlot(slot int) string {
5658
}
5759
}
5860

59-
// ExtractSlotFromVMName parses the trailing slot index from a VM name,
60-
// or -1 if absent.
61-
//
62-
// Deprecated: misclassifies toolbox names with numeric suffixes (e.g.
63-
// "vk-NS-CS-db-2" → slot 2). Prefer ExtractAgentSlot.
64-
func ExtractSlotFromVMName(vmName string) int {
65-
_, after, ok := lastCut(vmName, "-")
66-
if !ok {
67-
return -1
68-
}
69-
n, err := strconv.Atoi(after)
70-
if err != nil {
71-
return -1
72-
}
73-
return n
74-
}
75-
76-
// InferRoleFromVMName returns RoleMain for slot 0, RoleSubAgent otherwise.
77-
//
78-
// Deprecated: shares the toolbox-collision bug of ExtractSlotFromVMName.
79-
// Prefer InferRoleFromAgentSlot(ExtractAgentSlot(ns, cs, vmName)).
80-
func InferRoleFromVMName(vmName string) string {
81-
if ExtractSlotFromVMName(vmName) == 0 {
82-
return RoleMain
83-
}
84-
return RoleSubAgent
61+
// RoleForPod derives a pod's role (RoleMain, RoleSubAgent, RoleToolbox)
62+
// from its CocoonSet owner and VM name.
63+
func RoleForPod(pod *corev1.Pod, vmName string) string {
64+
cocoonSet := CocoonSetOwnerName(pod.OwnerReferences)
65+
return InferRoleFromAgentSlot(ExtractAgentSlot(pod.Namespace, cocoonSet, vmName))
8566
}
8667

8768
// lastCut is like strings.Cut but splits at the last occurrence of sep.

meta/vmspec.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,13 @@ func FromToolboxSpec(spec cocoonv1.ToolboxSpec, vmName string, snapshotPolicy co
115115
}
116116
}
117117

118-
// ShouldSnapshotVM reports whether the VM should be snapshotted based on its SnapshotPolicy.
119-
func ShouldSnapshotVM(spec VMSpec) bool {
118+
// ShouldSnapshotVM reports whether the VM should be snapshotted based on its SnapshotPolicy and role.
119+
func ShouldSnapshotVM(spec VMSpec, role string) bool {
120120
switch cocoonv1.SnapshotPolicy(spec.SnapshotPolicy).Default() {
121121
case cocoonv1.SnapshotPolicyNever:
122122
return false
123123
case cocoonv1.SnapshotPolicyMainOnly:
124-
return ExtractSlotFromVMName(spec.VMName) == 0
124+
return role == RoleMain
125125
default:
126126
return true
127127
}

meta/vmspec_test.go

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -218,28 +218,30 @@ func TestShouldSnapshotVM(t *testing.T) {
218218
cases := []struct {
219219
name string
220220
policy cocoonv1.SnapshotPolicy
221-
vmName string
221+
role string
222222
want bool
223223
}{
224-
{"always/slot0", cocoonv1.SnapshotPolicyAlways, "vk-prod-demo-0", true},
225-
{"always/slot3", cocoonv1.SnapshotPolicyAlways, "vk-prod-demo-3", true},
226-
{"always/toolbox", cocoonv1.SnapshotPolicyAlways, "vk-prod-my-tb", true},
227-
{"empty-defaults-to-always", "", "vk-prod-demo-0", true},
228-
{"empty-defaults-to-always/sub", "", "vk-prod-demo-2", true},
229-
230-
{"never/slot0", cocoonv1.SnapshotPolicyNever, "vk-prod-demo-0", false},
231-
{"never/slot3", cocoonv1.SnapshotPolicyNever, "vk-prod-demo-3", false},
232-
{"never/toolbox", cocoonv1.SnapshotPolicyNever, "vk-prod-my-tb", false},
233-
234-
{"main-only/slot0", cocoonv1.SnapshotPolicyMainOnly, "vk-prod-demo-0", true},
235-
{"main-only/slot3", cocoonv1.SnapshotPolicyMainOnly, "vk-prod-demo-3", false},
236-
{"main-only/toolbox", cocoonv1.SnapshotPolicyMainOnly, "vk-prod-my-tb", false},
224+
{"always/main", cocoonv1.SnapshotPolicyAlways, RoleMain, true},
225+
{"always/sub-agent", cocoonv1.SnapshotPolicyAlways, RoleSubAgent, true},
226+
{"always/toolbox", cocoonv1.SnapshotPolicyAlways, RoleToolbox, true},
227+
{"always/empty-role", cocoonv1.SnapshotPolicyAlways, "", true},
228+
{"empty-policy-defaults-to-always", "", RoleToolbox, true},
229+
230+
{"never/main", cocoonv1.SnapshotPolicyNever, RoleMain, false},
231+
{"never/sub-agent", cocoonv1.SnapshotPolicyNever, RoleSubAgent, false},
232+
{"never/toolbox", cocoonv1.SnapshotPolicyNever, RoleToolbox, false},
233+
{"never/empty-role", cocoonv1.SnapshotPolicyNever, "", false},
234+
235+
{"main-only/main", cocoonv1.SnapshotPolicyMainOnly, RoleMain, true},
236+
{"main-only/sub-agent", cocoonv1.SnapshotPolicyMainOnly, RoleSubAgent, false},
237+
{"main-only/toolbox", cocoonv1.SnapshotPolicyMainOnly, RoleToolbox, false},
238+
{"main-only/empty-role", cocoonv1.SnapshotPolicyMainOnly, "", false},
237239
}
238240
for _, c := range cases {
239241
t.Run(c.name, func(t *testing.T) {
240-
spec := VMSpec{VMName: c.vmName, SnapshotPolicy: string(c.policy)}
241-
if got := ShouldSnapshotVM(spec); got != c.want {
242-
t.Errorf("ShouldSnapshotVM(%s, %q) = %v, want %v", c.policy, c.vmName, got, c.want)
242+
spec := VMSpec{SnapshotPolicy: string(c.policy)}
243+
if got := ShouldSnapshotVM(spec, c.role); got != c.want {
244+
t.Errorf("ShouldSnapshotVM(%s, %q) = %v, want %v", c.policy, c.role, got, c.want)
243245
}
244246
})
245247
}

0 commit comments

Comments
 (0)