@@ -37,74 +37,90 @@ type AppWrapperWebhook struct {
3737
3838var _ webhook.CustomDefaulter = & AppWrapperWebhook {}
3939
40- // Default implements webhook.CustomDefaulter so a webhook will be registered for the type
40+ // Default ensures that Suspend is set appropriately when an AppWrapper is created
4141func (w * AppWrapperWebhook ) Default (ctx context.Context , obj runtime.Object ) error {
42- job := obj .(* workloadv1beta2.AppWrapper )
43- log .FromContext (ctx ).Info ("Applying defaults" , "job" , job )
44- jobframework .ApplyDefaultForSuspend ((* AppWrapper )(job ), w .ManageJobsWithoutQueueName )
42+ aw := obj .(* workloadv1beta2.AppWrapper )
43+ log .FromContext (ctx ).Info ("Applying defaults" , "job" , aw )
44+ jobframework .ApplyDefaultForSuspend ((* AppWrapper )(aw ), w .ManageJobsWithoutQueueName )
4545return nil
4646}
4747
4848//+kubebuilder:webhook:path=/validate-workload-codeflare-dev-v1beta2-appwrapper,mutating=false,failurePolicy=fail,sideEffects=None,groups=workload.codeflare.dev,resources=appwrappers,verbs=create;update,versions=v1beta2,name=vappwrapper.kb.io,admissionReviewVersions=v1
4949
5050var _ webhook.CustomValidator = & AppWrapperWebhook {}
5151
52- // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
52+ // ValidateCreate validates invariants when an AppWrapper is created
5353func (w * AppWrapperWebhook ) ValidateCreate (ctx context.Context , obj runtime.Object ) (admission.Warnings , error ) {
54- job := obj .(* workloadv1beta2.AppWrapper )
55- log .FromContext (ctx ).Info ("Validating create" , "job" , job )
56- return nil , w .validateCreate (job ).ToAggregate ()
57- }
54+ aw := obj .(* workloadv1beta2.AppWrapper )
55+ log .FromContext (ctx ).Info ("Validating create" , "job" , aw )
5856
59- func (w * AppWrapperWebhook ) validateCreate (job * workloadv1beta2.AppWrapper ) field.ErrorList {
60- var allErrors field.ErrorList
57+ allErrors := w .validateAppWrapperInvariants (ctx , aw )
6158
62- if w .ManageJobsWithoutQueueName || jobframework .QueueName ((* AppWrapper )(job )) != "" {
63- components := job .Spec .Components
64- componentsPath := field .NewPath ("spec" ).Child ("components" )
65- podSpecCount := 0
66- for idx , component := range components {
67- podSetsPath := componentsPath .Index (idx ).Child ("podSets" )
68- for psIdx , ps := range component .PodSets {
69- podSetPath := podSetsPath .Index (psIdx )
70- if ps .Path == "" {
71- allErrors = append (allErrors , field .Required (podSetPath .Child ("path" ), "podspec must specify path" ))
72- }
59+ if w .ManageJobsWithoutQueueName || jobframework .QueueName ((* AppWrapper )(aw )) != "" {
60+ allErrors = append (allErrors , jobframework .ValidateCreateForQueueName ((* AppWrapper )(aw ))... )
61+ }
7362
74- // TODO: Validatate the ps.Path resolves to a PodSpec
63+ return nil , allErrors .ToAggregate ()
64+ }
7565
76- // TODO: RBAC check to make sure that the user has the ability to create the wrapped resources
66+ // ValidateUpdate validates invariants when an AppWrapper is updated
67+ func (w * AppWrapperWebhook ) ValidateUpdate (ctx context.Context , oldObj , newObj runtime.Object ) (admission.Warnings , error ) {
68+ oldAW := oldObj .(* workloadv1beta2.AppWrapper )
69+ newAW := newObj .(* workloadv1beta2.AppWrapper )
70+ log .FromContext (ctx ).Info ("Validating update" , "job" , newAW )
7771
78- podSpecCount += 1
79- }
80- }
81- if podSpecCount == 0 {
82- allErrors = append (allErrors , field .Invalid (componentsPath , components , "components contains no podspecs" ))
83- }
84- if podSpecCount > 8 {
85- allErrors = append (allErrors , field .Invalid (componentsPath , components , fmt .Sprintf ("components contains %v podspecs; at most 8 are allowed" , podSpecCount )))
86- }
72+ allErrors := w .validateAppWrapperInvariants (ctx , newAW )
73+
74+ if w .ManageJobsWithoutQueueName || jobframework .QueueName ((* AppWrapper )(newAW )) != "" {
75+ allErrors = append (allErrors , jobframework .ValidateUpdateForQueueName ((* AppWrapper )(oldAW ), (* AppWrapper )(newAW ))... )
76+ allErrors = append (allErrors , jobframework .ValidateUpdateForWorkloadPriorityClassName ((* AppWrapper )(oldAW ), (* AppWrapper )(newAW ))... )
8777}
8878
89- allErrors = append (allErrors , jobframework .ValidateCreateForQueueName ((* AppWrapper )(job ))... )
90- return allErrors
79+ return nil , allErrors .ToAggregate ()
9180}
9281
93- // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
94- func (w * AppWrapperWebhook ) ValidateUpdate (ctx context.Context , oldObj , newObj runtime.Object ) (admission.Warnings , error ) {
95- oldJob := oldObj .(* workloadv1beta2.AppWrapper )
96- newJob := newObj .(* workloadv1beta2.AppWrapper )
97- if w .ManageJobsWithoutQueueName || jobframework .QueueName ((* AppWrapper )(newJob )) != "" {
98- log .FromContext (ctx ).Info ("Validating update" , "job" , newJob )
99- allErrors := jobframework .ValidateUpdateForQueueName ((* AppWrapper )(oldJob ), (* AppWrapper )(newJob ))
100- allErrors = append (allErrors , w .validateCreate (newJob )... )
101- allErrors = append (allErrors , jobframework .ValidateUpdateForWorkloadPriorityClassName ((* AppWrapper )(oldJob ), (* AppWrapper )(newJob ))... )
102- return nil , allErrors .ToAggregate ()
103- }
82+ // ValidateDelete is a noop for us, but is required to implement the CustomValidator interface
83+ func (w * AppWrapperWebhook ) ValidateDelete (context.Context , runtime.Object ) (admission.Warnings , error ) {
10484return nil , nil
10585}
10686
107- // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type
108- func (w * AppWrapperWebhook ) ValidateDelete (ctx context.Context , obj runtime.Object ) (admission.Warnings , error ) {
109- return nil , nil
87+ // validateAppWrapperInvariants checks AppWrapper-specific invariants
88+ func (w * AppWrapperWebhook ) validateAppWrapperInvariants (_ context.Context , aw * workloadv1beta2.AppWrapper ) field.ErrorList {
89+ allErrors := field.ErrorList {}
90+ components := aw .Spec .Components
91+ componentsPath := field .NewPath ("spec" ).Child ("components" )
92+ podSpecCount := 0
93+
94+ for idx , component := range components {
95+
96+ // Each PodSet.Path must specify a path within Template to a v1.PodSpecTemplate
97+ podSetsPath := componentsPath .Index (idx ).Child ("podSets" )
98+ for psIdx , ps := range component .PodSets {
99+ podSetPath := podSetsPath .Index (psIdx )
100+ if ps .Path == "" {
101+ allErrors = append (allErrors , field .Required (podSetPath .Child ("path" ), "podspec must specify path" ))
102+ }
103+ if _ , err := getPodTemplateSpec (component .Template .Raw , ps .Path ); err != nil {
104+ allErrors = append (allErrors , field .Invalid (podSetPath .Child ("path" ), ps .Path ,
105+ fmt .Sprintf ("path does not refer to a v1.PodSpecTemplate: %v" , err )))
106+ }
107+ podSpecCount += 1
108+ }
109+
110+ // TODO: RBAC check to make sure that the user has permissions to create the component
111+
112+ // TODO: We could attempt to validate the object is namespaced and the namespace is the same as the AppWrapper's namespace
113+ // This is currently enforced when the resources are created.
114+
115+ }
116+
117+ // Enforce Kueue limitation that 0 < podSpecCount <= 8
118+ if podSpecCount == 0 {
119+ allErrors = append (allErrors , field .Invalid (componentsPath , components , "components contains no podspecs" ))
120+ }
121+ if podSpecCount > 8 {
122+ allErrors = append (allErrors , field .Invalid (componentsPath , components , fmt .Sprintf ("components contains %v podspecs; at most 8 are allowed" , podSpecCount )))
123+ }
124+
125+ return allErrors
110126}
0 commit comments