Skip to content

Commit c747dcc

Browse files
committed
PodSet inference only updates the Status, not the Spec
1 parent f4b63c2 commit c747dcc

File tree

3 files changed

+55
-72
lines changed

3 files changed

+55
-72
lines changed

internal/controller/appwrapper/suite_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"sigs.k8s.io/controller-runtime/pkg/log/zap"
4040

4141
workloadv1beta2 "github.com/project-codeflare/appwrapper/api/v1beta2"
42+
"github.com/project-codeflare/appwrapper/internal/controller/awstatus"
4243
kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1"
4344
)
4445

@@ -100,6 +101,7 @@ var _ = BeforeSuite(func() {
100101
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
101102
Expect(err).NotTo(HaveOccurred())
102103
Expect(k8sClient).NotTo(BeNil())
104+
awstatus.CacheClient(k8sClient)
103105
})
104106

105107
var _ = AfterSuite(func() {

internal/webhook/appwrapper_webhook.go

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,21 @@ type AppWrapperWebhook struct {
6262

6363
var _ webhook.CustomDefaulter = &AppWrapperWebhook{}
6464

65-
// Default ensures that Suspend is set appropriately when an AppWrapper is created
65+
// Default fills in default values when an AppWrapper is created:
66+
// 1. Inject default queue name
67+
// 2. Ensure Suspend is set appropriately
68+
// 3. Add labels with the user name and id
6669
func (w *AppWrapperWebhook) Default(ctx context.Context, obj runtime.Object) error {
6770
aw := obj.(*workloadv1beta2.AppWrapper)
6871
log.FromContext(ctx).Info("Applying defaults", "job", aw)
72+
73+
// Queue name and Suspend
6974
if w.Config.EnableKueueIntegrations {
75+
if w.Config.QueueName != "" && aw.Annotations[QueueNameLabel] == "" && aw.Labels[QueueNameLabel] == "" {
76+
aw.Labels[QueueNameLabel] = w.Config.QueueName
77+
}
7078
jobframework.ApplyDefaultForSuspend((*wlc.AppWrapper)(aw), w.Config.ManageJobsWithoutQueueName)
7179
}
72-
if err := inferPodSets(ctx, aw); err != nil {
73-
log.FromContext(ctx).Info("Error raised during podSet inference", "job", aw)
74-
return err
75-
}
7680

7781
// inject labels with user name and id
7882
request, err := admission.RequestFromContext(ctx)
@@ -83,11 +87,6 @@ func (w *AppWrapperWebhook) Default(ctx context.Context, obj runtime.Object) err
8387
username := utils.SanitizeLabel(userInfo.Username)
8488
aw.Labels = utilmaps.MergeKeepFirst(map[string]string{AppWrapperUsernameLabel: username, AppWrapperUserIDLabel: userInfo.UID}, aw.Labels)
8589

86-
// inject default queue name if missing from appwrapper and configured on controller
87-
if w.Config.QueueName != "" && aw.Annotations[QueueNameLabel] == "" && aw.Labels[QueueNameLabel] == "" {
88-
aw.Labels[QueueNameLabel] = w.Config.QueueName
89-
}
90-
9190
return nil
9291
}
9392

@@ -124,30 +123,6 @@ func (w *AppWrapperWebhook) ValidateDelete(context.Context, runtime.Object) (adm
124123
return nil, nil
125124
}
126125

127-
// inferPodSets infers the AppWrapper's PodSets
128-
func inferPodSets(_ context.Context, aw *workloadv1beta2.AppWrapper) error {
129-
components := aw.Spec.Components
130-
componentsPath := field.NewPath("spec").Child("components")
131-
for idx, component := range components {
132-
compPath := componentsPath.Index(idx)
133-
134-
// Automatically create elided PodSets for known GVKs
135-
if len(component.DeclaredPodSets) == 0 {
136-
unstruct := &unstructured.Unstructured{}
137-
_, _, err := unstructured.UnstructuredJSONScheme.Decode(component.Template.Raw, nil, unstruct)
138-
if err != nil {
139-
return field.Invalid(compPath.Child("template"), component.Template, "failed to decode as JSON")
140-
}
141-
podSets, err := utils.InferPodSets(unstruct)
142-
if err != nil {
143-
return err
144-
}
145-
components[idx].DeclaredPodSets = podSets
146-
}
147-
}
148-
return nil
149-
}
150-
151126
// rbacs required to enable SubjectAccessReview
152127
//+kubebuilder:rbac:groups=authorization.k8s.io,resources=subjectaccessreviews,verbs=create
153128
//+kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=list
@@ -221,25 +196,31 @@ func (w *AppWrapperWebhook) validateAppWrapperCreate(ctx context.Context, aw *wo
221196
}
222197
}
223198

224-
// 4. Each PodSet.Path must specify a path within Template to a v1.PodSpecTemplate
199+
// 4. Every DeclaredPodSet must specify a path within Template to a v1.PodSpecTemplate
225200
podSetsPath := compPath.Child("podSets")
226201
for psIdx, ps := range component.DeclaredPodSets {
227202
podSetPath := podSetsPath.Index(psIdx)
228203
if ps.Path == "" {
229204
allErrors = append(allErrors, field.Required(podSetPath.Child("path"), "podspec must specify path"))
230205
}
231206
if _, err := utils.GetPodTemplateSpec(unstruct, ps.Path); err != nil {
232-
allErrors = append(allErrors, field.Invalid(podSetPath.Child("path"), ps.Path,
233-
fmt.Sprintf("path does not refer to a v1.PodSpecTemplate: %v", err)))
207+
allErrors = append(allErrors, field.Invalid(podSetPath.Child("path"), ps.Path, fmt.Sprintf("path does not refer to a v1.PodSpecTemplate: %v", err)))
234208
}
235-
podSpecCount += 1
236209
}
237210

238211
// 5. Validate PodSets for known GVKs
239-
if err := utils.ValidatePodSets(unstruct, component.DeclaredPodSets); err != nil {
240-
allErrors = append(allErrors, field.Invalid(podSetsPath, component.DeclaredPodSets, err.Error()))
212+
if inferred, err := utils.InferPodSets(unstruct); err != nil {
213+
allErrors = append(allErrors, field.Invalid(compPath.Child("template"), component.Template, fmt.Sprintf("error inferring PodSets: %v", err)))
214+
} else {
215+
if len(component.DeclaredPodSets) > len(inferred) {
216+
podSpecCount += len(component.DeclaredPodSets)
217+
} else {
218+
podSpecCount += len(inferred)
219+
}
220+
if err := utils.ValidatePodSets(component.DeclaredPodSets, inferred); err != nil {
221+
allErrors = append(allErrors, field.Invalid(podSetsPath, component.DeclaredPodSets, err.Error()))
222+
}
241223
}
242-
243224
}
244225

245226
// 6. Enforce Kueue limitation that 0 < podSpecCount <= 8

pkg/utils/utils.go

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -302,48 +302,48 @@ func InferPodSets(obj *unstructured.Unstructured) ([]workloadv1beta2.AppWrapperP
302302
}
303303
}
304304

305-
return podSets, nil
306-
}
307-
308-
// ValidatePodSets compares declared and inferred PodSets for known GVKs
309-
func ValidatePodSets(obj *unstructured.Unstructured, podSets []workloadv1beta2.AppWrapperPodSet) error {
310-
declared := map[string]workloadv1beta2.AppWrapperPodSet{}
311-
312-
// construct a map with declared PodSets and find duplicates
313-
for _, p := range podSets {
314-
if _, ok := declared[p.Path]; ok {
315-
return fmt.Errorf("duplicate PodSets with path '%v'", p.Path)
305+
for _, ps := range podSets {
306+
if _, err := GetPodTemplateSpec(obj, ps.Path); err != nil {
307+
return nil, fmt.Errorf("%v does not refer to a v1.PodSpecTemplate: %v", ps.Path, err)
316308
}
317-
declared[p.Path] = p
318309
}
319310

320-
// infer PodSets
321-
inferred, err := InferPodSets(obj)
322-
if err != nil {
323-
return err
324-
}
311+
return podSets, nil
312+
}
325313

326-
// nothing inferred, nothing to validate
327-
if len(inferred) == 0 {
314+
// ValidatePodSets valdiates the declared and inferred PodSets
315+
func ValidatePodSets(declared []workloadv1beta2.AppWrapperPodSet, inferred []workloadv1beta2.AppWrapperPodSet) error {
316+
if len(declared) == 0 {
328317
return nil
329318
}
330319

331-
// compare PodSet counts
332-
if len(inferred) != len(declared) {
333-
return fmt.Errorf("PodSet count %v differs from expected count %v", len(declared), len(inferred))
320+
// Validate that there are no duplicate paths in declared
321+
declaredPaths := map[string]workloadv1beta2.AppWrapperPodSet{}
322+
for _, p := range declared {
323+
if _, ok := declaredPaths[p.Path]; ok {
324+
return fmt.Errorf("multiple DeclaredPodSets with path '%v'", p.Path)
325+
}
326+
declaredPaths[p.Path] = p
334327
}
335328

336-
// match inferred PodSets to declared PodSets
337-
for _, ips := range inferred {
338-
dps, ok := declared[ips.Path]
339-
if !ok {
340-
return fmt.Errorf("PodSet with path '%v' is missing", ips.Path)
329+
// Validate that the declared PodSets match what inference computed
330+
if len(inferred) > 0 {
331+
if len(inferred) != len(declared) {
332+
return fmt.Errorf("DeclaredPodSet count %v differs from inferred count %v", len(declared), len(inferred))
341333
}
342334

343-
ipr := ptr.Deref(ips.Replicas, 1)
344-
dpr := ptr.Deref(dps.Replicas, 1)
345-
if ipr != dpr {
346-
return fmt.Errorf("replica count %v differs from expected count %v for PodSet at path position '%v'", dpr, ipr, ips.Path)
335+
// match inferred PodSets to declared PodSets
336+
for _, ips := range inferred {
337+
dps, ok := declaredPaths[ips.Path]
338+
if !ok {
339+
return fmt.Errorf("PodSet with path '%v' is missing", ips.Path)
340+
}
341+
342+
ipr := ptr.Deref(ips.Replicas, 1)
343+
dpr := ptr.Deref(dps.Replicas, 1)
344+
if ipr != dpr {
345+
return fmt.Errorf("replica count %v differs from inferred count %v for PodSet at path position '%v'", dpr, ipr, ips.Path)
346+
}
347347
}
348348
}
349349

0 commit comments

Comments
 (0)