Skip to content

Commit 81f09c5

Browse files
committed
fix(runtime): preserve stable workload selectors
1 parent 689c08c commit 81f09c5

3 files changed

Lines changed: 135 additions & 3 deletions

File tree

operator/controllers/acp_probe_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
appsv1 "k8s.io/api/apps/v1"
7+
corev1 "k8s.io/api/core/v1"
78
"k8s.io/apimachinery/pkg/api/meta"
89
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
910
"k8s.io/apimachinery/pkg/runtime"
@@ -26,6 +27,9 @@ func newControllerTestScheme(t *testing.T) *runtime.Scheme {
2627
if err := appsv1.AddToScheme(scheme); err != nil {
2728
t.Fatalf("failed to register apps scheme: %v", err)
2829
}
30+
if err := corev1.AddToScheme(scheme); err != nil {
31+
t.Fatalf("failed to register core scheme: %v", err)
32+
}
2933
return scheme
3034
}
3135

operator/controllers/service_account_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
appsv1 "k8s.io/api/apps/v1"
8+
corev1 "k8s.io/api/core/v1"
89
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
910
"sigs.k8s.io/controller-runtime/pkg/client"
1011
"sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -99,3 +100,116 @@ func TestReconcileDeploymentKeepsRuntimePolicyLabelsOutOfSelector(t *testing.T)
99100
t.Fatalf("expected runtime policy label on pod template, got %#v", deployment.Spec.Template.Labels)
100101
}
101102
}
103+
104+
func TestReconcileDeploymentPreservesExistingSelectorOnUpgrade(t *testing.T) {
105+
scheme := newControllerTestScheme(t)
106+
oldSelector := map[string]string{
107+
"spritz.sh/name": "tidy-otter",
108+
ownerLabelKey: ownerLabelValue("user-1"),
109+
}
110+
spritz := &spritzv1.Spritz{
111+
ObjectMeta: metav1.ObjectMeta{Name: "tidy-otter", Namespace: "spritz-test"},
112+
Spec: spritzv1.SpritzSpec{
113+
Image: "example.com/openclaw:latest",
114+
Owner: spritzv1.SpritzOwner{ID: "user-1"},
115+
RuntimePolicy: &spritzv1.SpritzRuntimePolicy{
116+
NetworkProfile: "dev-cluster-only",
117+
MountProfile: "dev-default",
118+
ExposureProfile: "internal-acp",
119+
Revision: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
120+
},
121+
},
122+
}
123+
existingDeployment := &appsv1.Deployment{
124+
ObjectMeta: metav1.ObjectMeta{Name: spritz.Name, Namespace: spritz.Namespace},
125+
Spec: appsv1.DeploymentSpec{
126+
Selector: &metav1.LabelSelector{MatchLabels: oldSelector},
127+
Template: corev1.PodTemplateSpec{
128+
ObjectMeta: metav1.ObjectMeta{Labels: oldSelector},
129+
Spec: corev1.PodSpec{
130+
Containers: []corev1.Container{
131+
{Name: spritzContainerName, Image: spritz.Spec.Image},
132+
},
133+
},
134+
},
135+
},
136+
}
137+
k8sClient := fake.NewClientBuilder().
138+
WithScheme(scheme).
139+
WithObjects(spritz, existingDeployment).
140+
Build()
141+
reconciler := &SpritzReconciler{
142+
Client: k8sClient,
143+
Scheme: scheme,
144+
}
145+
146+
if err := reconciler.reconcileDeployment(context.Background(), spritz); err != nil {
147+
t.Fatalf("reconcileDeployment returned error: %v", err)
148+
}
149+
150+
deployment := &appsv1.Deployment{}
151+
if err := k8sClient.Get(
152+
context.Background(),
153+
client.ObjectKey{Name: spritz.Name, Namespace: spritz.Namespace},
154+
deployment,
155+
); err != nil {
156+
t.Fatalf("failed to load deployment: %v", err)
157+
}
158+
if deployment.Spec.Selector == nil {
159+
t.Fatal("expected deployment selector")
160+
}
161+
if deployment.Spec.Selector.MatchLabels["spritz.sh/name"] != spritz.Name {
162+
t.Fatalf("expected existing deployment selector to keep spritz name, got %#v", deployment.Spec.Selector.MatchLabels)
163+
}
164+
if deployment.Spec.Selector.MatchLabels[ownerLabelKey] != ownerLabelValue("user-1") {
165+
t.Fatalf("expected existing deployment selector to keep owner label, got %#v", deployment.Spec.Selector.MatchLabels)
166+
}
167+
if deployment.Spec.Template.Labels[runtimeNetworkProfileLabelKey] != "dev-cluster-only" {
168+
t.Fatalf("expected runtime policy label on pod template, got %#v", deployment.Spec.Template.Labels)
169+
}
170+
}
171+
172+
func TestReconcileServiceKeepsRuntimePolicyLabelsOutOfSelector(t *testing.T) {
173+
scheme := newControllerTestScheme(t)
174+
spritz := &spritzv1.Spritz{
175+
ObjectMeta: metav1.ObjectMeta{Name: "tidy-otter", Namespace: "spritz-test"},
176+
Spec: spritzv1.SpritzSpec{
177+
Image: "example.com/openclaw:latest",
178+
Owner: spritzv1.SpritzOwner{ID: "user-1"},
179+
RuntimePolicy: &spritzv1.SpritzRuntimePolicy{
180+
NetworkProfile: "dev-cluster-only",
181+
MountProfile: "dev-default",
182+
ExposureProfile: "internal-acp",
183+
Revision: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
184+
},
185+
},
186+
}
187+
k8sClient := fake.NewClientBuilder().
188+
WithScheme(scheme).
189+
WithObjects(spritz).
190+
Build()
191+
reconciler := &SpritzReconciler{
192+
Client: k8sClient,
193+
Scheme: scheme,
194+
}
195+
196+
if err := reconciler.reconcileService(context.Background(), spritz); err != nil {
197+
t.Fatalf("reconcileService returned error: %v", err)
198+
}
199+
200+
service := &corev1.Service{}
201+
if err := k8sClient.Get(
202+
context.Background(),
203+
client.ObjectKey{Name: spritz.Name, Namespace: spritz.Namespace},
204+
service,
205+
); err != nil {
206+
t.Fatalf("failed to load service: %v", err)
207+
}
208+
if len(service.Spec.Selector) != 1 ||
209+
service.Spec.Selector["spritz.sh/name"] != spritz.Name {
210+
t.Fatalf("expected stable service selector, got %#v", service.Spec.Selector)
211+
}
212+
if _, ok := service.Spec.Selector[runtimeNetworkProfileLabelKey]; ok {
213+
t.Fatalf("service selector must not depend on runtime policy labels: %#v", service.Spec.Selector)
214+
}
215+
}

