Skip to content

Commit 0fdb9cc

Browse files
committed
fix: Separate CronJob and feature-server ServiceAccounts
1 parent a9219d9 commit 0fdb9cc

3 files changed

Lines changed: 85 additions & 4 deletions

File tree

infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,36 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() {
124124
startingDeadlineSeconds := int64(5)
125125
Expect(cronJob.Spec.StartingDeadlineSeconds).To(Equal(&startingDeadlineSeconds))
126126

127+
// verify CronJob uses a dedicated SA, separate from the feature-server SA
128+
cronJobSAName := services.GetFeastServiceName(resource, services.CronJobFeastType)
129+
deploymentSAName := objMeta.Name
130+
podSpec := cronJob.Spec.JobTemplate.Spec.Template.Spec
131+
Expect(podSpec.ServiceAccountName).To(Equal(cronJobSAName))
132+
Expect(podSpec.ServiceAccountName).NotTo(Equal(deploymentSAName))
133+
134+
// verify the dedicated CronJob SA exists
135+
cronJobSA := &corev1.ServiceAccount{}
136+
err = k8sClient.Get(ctx, types.NamespacedName{
137+
Name: cronJobSAName,
138+
Namespace: objMeta.Namespace,
139+
}, cronJobSA)
140+
Expect(err).NotTo(HaveOccurred())
141+
Expect(controllerutil.HasControllerReference(cronJobSA)).To(BeTrue())
142+
143+
// verify restricted pod security context
144+
Expect(podSpec.SecurityContext).NotTo(BeNil())
145+
Expect(*podSpec.SecurityContext.RunAsNonRoot).To(BeTrue())
146+
Expect(podSpec.SecurityContext.SeccompProfile).NotTo(BeNil())
147+
Expect(podSpec.SecurityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault))
148+
149+
// verify restricted container security context
150+
for _, c := range append(podSpec.InitContainers, podSpec.Containers...) {
151+
Expect(c.SecurityContext).NotTo(BeNil())
152+
Expect(*c.SecurityContext.AllowPrivilegeEscalation).To(BeFalse())
153+
Expect(c.SecurityContext.Capabilities).NotTo(BeNil())
154+
Expect(c.SecurityContext.Capabilities.Drop).To(ContainElement(corev1.Capability("ALL")))
155+
}
156+
127157
checkCronJob(resource.Status.Applied.CronJob, cronJob.Spec)
128158
})
129159

@@ -244,6 +274,12 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() {
244274
Expect(cronJob.Spec.StartingDeadlineSeconds).To(Equal(&startingDeadlineSeconds))
245275
Expect(cronJob.Spec.JobTemplate.Spec.Parallelism).To(Equal(&int32Var))
246276

277+
// verify CronJob uses a dedicated SA, separate from the feature-server SA
278+
cronJobSAName := services.GetFeastServiceName(resource, services.CronJobFeastType)
279+
podSpec := cronJob.Spec.JobTemplate.Spec.Template.Spec
280+
Expect(podSpec.ServiceAccountName).To(Equal(cronJobSAName))
281+
Expect(podSpec.ServiceAccountName).NotTo(Equal(objMeta.Name))
282+
247283
checkCronJob(resource.Status.Applied.CronJob, cronJob.Spec)
248284
})
249285
})

