Skip to content

Commit e7475d8

Browse files
committed
fix: take into account the moment seen when cleaning up CRI images
Fixes #8069 The image age from the CRI is the moment the image was pulled, so if it was pulled long time ago, the previous version would nuke the image as soon as it is unreferenced. The new version would allow the image to stay for the full grace period in case the rollback is requested. Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com> (cherry picked from commit 17567f1)
1 parent 9b819ee commit e7475d8

File tree

5 files changed

+32
-6
lines changed

5 files changed

+32
-6
lines changed

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ linters-settings:
131131
replace-local: true
132132
replace-allow-list:
133133
- gopkg.in/yaml.v3
134+
- github.com/siderolabs/gen
134135
retract-allow-no-explanation: false
135136
exclude-forbidden: true
136137

internal/app/machined/pkg/controllers/runtime/cri_image_gc.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ const ImageGCGracePeriod = 4 * ImageCleanupInterval
3939
type CRIImageGCController struct {
4040
ImageServiceProvider func() (ImageServiceProvider, error)
4141
Clock clock.Clock
42+
43+
imageFirstSeenUnreferenced map[string]time.Time
4244
}
4345

4446
// ImageServiceProvider wraps the containerd image service.
@@ -116,6 +118,10 @@ func (ctrl *CRIImageGCController) Run(ctx context.Context, r controller.Runtime,
116118
ctrl.Clock = clock.New()
117119
}
118120

121+
if ctrl.imageFirstSeenUnreferenced == nil {
122+
ctrl.imageFirstSeenUnreferenced = map[string]time.Time{}
123+
}
124+
119125
var (
120126
criIsUp bool
121127
expectedImages []string
@@ -273,10 +279,26 @@ func (ctrl *CRIImageGCController) cleanup(ctx context.Context, logger *zap.Logge
273279
if shouldKeep {
274280
logger.Debug("image is referenced, skipping garbage collection", zap.String("image", image.Name))
275281

282+
delete(ctrl.imageFirstSeenUnreferenced, image.Name)
283+
276284
continue
277285
}
278286

279-
imageAge := ctrl.Clock.Since(image.CreatedAt)
287+
if _, ok := ctrl.imageFirstSeenUnreferenced[image.Name]; !ok {
288+
ctrl.imageFirstSeenUnreferenced[image.Name] = ctrl.Clock.Now()
289+
}
290+
291+
// calculate image age two ways, and pick the minimum:
292+
// * as CRI reports it, which is the time image got pulled
293+
// * as we see it, this means the image won't be deleted until it reaches the age of ImageGCGracePeriod from the moment it became unreferenced
294+
imageAgeCRI := ctrl.Clock.Since(image.CreatedAt)
295+
imageAgeInternal := ctrl.Clock.Since(ctrl.imageFirstSeenUnreferenced[image.Name])
296+
297+
imageAge := imageAgeCRI
298+
if imageAgeInternal < imageAge {
299+
imageAge = imageAgeInternal
300+
}
301+
280302
if imageAge < ImageGCGracePeriod {
281303
logger.Debug("skipping image cleanup, as it's below minimum age", zap.String("image", image.Name), zap.Duration("age", imageAge))
282304

@@ -287,6 +309,7 @@ func (ctrl *CRIImageGCController) cleanup(ctx context.Context, logger *zap.Logge
287309
return fmt.Errorf("failed to delete an image %s: %w", image.Name, err)
288310
}
289311

312+
delete(ctrl.imageFirstSeenUnreferenced, image.Name)
290313
logger.Info("deleted an image", zap.String("image", image.Name))
291314
}
292315

internal/app/machined/pkg/controllers/runtime/cri_image_gc_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,9 @@ func (suite *CRIImageGCSuite) TestReconcile() {
110110
},
111111
}, // ok to be gc'd
112112
{
113-
Name: "sha256:6b094bd0b063a1172eec7da249eccbb48cc48333800569363d67c747960cfa0a",
114-
CreatedAt: suite.fakeClock.Now().Add(-2 * runtimectrl.ImageGCGracePeriod),
113+
Name: "sha256:6b094bd0b063a1172eec7da249eccbb48cc48333800569363d67c747960cfa0a",
114+
// the image age is more than the grace period, but the controller won't remove due to the check on the last seen unreferenced timestamp
115+
CreatedAt: suite.fakeClock.Now().Add(-4 * runtimectrl.ImageGCGracePeriod),
115116
Target: v1.Descriptor{
116117
Digest: must(digest.Parse("sha256:6b094bd0b063a1172eec7da249eccbb48cc48333800569363d67c747960cfa0a")),
117118
},
@@ -122,7 +123,7 @@ func (suite *CRIImageGCSuite) TestReconcile() {
122123
Target: v1.Descriptor{
123124
Digest: must(digest.Parse("sha256:7051a34bcd2522e58a2291d1aa065667f225fd07e4445590b091e86c6799b135")),
124125
},
125-
}, // current image
126+
}, // current image``
126127
{
127128
Name: "registry.io/org/image1@sha256:7051a34bcd2522e58a2291d1aa065667f225fd07e4445590b091e86c6799b135",
128129
CreatedAt: suite.fakeClock.Now().Add(-2 * runtimectrl.ImageGCGracePeriod),
@@ -139,7 +140,7 @@ func (suite *CRIImageGCSuite) TestReconcile() {
139140
}, // current image, digest ref
140141
{
141142
Name: "registry.io/org/image1:v1.3.8",
142-
CreatedAt: suite.fakeClock.Now(),
143+
CreatedAt: suite.fakeClock.Now().Add(runtimectrl.ImageGCGracePeriod),
143144
Target: v1.Descriptor{
144145
Digest: must(digest.Parse("sha256:fd03335dd2e7163e5e36e933a0c735d7fec6f42b33ddafad0bc54f333e4a23c0")),
145146
},

internal/app/machined/pkg/system/services/extension_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func TestGetOCIOptions(t *testing.T) {
9494
"/proc/timer_stats",
9595
"/proc/sched_debug",
9696
"/sys/firmware",
97+
"/sys/devices/virtual/powercap",
9798
"/proc/scsi",
9899
}, spec.Linux.MaskedPaths)
99100
assert.Equal(t, []string{

pkg/imager/profile/profile.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func (p *Profile) SecureBootEnabled() bool {
5555

5656
// Validate the profile.
5757
//
58-
//nolint:gocyclo
58+
//nolint:gocyclo,cyclop
5959
func (p *Profile) Validate() error {
6060
if p.Arch != "amd64" && p.Arch != "arm64" {
6161
return fmt.Errorf("invalid arch %q", p.Arch)

0 commit comments

Comments
 (0)