@@ -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