infra/feast-operator/internal/controller/featurestore_controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ var _ = Describe("FeatureStore Controller", func() {
745745
saList := corev1.ServiceAccountList{}
746746
err = k8sClient.List(ctx, &saList, listOpts)
747747
Expect(err).NotTo(HaveOccurred())
748-
Expect(saList.Items).To(HaveLen(1))
748+
Expect(saList.Items).To(HaveLen(2))
749749

750750
svcList := corev1.ServiceList{}
751751
err = k8sClient.List(ctx, &svcList, listOpts)

infra/feast-operator/internal/controller/services/cronjob.go

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import (
1616
)
1717

1818
func (feast *FeastServices) deployCronJob() error {
19+
if err := feast.createCronJobServiceAccount(); err != nil {
20+
return feast.setFeastServiceCondition(err, CronJobFeastType)
21+
}
1922
if err := feast.createCronJobRole(); err != nil {
2023
return feast.setFeastServiceCondition(err, CronJobFeastType)
2124
}
@@ -146,8 +149,14 @@ func (feast *FeastServices) setCronJob(cronJob *batchv1.CronJob) error {
146149

147150
func (feast *FeastServices) getCronJobPodSpec() corev1.PodSpec {
148151
podSpec := corev1.PodSpec{
149-
ServiceAccountName: feast.initFeastSA().Name,
152+
ServiceAccountName: feast.initCronJobSA().Name,
150153
RestartPolicy: corev1.RestartPolicyNever,
154+
SecurityContext: &corev1.PodSecurityContext{
155+
RunAsNonRoot: boolPtr(true),
156+
SeccompProfile: &corev1.SeccompProfile{
157+
Type: corev1.SeccompProfileTypeRuntimeDefault,
158+
},
159+
},
151160
}
152161
feast.setCronJobContainers(&podSpec)
153162
return podSpec
@@ -167,7 +176,7 @@ func (feast *FeastServices) setCronJobContainers(podSpec *corev1.PodSpec) {
167176
}
168177

169178
func (feast *FeastServices) getCronJobContainer(containerName, cronJobCmd string) corev1.Container {
170-
return *getContainer(
179+
container := getContainer(
171180
containerName,
172181
"",
173182
[]string{
@@ -178,6 +187,42 @@ func (feast *FeastServices) getCronJobContainer(containerName, cronJobCmd string
178187
feast.Handler.FeatureStore.Status.Applied.CronJob.ContainerConfigs.ContainerConfigs,
179188
"",
180189
)
190+
container.SecurityContext = &corev1.SecurityContext{
191+
AllowPrivilegeEscalation: boolPtr(false),
192+
Capabilities: &corev1.Capabilities{
193+
Drop: []corev1.Capability{"ALL"},
194+
},
195+
}
196+
return *container
197+
}
198+
199+
func (feast *FeastServices) createCronJobServiceAccount() error {
200+
logger := log.FromContext(feast.Handler.Context)
201+
sa := feast.initCronJobSA()
202+
if op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, sa, controllerutil.MutateFn(func() error {
203+
return feast.setCronJobServiceAccount(sa)
204+
})); err != nil {
205+
return err
206+
} else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated {
207+
logger.Info("Successfully reconciled", "ServiceAccount", sa.Name, "operation", op)
208+
}
209+
return nil
210+
}
211+
212+
func (feast *FeastServices) initCronJobSA() *corev1.ServiceAccount {
213+
sa := &corev1.ServiceAccount{
214+
ObjectMeta: metav1.ObjectMeta{
215+
Name: feast.getCronJobRoleName(),
216+
Namespace: feast.Handler.FeatureStore.Namespace,
217+
},
218+
}
219+
sa.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ServiceAccount"))
220+
return sa
221+
}
222+
223+
func (feast *FeastServices) setCronJobServiceAccount(sa *corev1.ServiceAccount) error {
224+
sa.Labels = feast.getFeastTypeLabels(CronJobFeastType)
225+
return controllerutil.SetControllerReference(feast.Handler.FeatureStore, sa, feast.Handler.Scheme)
181226
}
182227

183228
func (feast *FeastServices) createCronJobRole() error {
@@ -254,7 +299,7 @@ func (feast *FeastServices) setCronJobRoleBinding(roleBinding *rbacv1.RoleBindin
254299
roleBinding.Labels = feast.getFeastTypeLabels(CronJobFeastType)
255300
roleBinding.Subjects = []rbacv1.Subject{{
256301
Kind: rbacv1.ServiceAccountKind,
257-
Name: feast.initFeastSA().Name,
302+
Name: feast.initCronJobSA().Name,
258303
Namespace: feast.Handler.FeatureStore.Namespace,
259304
}}
260305
roleBinding.RoleRef = rbacv1.RoleRef{

0 commit comments

Comments
 (0)