@@ -17,6 +17,7 @@ limitations under the License.
1717package controller
1818
1919import (
20+ "bytes"
2021 "context"
2122 "fmt"
2223
@@ -51,8 +52,8 @@ var _ webhook.CustomDefaulter = &AppWrapperWebhook{}
5152// Default ensures that Suspend is set appropriately when an AppWrapper is created
5253func (w * AppWrapperWebhook ) Default (ctx context.Context , obj runtime.Object ) error {
5354 aw := obj .(* workloadv1beta2.AppWrapper )
54- log .FromContext (ctx ).Info ("Applying defaults" , "job" , aw )
5555 jobframework .ApplyDefaultForSuspend ((* AppWrapper )(aw ), w .Config .ManageJobsWithoutQueueName )
56+ log .FromContext (ctx ).Info ("Applied defaults" , "job" , aw )
5657 return nil
5758}
5859
@@ -64,7 +65,7 @@ var _ webhook.CustomValidator = &AppWrapperWebhook{}
6465func (w * AppWrapperWebhook ) ValidateCreate (ctx context.Context , obj runtime.Object ) (admission.Warnings , error ) {
6566 aw := obj .(* workloadv1beta2.AppWrapper )
6667
67- allErrors := w .validateAppWrapperInvariants (ctx , aw )
68+ allErrors := w .validateAppWrapperCreate (ctx , aw )
6869 if w .Config .ManageJobsWithoutQueueName || jobframework .QueueName ((* AppWrapper )(aw )) != "" {
6970 allErrors = append (allErrors , jobframework .ValidateCreateForQueueName ((* AppWrapper )(aw ))... )
7071 }
@@ -83,7 +84,7 @@ func (w *AppWrapperWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj r
8384 oldAW := oldObj .(* workloadv1beta2.AppWrapper )
8485 newAW := newObj .(* workloadv1beta2.AppWrapper )
8586
86- allErrors := w .validateAppWrapperInvariants (ctx , newAW )
87+ allErrors := w .validateAppWrapperUpdate (ctx , oldAW , newAW )
8788 if w .Config .ManageJobsWithoutQueueName || jobframework .QueueName ((* AppWrapper )(newAW )) != "" {
8889 allErrors = append (allErrors , jobframework .ValidateUpdateForQueueName ((* AppWrapper )(oldAW ), (* AppWrapper )(newAW ))... )
8990 allErrors = append (allErrors , jobframework .ValidateUpdateForWorkloadPriorityClassName ((* AppWrapper )(oldAW ), (* AppWrapper )(newAW ))... )
@@ -107,13 +108,13 @@ func (w *AppWrapperWebhook) ValidateDelete(context.Context, runtime.Object) (adm
107108//+kubebuilder:rbac:groups=authorization.k8s.io,resources=subjectaccessreviews,verbs=create
108109//+kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=list
109110
110- // validateAppWrapperInvariants checks these invariants:
111+ // validateAppWrapperCreate checks these invariants:
111112// 1. AppWrappers must not contain other AppWrappers
112113// 2. AppWrappers must only contain resources intended for their own namespace
113114// 3. AppWrappers must not contain any resources that the user could not create directly
114115// 4. Every PodSet must be well-formed: the Path must exist and must be parseable as a PodSpecTemplate
115116// 5. AppWrappers must contain between 1 and 8 PodSets (Kueue invariant)
116- func (w * AppWrapperWebhook ) validateAppWrapperInvariants (ctx context.Context , aw * workloadv1beta2.AppWrapper ) field.ErrorList {
117+ func (w * AppWrapperWebhook ) validateAppWrapperCreate (ctx context.Context , aw * workloadv1beta2.AppWrapper ) field.ErrorList {
117118 allErrors := field.ErrorList {}
118119 components := aw .Spec .Components
119120 componentsPath := field .NewPath ("spec" ).Child ("components" )
@@ -124,11 +125,6 @@ func (w *AppWrapperWebhook) validateAppWrapperInvariants(ctx context.Context, aw
124125 }
125126 userInfo := request .UserInfo
126127
127- // To reduce overhead, skip invariant validation when the AppWrapper controller is the user performing the update
128- if w .Config .ServiceAccountName != "" && userInfo .Username == w .Config .ServiceAccountName {
129- return allErrors
130- }
131-
132128 for idx , component := range components {
133129 compPath := componentsPath .Index (idx )
134130 unstruct := & unstructured.Unstructured {}
@@ -205,6 +201,46 @@ func (w *AppWrapperWebhook) validateAppWrapperInvariants(ctx context.Context, aw
205201 return allErrors
206202}
207203
204+ // validateAppWrapperUpdate enforces that AppWrapper.Spec.Components is deeply immutable
205+ func (w * AppWrapperWebhook ) validateAppWrapperUpdate (ctx context.Context , old * workloadv1beta2.AppWrapper , new * workloadv1beta2.AppWrapper ) field.ErrorList {
206+ // The AppWrapper controller must be allowed to mutate Spec.Components
207+ // to enable it to implement RunWithPodSetsInfo and RestorePodSetsInfo
208+ if request , err := admission .RequestFromContext (ctx ); err == nil {
209+ if w .Config .ServiceAccountName != "" && request .UserInfo .Username == w .Config .ServiceAccountName {
210+ return field.ErrorList {}
211+ }
212+ }
213+
214+ allErrors := field.ErrorList {}
215+ msg := "attempt to change immutable field"
216+ componentsPath := field .NewPath ("spec" ).Child ("components" )
217+ if len (old .Spec .Components ) != len (new .Spec .Components ) {
218+ return field.ErrorList {field .Forbidden (componentsPath , msg )}
219+ }
220+ for idx := range new .Spec .Components {
221+ compPath := componentsPath .Index (idx )
222+ oldComponent := old .Spec .Components [idx ]
223+ newComponent := new .Spec .Components [idx ]
224+ if ! bytes .Equal (oldComponent .Template .Raw , newComponent .Template .Raw ) {
225+ allErrors = append (allErrors , field .Forbidden (compPath .Child ("template" ).Child ("raw" ), msg ))
226+ }
227+ if len (oldComponent .PodSets ) != len (newComponent .PodSets ) {
228+ allErrors = append (allErrors , field .Forbidden (compPath .Child ("podsets" ), msg ))
229+ } else {
230+ for psIdx := range newComponent .PodSets {
231+ if replicas (oldComponent .PodSets [psIdx ]) != replicas (newComponent .PodSets [psIdx ]) {
232+ allErrors = append (allErrors , field .Forbidden (compPath .Child ("podsets" ).Index (psIdx ).Child ("replicas" ), msg ))
233+ }
234+ if oldComponent .PodSets [psIdx ].Path != newComponent .PodSets [psIdx ].Path {
235+ allErrors = append (allErrors , field .Forbidden (compPath .Child ("podsets" ).Index (psIdx ).Child ("path" ), msg ))
236+ }
237+ }
238+ }
239+ }
240+
241+ return allErrors
242+ }
243+
208244func (w * AppWrapperWebhook ) lookupResource (gvk * schema.GroupVersionKind ) string {
209245 if known , ok := w .kindToResourceCache [gvk .String ()]; ok {
210246 return known
0 commit comments