Skip to content

Commit a64efe9

Browse files
GEP-33 addition : update garbage collection for images (#35)
* feature * fix lint * fix checks * fix tests * fix copilot
1 parent b8ea370 commit a64efe9

5 files changed

Lines changed: 545 additions & 23 deletions

File tree

.golangci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ linters:
136136
# for github.com/sapcc/vpa_butler
137137
- k8s.io/client-go
138138
toolchain-forbidden: true
139-
go-version-pattern: 1\.\d+(\.0)?$
139+
go-version-pattern: 1\.\d+(\.\d+)?$ # manually edited, as default rule does not allow go version with patch, but some deps require e.g. go 1.26.2
140140
gosec:
141141
excludes:
142142
# gosec wants us to set a short ReadHeaderTimeout to avoid Slowloris attacks, but doing so would expose us to Keep-Alive race conditions (see https://iximiuz.com/en/posts/reverse-proxy-http-keep-alive-and-502s/

.typos.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
[default.extend-words]
66

7+
[default]
8+
extend-ignore-identifiers-re = ["ANDed"]
9+
710
[files]
811
extend-exclude = [
912
"go.mod",

controllers/managedcloudprofile_controller.go

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,11 @@ func (r *Reconciler) reconcileGarbageCollection(ctx context.Context, mcp *v1alph
190190
}
191191
}
192192

193-
if len(versionsToDelete) > 0 {
194-
if err := r.deleteVersions(ctx, mcp.Name, updates.ImageName, versionsToDelete); err != nil {
195-
if apierrors.IsInvalid(err) {
196-
continue
197-
}
198-
return r.failWithStatusUpdate(ctx, mcp, fmt.Errorf("failed to delete image versions: %w", err))
193+
if err := r.deleteVersions(ctx, mcp.Name, updates.ImageName, versionsToDelete); err != nil {
194+
if apierrors.IsInvalid(err) {
195+
continue
199196
}
197+
return r.failWithStatusUpdate(ctx, mcp, fmt.Errorf("failed to delete image versions: %w", err))
200198
}
201199
}
202200

@@ -209,15 +207,11 @@ func (r *Reconciler) deleteVersions(ctx context.Context, cloudProfileName, image
209207
return err
210208
}
211209

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-
}
210+
// Track which clean versions still have remaining capability flavors after deletion,
211+
// so we can cascade-delete empty clean version entries from spec.machineImages.
212+
// A version present in this map was a clean version entry; true means it still has flavors.
213+
cleanVersionsWithFlavors := make(map[string]bool)
214+
221215
if cp.Spec.ProviderConfig != nil {
222216
var cfg providercfg.CloudProfileConfig
223217
if err := json.Unmarshal(cp.Spec.ProviderConfig.Raw, &cfg); err != nil {
@@ -227,14 +221,40 @@ func (r *Reconciler) deleteVersions(ctx context.Context, cloudProfileName, image
227221
if cfg.MachineImages[i].Name != imageName {
228222
continue
229223
}
224+
for j := range cfg.MachineImages[i].Versions {
225+
v := &cfg.MachineImages[i].Versions[j]
226+
if v.Image != "" {
227+
// Legacy flat entry — not a clean version, skip.
228+
continue
229+
}
230+
// Mark as a clean version entry; value indicates whether any flavors remain.
231+
cleanVersionsWithFlavors[v.Version] = len(v.CapabilityFlavors) > 0
232+
if len(v.CapabilityFlavors) == 0 {
233+
continue
234+
}
235+
v.CapabilityFlavors = slices.DeleteFunc(v.CapabilityFlavors, func(f providercfg.MachineImageFlavor) bool {
236+
idx := strings.LastIndex(f.Image, ":")
237+
if idx == -1 {
238+
return false
239+
}
240+
_, exists := versionsToDelete[f.Image[idx+1:]]
241+
return exists
242+
})
243+
cleanVersionsWithFlavors[v.Version] = len(v.CapabilityFlavors) > 0
244+
}
245+
// Remove version entries that have no legacy image ref and no remaining flavors.
230246
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
247+
if mv.Image != "" {
248+
// Legacy flat entry — delete if its tag is in versionsToDelete.
249+
idx := strings.LastIndex(mv.Image, ":")
250+
if idx == -1 {
251+
return false
252+
}
253+
_, exists := versionsToDelete[mv.Image[idx+1:]]
254+
return exists
234255
}
235-
version := mv.Image[idx+1:]
236-
_, exists := versionsToDelete[version]
237-
return exists
256+
// Clean version entry — delete if all flavors were removed.
257+
return !cleanVersionsWithFlavors[mv.Version]
238258
})
239259
}
240260
raw, err := json.Marshal(cfg)
@@ -243,6 +263,22 @@ func (r *Reconciler) deleteVersions(ctx context.Context, cloudProfileName, image
243263
}
244264
cp.Spec.ProviderConfig.Raw = raw
245265
}
266+
267+
for i := range cp.Spec.MachineImages {
268+
if cp.Spec.MachineImages[i].Name != imageName {
269+
continue
270+
}
271+
cp.Spec.MachineImages[i].Versions = slices.DeleteFunc(cp.Spec.MachineImages[i].Versions, func(mv gardenerv1beta1.MachineImageVersion) bool {
272+
if _, exists := versionsToDelete[mv.Version]; exists {
273+
return true
274+
}
275+
// Cascade-delete clean version entry if all its capability flavors were removed.
276+
// Only entries tracked as clean versions (present in the map) are eligible.
277+
hasRemainingFlavors, isCleanVersion := cleanVersionsWithFlavors[mv.Version]
278+
return isCleanVersion && !hasRemainingFlavors
279+
})
280+
}
281+
246282
if err := r.Update(ctx, &cp); err != nil {
247283
return err
248284
}
@@ -271,6 +307,39 @@ func (r *Reconciler) getReferencedVersions(ctx context.Context, cloudProfileName
271307
}
272308
}
273309

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

0 commit comments

Comments
 (0)