Skip to content

Commit 99ec99d

Browse files
chris-rockclaude
andauthored
⭐️ use Server-Side Apply (SSA) for resource reconciliation (#1381)
* ⭐️ migrate to Server-Side Apply (SSA) for resource reconciliation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ⭐️ replace custom SSA Apply with upstream CreateOrUpdate Replace the custom Server-Side Apply implementation with controller-runtime's upstream CreateOrUpdate pattern, which is simpler and avoids issues with server-set defaults being zeroed out on every reconcile. Key changes: - Delete custom apply.go/apply_test.go - Restore CreateOrUpdate wrapper from main branch - Add field-level update helpers (UpdateCronJobFields, UpdateDeploymentFields, UpdateDaemonSetFields) to preserve server-set defaults like DNSPolicy, SchedulerName, TerminationGracePeriodSeconds, etc. - Replace DeleteAllOf with DeleteCompletedJobs to avoid killing running scans - Add TerminationMessagePath/TerminationMessagePolicy to all container specs to prevent unnecessary diffs against server defaults Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6166e68 commit 99ec99d

20 files changed

Lines changed: 457 additions & 1795 deletions

config/rbac/role.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ rules:
8585
resources:
8686
- jobs
8787
verbs:
88-
- delete
8988
- deletecollection
9089
- get
9190
- list

controllers/container_image/deployment_handler.go

Lines changed: 21 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package container_image
55

66
import (
77
"context"
8-
"reflect"
98

109
"go.mondoo.com/mondoo-operator/api/v1alpha2"
1110
"go.mondoo.com/mondoo-operator/pkg/utils/k8s"
@@ -16,6 +15,7 @@ import (
1615
"k8s.io/apimachinery/pkg/labels"
1716
ctrl "sigs.k8s.io/controller-runtime"
1817
"sigs.k8s.io/controller-runtime/pkg/client"
18+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1919
)
2020

2121
var logger = ctrl.Log.WithName("k8s-images-scanning")
@@ -71,56 +71,31 @@ func (n *DeploymentHandler) syncCronJob(ctx context.Context) error {
7171
return err
7272
}
7373

74-
updated, err := n.syncConfigMap(ctx, clusterUid)
75-
if err != nil {
74+
if err := n.syncConfigMap(ctx, clusterUid); err != nil {
7675
return err
7776
}
7877

79-
// TODO: for CronJob we might consider triggering the CronJob now after the ConfigMap has been changed. It will make sense from the
80-
// user perspective to want to run the jobs after you have updated the config.
81-
if updated {
82-
logger.Info(
83-
"Inventory ConfigMap was just updated. The job will use the new config during the next scheduled run.",
84-
"namespace", n.Mondoo.Namespace,
85-
"name", CronJobName(n.Mondoo.Name))
86-
}
87-
8878
// Reconcile private registry secrets (merges multiple secrets if needed)
8979
privateRegistrySecretName, err := k8s.ReconcilePrivateRegistriesSecret(ctx, n.KubeClient, n.Mondoo)
9080
if err != nil {
9181
logger.Error(err, "Failed to reconcile private registry secrets")
9282
return err
9383
}
9484

95-
existing := &batchv1.CronJob{}
9685
desired := CronJob(mondooClientImage, integrationMrn, clusterUid, privateRegistrySecretName, n.Mondoo, *n.MondooOperatorConfig)
97-
if err := ctrl.SetControllerReference(n.Mondoo, desired, n.KubeClient.Scheme()); err != nil {
98-
logger.Error(err, "Failed to set ControllerReference", "namespace", desired.Namespace, "name", desired.Name)
99-
return err
100-
}
101-
102-
created, err := k8s.CreateIfNotExist(ctx, n.KubeClient, existing, desired)
86+
obj := &batchv1.CronJob{ObjectMeta: metav1.ObjectMeta{Name: desired.Name, Namespace: desired.Namespace}}
87+
op, err := k8s.CreateOrUpdate(ctx, n.KubeClient, obj, n.Mondoo, logger, func() error {
88+
k8s.UpdateCronJobFields(obj, desired)
89+
return nil
90+
})
10391
if err != nil {
104-
logger.Error(err, "Failed to create CronJob", "namespace", desired.Namespace, "name", desired.Name)
10592
return err
10693
}
10794

108-
if created {
109-
logger.Info("Created CronJob", "namespace", desired.Namespace, "name", desired.Name)
110-
} else if !k8s.AreCronJobsEqual(*existing, *desired) {
111-
existing.Spec.JobTemplate = desired.Spec.JobTemplate
112-
existing.Spec.Schedule = desired.Spec.Schedule
113-
existing.Spec.ConcurrencyPolicy = desired.Spec.ConcurrencyPolicy
114-
existing.SetOwnerReferences(desired.GetOwnerReferences())
115-
116-
// Remove completed/failed jobs because they won't be updated when the cronjob changes.
117-
// Active jobs are preserved to avoid killing in-progress scans.
95+
// When a CronJob is updated, remove completed Jobs so they don't linger with stale config
96+
if op == controllerutil.OperationResultUpdated {
11897
if err := k8s.DeleteCompletedJobs(ctx, n.KubeClient, n.Mondoo.Namespace, CronJobLabels(*n.Mondoo), logger); err != nil {
119-
return err
120-
}
121-
122-
if err := n.KubeClient.Update(ctx, existing); err != nil {
123-
logger.Error(err, "Failed to update CronJob", "namespace", existing.Namespace, "name", existing.Name)
98+
logger.Error(err, "Failed to clean up completed Jobs after CronJob update")
12499
return err
125100
}
126101
}
@@ -154,53 +129,29 @@ func (n *DeploymentHandler) syncCronJob(ctx context.Context) error {
154129
return nil
155130
}
156131

157-
// syncConfigMap syncs the inventory ConfigMap. Returns a boolean indicating whether the ConfigMap has been updated. It
158-
// can only be "true", if the ConfigMap existed before this reconcile cycle and the inventory was different from the
159-
// desired state.
160-
func (n *DeploymentHandler) syncConfigMap(ctx context.Context, clusterUid string) (bool, error) {
161-
existing := &corev1.ConfigMap{}
162-
132+
func (n *DeploymentHandler) syncConfigMap(ctx context.Context, clusterUid string) error {
163133
integrationMrn, err := k8s.TryGetIntegrationMrnForAuditConfig(ctx, n.KubeClient, *n.Mondoo)
164134
if err != nil {
165135
logger.Error(err, "failed to retrieve IntegrationMRN")
166-
return false, err
136+
return err
167137
}
168138

169139
desired, err := ConfigMap(integrationMrn, clusterUid, *n.Mondoo, *n.MondooOperatorConfig)
170140
if err != nil {
171141
logger.Error(err, "failed to generate desired ConfigMap with inventory")
172-
return false, err
173-
}
174-
175-
if err := ctrl.SetControllerReference(n.Mondoo, desired, n.KubeClient.Scheme()); err != nil {
176-
logger.Error(err, "Failed to set ControllerReference", "namespace", desired.Namespace, "name", desired.Name)
177-
return false, err
178-
}
179-
180-
created, err := k8s.CreateIfNotExist(ctx, n.KubeClient, existing, desired)
181-
if err != nil {
182-
logger.Error(err, "Failed to create inventory ConfigMap", "namespace", desired.Namespace, "name", desired.Name)
183-
return false, err
142+
return err
184143
}
185144

186-
if created {
187-
logger.Info("Created inventory ConfigMap", "namespace", desired.Namespace, "name", desired.Name)
188-
return false, nil
145+
obj := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: desired.Name, Namespace: desired.Namespace}}
146+
if _, err := k8s.CreateOrUpdate(ctx, n.KubeClient, obj, n.Mondoo, logger, func() error {
147+
obj.Labels = desired.Labels
148+
obj.Data = desired.Data
149+
return nil
150+
}); err != nil {
151+
return err
189152
}
190153

191-
updated := false
192-
if existing.Data["inventory"] != desired.Data["inventory"] ||
193-
!reflect.DeepEqual(existing.GetOwnerReferences(), desired.GetOwnerReferences()) {
194-
existing.Data["inventory"] = desired.Data["inventory"]
195-
existing.SetOwnerReferences(desired.GetOwnerReferences())
196-
197-
if err := n.KubeClient.Update(ctx, existing); err != nil {
198-
logger.Error(err, "Failed to update inventory ConfigMap", "namespace", existing.Namespace, "name", existing.Name)
199-
return false, err
200-
}
201-
updated = true
202-
}
203-
return updated, nil
154+
return nil
204155
}
205156

206157
func (n *DeploymentHandler) getCronJobsForAuditConfig(ctx context.Context) ([]batchv1.CronJob, error) {

controllers/container_image/deployment_handler_test.go

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package container_image
55

66
import (
77
"context"
8-
"fmt"
98
"testing"
109
"time"
1110

@@ -16,7 +15,6 @@ import (
1615
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1716
"k8s.io/apimachinery/pkg/runtime"
1817
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
19-
ctrl "sigs.k8s.io/controller-runtime"
2018
"sigs.k8s.io/controller-runtime/pkg/client"
2119
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2220

@@ -65,17 +63,13 @@ func (s *DeploymentHandlerSuite) TestReconcile_Create() {
6563
s.NoError(err)
6664

6765
expected := CronJob(image, "", test.KubeSystemNamespaceUid, "", &s.auditConfig, mondoov1alpha2.MondooOperatorConfig{})
68-
s.NoError(ctrl.SetControllerReference(&s.auditConfig, expected, d.KubeClient.Scheme()))
69-
70-
// Set some fields that the kube client sets
71-
expected.ResourceVersion = "1"
7266

7367
created := &batchv1.CronJob{}
7468
created.Name = expected.Name
7569
created.Namespace = expected.Namespace
7670
s.NoError(d.KubeClient.Get(s.ctx, client.ObjectKeyFromObject(created), created))
7771

78-
s.Equal(expected, created)
72+
s.Equal(expected.Spec, created.Spec)
7973
}
8074

8175
func (s *DeploymentHandlerSuite) TestReconcile_Create_CustomEnvVars() {
@@ -93,10 +87,6 @@ func (s *DeploymentHandlerSuite) TestReconcile_Create_CustomEnvVars() {
9387
s.NoError(err)
9488

9589
expected := CronJob(image, "", test.KubeSystemNamespaceUid, "", &s.auditConfig, mondoov1alpha2.MondooOperatorConfig{})
96-
s.NoError(ctrl.SetControllerReference(&s.auditConfig, expected, d.KubeClient.Scheme()))
97-
98-
// Set some fields that the kube client sets
99-
expected.ResourceVersion = "1"
10090

10191
created := &batchv1.CronJob{}
10292
created.Name = expected.Name
@@ -107,7 +97,7 @@ func (s *DeploymentHandlerSuite) TestReconcile_Create_CustomEnvVars() {
10797
utils.SortEnvVars(expected.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env)
10898
utils.SortEnvVars(created.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env)
10999

110-
s.Equal(expected, created)
100+
s.Equal(expected.Spec, created.Spec)
111101
}
112102

113103
func (s *DeploymentHandlerSuite) TestReconcile_CreateWithCustomImage() {
@@ -126,17 +116,13 @@ func (s *DeploymentHandlerSuite) TestReconcile_CreateWithCustomImage() {
126116
s.NoError(err)
127117

128118
expected := CronJob(image, "", test.KubeSystemNamespaceUid, "", &s.auditConfig, mondoov1alpha2.MondooOperatorConfig{})
129-
s.NoError(ctrl.SetControllerReference(&s.auditConfig, expected, d.KubeClient.Scheme()))
130-
131-
// Set some fields that the kube client sets
132-
expected.ResourceVersion = "1"
133119

134120
created := &batchv1.CronJob{}
135121
created.Name = expected.Name
136122
created.Namespace = expected.Namespace
137123
s.NoError(d.KubeClient.Get(s.ctx, client.ObjectKeyFromObject(created), created))
138124

139-
s.Equal(expected, created)
125+
s.Equal(expected.Spec, created.Spec)
140126
}
141127

142128
func (s *DeploymentHandlerSuite) TestReconcile_CreateWithCustomSchedule() {
@@ -192,17 +178,13 @@ func (s *DeploymentHandlerSuite) TestReconcile_Create_PrivateRegistriesSecret()
192178
s.NoError(err)
193179

194180
expected := CronJob(image, "", test.KubeSystemNamespaceUid, s.auditConfig.Spec.Scanner.PrivateRegistriesPullSecretRef.Name, &s.auditConfig, mondoov1alpha2.MondooOperatorConfig{})
195-
s.NoError(ctrl.SetControllerReference(&s.auditConfig, expected, d.KubeClient.Scheme()))
196-
197-
// Set some fields that the kube client sets
198-
expected.ResourceVersion = "1"
199181

200182
created := &batchv1.CronJob{}
201183
created.Name = expected.Name
202184
created.Namespace = expected.Namespace
203185
s.NoError(d.KubeClient.Get(s.ctx, client.ObjectKeyFromObject(created), created))
204186

205-
s.Equal(expected, created)
187+
s.Equal(expected.Spec, created.Spec)
206188
}
207189

208190
func (s *DeploymentHandlerSuite) TestReconcile_Create_MultiplePrivateRegistriesSecrets() {
@@ -292,17 +274,13 @@ func (s *DeploymentHandlerSuite) TestReconcile_Create_ConsoleIntegration() {
292274
s.NoError(err)
293275

294276
expected := CronJob(image, integrationMrn, test.KubeSystemNamespaceUid, "", &s.auditConfig, mondoov1alpha2.MondooOperatorConfig{})
295-
s.NoError(ctrl.SetControllerReference(&s.auditConfig, expected, d.KubeClient.Scheme()))
296-
297-
// Set some fields that the kube client sets
298-
expected.ResourceVersion = "1"
299277

300278
created := &batchv1.CronJob{}
301279
created.Name = expected.Name
302280
created.Namespace = expected.Namespace
303281
s.NoError(d.KubeClient.Get(s.ctx, client.ObjectKeyFromObject(created), created))
304282

305-
s.Equal(expected, created)
283+
s.Equal(expected.Spec, created.Spec)
306284
}
307285

308286
func (s *DeploymentHandlerSuite) TestReconcile_Update() {
@@ -324,17 +302,13 @@ func (s *DeploymentHandlerSuite) TestReconcile_Update() {
324302
s.True(result.IsZero())
325303

326304
expected := CronJob(image, "", test.KubeSystemNamespaceUid, "", &s.auditConfig, mondoov1alpha2.MondooOperatorConfig{})
327-
s.NoError(ctrl.SetControllerReference(&s.auditConfig, expected, d.KubeClient.Scheme()))
328-
329-
// The second node has an updated cron job so resource version is +1
330-
expected.ResourceVersion = fmt.Sprintf("%d", 2)
331305

332306
created := &batchv1.CronJob{}
333307
created.Name = expected.Name
334308
created.Namespace = expected.Namespace
335309
s.NoError(d.KubeClient.Get(s.ctx, client.ObjectKeyFromObject(created), created))
336310

337-
s.Equal(expected, created)
311+
s.Equal(expected.Spec, created.Spec)
338312
}
339313

340314
func (s *DeploymentHandlerSuite) TestReconcile_K8sContainerImageScanningStatus() {

controllers/container_image/resources.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ func CronJob(image, integrationMrn, clusterUid, privateRegistrySecretName string
9898
MountPath: "/tmp",
9999
},
100100
},
101-
Env: envVars,
101+
Env: envVars,
102+
TerminationMessagePath: "/dev/termination-log",
103+
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
102104
},
103105
},
104106
ServiceAccountName: m.Spec.Scanner.ServiceAccountName,

0 commit comments

Comments
 (0)