Skip to content

Commit e2cc886

Browse files
(fix): Preserve immutable fields during upgrade (#131)
During upgrades, if we do server-side apply without preserving the selectors field, we get 'field is immutable' error which blocks deployment updates and also causes continuous operator reconciliation. Preserving existing selector values fixes the issue. Approved-by: rhdedgar Approved-by: mfleader
1 parent 780ac21 commit e2cc886

File tree

2 files changed

+112
-2
lines changed

2 files changed

+112
-2
lines changed

pkg/deploy/deploy.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@ func ApplyDeployment(ctx context.Context, cli client.Client, scheme *runtime.Sch
3030
return fmt.Errorf("failed to fetch deployment: %w", err)
3131
}
3232

33-
// For updates, use server-side apply to preserve annotations and labels
34-
// that might have been added by other operators (like OpenTelemetry)
33+
// For updates, preserve the existing selector since it's immutable
34+
// and use server-side apply for other fields
3535
if !reflect.DeepEqual(found.Spec, deployment.Spec) {
3636
logger.Info("Updating Deployment", "deployment", deployment.Name)
37+
38+
// Preserve the existing selector to avoid immutable field error during upgrades
39+
deployment.Spec.Selector = found.Spec.Selector
40+
3741
// Use server-side apply to merge changes properly
3842
// Ensure the deployment has proper TypeMeta for server-side apply
3943
deployment.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind("Deployment"))

pkg/deploy/deploy_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package deploy
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
llamav1alpha1 "github.com/llamastack/llama-stack-k8s-operator/api/v1alpha1"
8+
"github.com/stretchr/testify/require"
9+
appsv1 "k8s.io/api/apps/v1"
10+
corev1 "k8s.io/api/core/v1"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/types"
13+
logf "sigs.k8s.io/controller-runtime/pkg/log"
14+
)
15+
16+
func TestApplyDeploymentPreservesSelector(t *testing.T) {
17+
ctx := context.Background()
18+
logger := logf.Log.WithName("test-apply-deployment")
19+
20+
instance := &llamav1alpha1.LlamaStackDistribution{
21+
ObjectMeta: metav1.ObjectMeta{
22+
Name: "test-instance",
23+
Namespace: "default",
24+
UID: "test-uid",
25+
},
26+
}
27+
28+
deploymentName := "test-deployment-selector"
29+
namespace := "default"
30+
31+
// Initial deployment with a specific selector
32+
initialDeployment := &appsv1.Deployment{
33+
ObjectMeta: metav1.ObjectMeta{
34+
Name: deploymentName,
35+
Namespace: namespace,
36+
},
37+
Spec: appsv1.DeploymentSpec{
38+
Selector: &metav1.LabelSelector{
39+
MatchLabels: map[string]string{"app": "initial"},
40+
},
41+
Template: corev1.PodTemplateSpec{
42+
ObjectMeta: metav1.ObjectMeta{
43+
Labels: map[string]string{"app": "initial"},
44+
},
45+
Spec: corev1.PodSpec{
46+
Containers: []corev1.Container{
47+
{
48+
Name: "llamastack",
49+
Image: "quay.io/llamastack/llama-stack-k8s-operator:v0.0.1",
50+
},
51+
},
52+
},
53+
},
54+
},
55+
}
56+
57+
err := ApplyDeployment(ctx, k8sClient, k8sClient.Scheme(), instance, initialDeployment.DeepCopy(), logger)
58+
require.NoError(t, err)
59+
60+
// Verify the deployment was created
61+
foundDeployment := &appsv1.Deployment{}
62+
err = k8sClient.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, foundDeployment)
63+
require.NoError(t, err)
64+
require.NotNil(t, foundDeployment.Spec.Selector)
65+
require.Equal(t, "initial", foundDeployment.Spec.Selector.MatchLabels["app"])
66+
67+
// Updated deployment with changes.
68+
// The fix should preserve the original selector.
69+
updatedDeployment := &appsv1.Deployment{
70+
ObjectMeta: metav1.ObjectMeta{
71+
Name: deploymentName,
72+
Namespace: namespace,
73+
},
74+
Spec: appsv1.DeploymentSpec{
75+
Selector: &metav1.LabelSelector{
76+
MatchLabels: map[string]string{"app": "initial"}, // Must match existing selector
77+
},
78+
Template: corev1.PodTemplateSpec{
79+
ObjectMeta: metav1.ObjectMeta{
80+
Labels: map[string]string{"app": "initial"},
81+
},
82+
Spec: corev1.PodSpec{
83+
Containers: []corev1.Container{
84+
{
85+
Name: "llamastack",
86+
Image: "quay.io/llamastack/llama-stack-k8s-operator:v0.0.2",
87+
},
88+
},
89+
},
90+
},
91+
},
92+
}
93+
94+
err = ApplyDeployment(ctx, k8sClient, k8sClient.Scheme(), instance, updatedDeployment.DeepCopy(), logger)
95+
require.NoError(t, err)
96+
97+
err = k8sClient.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, foundDeployment)
98+
require.NoError(t, err)
99+
100+
// The selector should be preserved from the initial deployment
101+
require.NotNil(t, foundDeployment.Spec.Selector)
102+
require.Equal(t, "initial", foundDeployment.Spec.Selector.MatchLabels["app"])
103+
104+
// And the other updates should be applied
105+
require.Equal(t, "quay.io/llamastack/llama-stack-k8s-operator:v0.0.2", foundDeployment.Spec.Template.Spec.Containers[0].Image)
106+
}

0 commit comments

Comments
 (0)