operator/controllers/spritz_controller.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,6 @@ func (r *SpritzReconciler) acpHealthProbePath() string {
289289

290290
func (r *SpritzReconciler) reconcileDeployment(ctx context.Context, spritz *spritzv1.Spritz) error {
291291
labels := baseLabels(spritz)
292-
selectorLabels := deploymentSelectorLabels(spritz)
293292
annotations := baseAnnotations(spritz)
294293
workspaceSizeLimit := emptyDirSizeLimit("SPRITZ_WORKSPACE_SIZE_LIMIT", defaultWorkspaceSizeLimit)
295294
homeSizeLimit := emptyDirSizeLimit("SPRITZ_HOME_SIZE_LIMIT", defaultHomeSizeLimit)
@@ -301,11 +300,12 @@ func (r *SpritzReconciler) reconcileDeployment(ctx context.Context, spritz *spri
301300
return err
302301
}
303302

303+
selectorLabels := stableWorkloadSelectorLabels(deploy.Spec.Selector, spritz)
304304
deploy.Labels = mergeMaps(labels, spritz.Spec.Labels)
305305
deploy.Annotations = mergeMaps(deploy.Annotations, spritz.Spec.Annotations)
306306
deploy.Annotations = mergeMaps(deploy.Annotations, annotations)
307307
deploy.Spec.Selector = &metav1.LabelSelector{MatchLabels: selectorLabels}
308-
deploy.Spec.Template.Labels = labels
308+
deploy.Spec.Template.Labels = mergeMaps(selectorLabels, labels)
309309
deploy.Spec.Template.Annotations = mergeMaps(deploy.Spec.Template.Annotations, spritz.Spec.Annotations)
310310
deploy.Spec.Template.Annotations = mergeMaps(deploy.Spec.Template.Annotations, annotations)
311311

@@ -481,7 +481,7 @@ func (r *SpritzReconciler) reconcileService(ctx context.Context, spritz *spritzv
481481
labels := baseLabels(spritz)
482482
annotations := baseAnnotations(spritz)
483483
svc.Labels = mergeMaps(labels, spritz.Spec.Labels)
484-
svc.Spec.Selector = labels
484+
svc.Spec.Selector = deploymentSelectorLabels(spritz)
485485
svc.Annotations = mergeMaps(svc.Annotations, spritz.Spec.Annotations)
486486
svc.Annotations = mergeMaps(svc.Annotations, annotations)
487487

@@ -924,6 +924,20 @@ func deploymentSelectorLabels(spritz *spritzv1.Spritz) map[string]string {
924924
}
925925
}
926926

927+
func stableWorkloadSelectorLabels(
928+
selector *metav1.LabelSelector,
929+
spritz *spritzv1.Spritz,
930+
) map[string]string {
931+
if selector != nil && len(selector.MatchLabels) > 0 {
932+
preserved := map[string]string{}
933+
for key, value := range selector.MatchLabels {
934+
preserved[key] = value
935+
}
936+
return preserved
937+
}
938+
return deploymentSelectorLabels(spritz)
939+
}
940+
927941
func baseAnnotations(spritz *spritzv1.Spritz) map[string]string {
928942
annotations := map[string]string{}
929943
if spritz.Spec.RuntimePolicy != nil {

0 commit comments

Comments
 (0)