Skip to content

Commit 9dc75de

Browse files
feature
1 parent e8ce62d commit 9dc75de

3 files changed

Lines changed: 488 additions & 16 deletions

File tree

controllers/managedcloudprofile_controller.go

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -209,15 +209,11 @@ func (r *Reconciler) deleteVersions(ctx context.Context, cloudProfileName, image
209209
return err
210210
}
211211

212-
for i := range cp.Spec.MachineImages {
213-
if cp.Spec.MachineImages[i].Name != imageName {
214-
continue
215-
}
216-
cp.Spec.MachineImages[i].Versions = slices.DeleteFunc(cp.Spec.MachineImages[i].Versions, func(mv gardenerv1beta1.MachineImageVersion) bool {
217-
_, exists := versionsToDelete[mv.Version]
218-
return exists
219-
})
220-
}
212+
// Track which clean versions still have remaining capability flavors after deletion,
213+
// so we can cascade-delete empty clean version entries from spec.machineImages.
214+
// A version present in this map was a clean version entry; true means it still has flavors.
215+
cleanVersionsWithFlavors := make(map[string]bool)
216+
221217
if cp.Spec.ProviderConfig != nil {
222218
var cfg providercfg.CloudProfileConfig
223219
if err := json.Unmarshal(cp.Spec.ProviderConfig.Raw, &cfg); err != nil {
@@ -227,14 +223,36 @@ func (r *Reconciler) deleteVersions(ctx context.Context, cloudProfileName, image
227223
if cfg.MachineImages[i].Name != imageName {
228224
continue
229225
}
226+
for j := range cfg.MachineImages[i].Versions {
227+
v := &cfg.MachineImages[i].Versions[j]
228+
if len(v.CapabilityFlavors) == 0 {
229+
continue
230+
}
231+
// Mark this as a clean version entry (key present = was a clean version).
232+
cleanVersionsWithFlavors[v.Version] = true
233+
v.CapabilityFlavors = slices.DeleteFunc(v.CapabilityFlavors, func(f providercfg.MachineImageFlavor) bool {
234+
idx := strings.LastIndex(f.Image, ":")
235+
if idx == -1 {
236+
return false
237+
}
238+
_, exists := versionsToDelete[f.Image[idx+1:]]
239+
return exists
240+
})
241+
cleanVersionsWithFlavors[v.Version] = len(v.CapabilityFlavors) > 0
242+
}
243+
// Remove version entries that have no legacy image ref and no remaining flavors.
230244
cfg.MachineImages[i].Versions = slices.DeleteFunc(cfg.MachineImages[i].Versions, func(mv providercfg.MachineImageVersion) bool {
231-
idx := strings.LastIndex(mv.Image, ":")
232-
if idx == -1 {
233-
return false
245+
if mv.Image != "" {
246+
// Legacy flat entry — delete if its tag is in versionsToDelete.
247+
idx := strings.LastIndex(mv.Image, ":")
248+
if idx == -1 {
249+
return false
250+
}
251+
_, exists := versionsToDelete[mv.Image[idx+1:]]
252+
return exists
234253
}
235-
version := mv.Image[idx+1:]
236-
_, exists := versionsToDelete[version]
237-
return exists
254+
// Clean version entry — delete if all flavors were removed.
255+
return !cleanVersionsWithFlavors[mv.Version]
238256
})
239257
}
240258
raw, err := json.Marshal(cfg)
@@ -243,6 +261,22 @@ func (r *Reconciler) deleteVersions(ctx context.Context, cloudProfileName, image
243261
}
244262
cp.Spec.ProviderConfig.Raw = raw
245263
}
264+
265+
for i := range cp.Spec.MachineImages {
266+
if cp.Spec.MachineImages[i].Name != imageName {
267+
continue
268+
}
269+
cp.Spec.MachineImages[i].Versions = slices.DeleteFunc(cp.Spec.MachineImages[i].Versions, func(mv gardenerv1beta1.MachineImageVersion) bool {
270+
if _, exists := versionsToDelete[mv.Version]; exists {
271+
return true
272+
}
273+
// Cascade-delete clean version entry if all its capability flavors were removed.
274+
// Only entries tracked as clean versions (present in the map) are eligible.
275+
hasRemainingFlavors, isCleanVersion := cleanVersionsWithFlavors[mv.Version]
276+
return isCleanVersion && !hasRemainingFlavors
277+
})
278+
}
279+
246280
if err := r.Update(ctx, &cp); err != nil {
247281
return err
248282
}
@@ -271,6 +305,39 @@ func (r *Reconciler) getReferencedVersions(ctx context.Context, cloudProfileName
271305
}
272306
}
273307

308+
// For any clean version referenced by a Shoot, also protect the raw OCI tags
309+
// that back it via capabilityFlavors — otherwise GC would delete the images
310+
// that the clean version depends on.
311+
if len(referenced) > 0 {
312+
var cp gardenerv1beta1.CloudProfile
313+
if err := r.Get(ctx, types.NamespacedName{Name: cloudProfileName}, &cp); err != nil {
314+
return nil, fmt.Errorf("failed to get CloudProfile: %w", err)
315+
}
316+
if cp.Spec.ProviderConfig != nil {
317+
var cfg providercfg.CloudProfileConfig
318+
if err := json.Unmarshal(cp.Spec.ProviderConfig.Raw, &cfg); err != nil {
319+
return nil, fmt.Errorf("failed to unmarshal ProviderConfig: %w", err)
320+
}
321+
for _, img := range cfg.MachineImages {
322+
if img.Name != imageName {
323+
continue
324+
}
325+
for _, v := range img.Versions {
326+
if _, isReferenced := referenced[v.Version]; !isReferenced {
327+
continue
328+
}
329+
for _, flavor := range v.CapabilityFlavors {
330+
idx := strings.LastIndex(flavor.Image, ":")
331+
if idx == -1 {
332+
continue
333+
}
334+
referenced[flavor.Image[idx+1:]] = struct{}{}
335+
}
336+
}
337+
}
338+
}
339+
}
340+
274341
return referenced, nil
275342
}
276343

0 commit comments

Comments
 (0)