Skip to content

Commit 03f3a01

Browse files
committed
(fix): Preserve immutable fields during upgrade (llamastack#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 (cherry picked from commit e2cc886)
1 parent 5a4721d commit 03f3a01

File tree

2 files changed

+107
-1
lines changed

2 files changed

+107
-1
lines changed

pkg/deploy/deploy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func ApplyDeployment(ctx context.Context, cli client.Client, scheme *runtime.Sch
3535
if !reflect.DeepEqual(found.Spec, deployment.Spec) {
3636
logger.Info("Updating Deployment", "deployment", deployment.Name)
3737

38-
// Preserve the existing selector to avoid immutable field error
38+
// Preserve the existing selector to avoid immutable field error during upgrades
3939
deployment.Spec.Selector = found.Spec.Selector
4040

4141
// Use server-side apply to merge changes properly

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)