Skip to content

Commit 4a0e192

Browse files
authored
Merge pull request #2152 from fluxcd/helmrepo-cache-evict
Evict stale Helm index entries from cache
2 parents 2b7ff06 + 95c3a31 commit 4a0e192

3 files changed

Lines changed: 120 additions & 0 deletions

File tree

‎docs/spec/v1/helmcharts.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,10 @@ then cache the index. The cached index TTL is refreshed every time the
598598
Helm repository index is loaded with the `helm-cache-ttl` value.
599599

600600
The cache is purged of expired items every `helm-cache-purge-interval`.
601+
When a `HelmRepository` index changes, or the `HelmRepository` is deleted,
602+
the controller removes the index of the previous revision from the cache.
603+
An index still in use by a `HelmChart` reconciliation may be added back,
604+
in which case it is removed once its TTL expires.
601605

602606
When the cache is full, no more items can be added to the cache, and the
603607
source-controller will report a warning event instead.

‎internal/controller/helmrepository_controller.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,9 @@ func (r *HelmRepositoryReconciler) reconcileStorage(ctx context.Context, sp *pat
352352

353353
// If the artifact is missing, remove it from the object
354354
if artifactMissing {
355+
if r.Cache != nil {
356+
r.Cache.Delete(artifact.Path)
357+
}
355358
obj.Status.Artifact = nil
356359
obj.Status.URL = ""
357360
}
@@ -587,6 +590,15 @@ func (r *HelmRepositoryReconciler) reconcileArtifact(ctx context.Context, sp *pa
587590
return sreconcile.ResultEmpty, e
588591
}
589592

593+
// Evict the index of the previous revision from the cache, as it is
594+
// no longer referenced by the object and would otherwise occupy a
595+
// cache slot until its TTL expires.
596+
if r.Cache != nil {
597+
if prev := obj.GetArtifact(); prev != nil && prev.Path != artifact.Path {
598+
r.Cache.Delete(prev.Path)
599+
}
600+
}
601+
590602
// Record it on the object.
591603
obj.Status.Artifact = artifact.DeepCopy()
592604

@@ -656,6 +668,10 @@ func (r *HelmRepositoryReconciler) garbageCollect(ctx context.Context, obj *sour
656668
r.eventLogf(ctx, obj, eventv1.EventTypeTrace, "GarbageCollectionSucceeded",
657669
"garbage collected artifacts for deleted resource")
658670
}
671+
// Evict the index from the cache.
672+
if r.Cache != nil && obj.GetArtifact() != nil {
673+
r.Cache.Delete(obj.GetArtifact().Path)
674+
}
659675
// Clean status sub-resource
660676
obj.Status.Artifact = nil
661677
obj.Status.URL = ""

‎internal/controller/helmrepository_controller_test.go‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ func TestHelmRepositoryReconciler_reconcileStorage(t *testing.T) {
179179
assertArtifact *meta.Artifact
180180
assertConditions []metav1.Condition
181181
assertPaths []string
182+
assertEvicted bool
182183
}{
183184
{
184185
name: "garbage collects",
@@ -244,6 +245,7 @@ func TestHelmRepositoryReconciler_reconcileStorage(t *testing.T) {
244245
assertPaths: []string{
245246
"!/reconcile-storage/invalid.txt",
246247
},
248+
assertEvicted: true,
247249
assertConditions: []metav1.Condition{
248250
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: disappeared from storage"),
249251
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: disappeared from storage"),
@@ -275,6 +277,7 @@ func TestHelmRepositoryReconciler_reconcileStorage(t *testing.T) {
275277
assertPaths: []string{
276278
"!/reconcile-storage/empty-digest.txt",
277279
},
280+
assertEvicted: true,
278281
assertConditions: []metav1.Condition{
279282
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: disappeared from storage"),
280283
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: disappeared from storage"),
@@ -306,6 +309,7 @@ func TestHelmRepositoryReconciler_reconcileStorage(t *testing.T) {
306309
assertPaths: []string{
307310
"!/reconcile-storage/digest-mismatch.txt",
308311
},
312+
assertEvicted: true,
309313
assertConditions: []metav1.Condition{
310314
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: disappeared from storage"),
311315
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: disappeared from storage"),
@@ -356,6 +360,8 @@ func TestHelmRepositoryReconciler_reconcileStorage(t *testing.T) {
356360
Build(),
357361
EventRecorder: record.NewFakeRecorder(32),
358362
Storage: testStorage,
363+
Cache: cache.New(10, time.Minute),
364+
TTL: time.Minute,
359365
patchOptions: getPatchOptions(helmRepositoryReadyCondition.Owned, "sc"),
360366
}
361367

@@ -369,6 +375,12 @@ func TestHelmRepositoryReconciler_reconcileStorage(t *testing.T) {
369375
g.Expect(tt.beforeFunc(obj, testStorage)).To(Succeed())
370376
}
371377

378+
var cachedPath string
379+
if a := obj.GetArtifact(); a != nil {
380+
cachedPath = a.Path
381+
g.Expect(r.Cache.Set(cachedPath, &repo.IndexFile{}, time.Minute)).To(Succeed())
382+
}
383+
372384
g.Expect(r.Client.Create(context.TODO(), obj)).ToNot(HaveOccurred())
373385
defer func() {
374386
g.Expect(r.Client.Delete(context.TODO(), obj)).ToNot(HaveOccurred())
@@ -388,6 +400,11 @@ func TestHelmRepositoryReconciler_reconcileStorage(t *testing.T) {
388400
}
389401
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
390402

403+
if cachedPath != "" {
404+
_, ok := r.Cache.Get(cachedPath)
405+
g.Expect(ok).To(Equal(!tt.assertEvicted))
406+
}
407+
391408
for _, p := range tt.assertPaths {
392409
absoluteP := filepath.Join(testStorage.BasePath, p)
393410
if !strings.HasPrefix(p, "!") {
@@ -1073,6 +1090,10 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
10731090
}
10741091

10751092
func TestHelmRepositoryReconciler_reconcileArtifact(t *testing.T) {
1093+
// The cache is at capacity with the previous revision and an index of
1094+
// another repository, the new revision can only be added after eviction.
1095+
evictCache := cache.New(2, time.Minute)
1096+
10761097
tests := []struct {
10771098
name string
10781099
cache *cache.Cache
@@ -1118,6 +1139,32 @@ func TestHelmRepositoryReconciler_reconcileArtifact(t *testing.T) {
11181139
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact: revision 'existing'"),
11191140
},
11201141
},
1142+
{
1143+
name: "Archiving new revision evicts the previous revision from cache",
1144+
cache: evictCache,
1145+
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact meta.Artifact, index *repository.ChartRepository) {
1146+
index.Index = &repo.IndexFile{
1147+
APIVersion: "v1",
1148+
Generated: time.Now(),
1149+
}
1150+
obj.Spec.Interval = metav1.Duration{Duration: interval}
1151+
prev := testStorage.NewArtifactFor(obj.Kind, obj, "previous", "index-previous.yaml")
1152+
obj.Status.Artifact = &prev
1153+
t.Expect(evictCache.Set(prev.Path, &repo.IndexFile{}, time.Minute)).To(Succeed())
1154+
t.Expect(evictCache.Set("helmrepository/default/other/index-other.yaml", &repo.IndexFile{}, time.Minute)).To(Succeed())
1155+
},
1156+
want: sreconcile.ResultSuccess,
1157+
afterFunc: func(t *WithT, obj *sourcev1.HelmRepository, c *cache.Cache) {
1158+
t.Expect(c.ItemCount()).To(Equal(2))
1159+
_, ok := c.Get(obj.GetArtifact().Path)
1160+
t.Expect(ok).To(BeTrue())
1161+
_, ok = c.Get("helmrepository/default/other/index-other.yaml")
1162+
t.Expect(ok).To(BeTrue())
1163+
},
1164+
assertConditions: []metav1.Condition{
1165+
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact: revision 'existing'"),
1166+
},
1167+
},
11211168
{
11221169
name: "Up-to-date artifact should not update status",
11231170
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact meta.Artifact, index *repository.ChartRepository) {
@@ -1224,6 +1271,59 @@ func TestHelmRepositoryReconciler_reconcileArtifact(t *testing.T) {
12241271
}
12251272
}
12261273

1274+
func TestHelmRepositoryReconciler_garbageCollectEvictsCache(t *testing.T) {
1275+
tests := []struct {
1276+
name string
1277+
beforeFunc func(obj *sourcev1.HelmRepository)
1278+
}{
1279+
{
1280+
name: "deleted object",
1281+
beforeFunc: func(obj *sourcev1.HelmRepository) {
1282+
obj.DeletionTimestamp = &metav1.Time{Time: time.Now()}
1283+
},
1284+
},
1285+
{
1286+
name: "type changed to OCI",
1287+
beforeFunc: func(obj *sourcev1.HelmRepository) {
1288+
obj.Spec.Type = sourcev1.HelmRepositoryTypeOCI
1289+
},
1290+
},
1291+
}
1292+
1293+
for _, tt := range tests {
1294+
t.Run(tt.name, func(t *testing.T) {
1295+
g := NewWithT(t)
1296+
1297+
c := cache.New(10, time.Minute)
1298+
r := &HelmRepositoryReconciler{
1299+
EventRecorder: record.NewFakeRecorder(32),
1300+
Storage: testStorage,
1301+
Cache: c,
1302+
TTL: time.Minute,
1303+
}
1304+
1305+
obj := &sourcev1.HelmRepository{
1306+
TypeMeta: metav1.TypeMeta{
1307+
Kind: sourcev1.HelmRepositoryKind,
1308+
},
1309+
ObjectMeta: metav1.ObjectMeta{
1310+
Name: "cache-evict",
1311+
Namespace: "default",
1312+
},
1313+
}
1314+
artifact := testStorage.NewArtifactFor(obj.Kind, obj, "existing", "index-existing.yaml")
1315+
obj.Status.Artifact = &artifact
1316+
g.Expect(c.Set(artifact.Path, &repo.IndexFile{}, time.Minute)).To(Succeed())
1317+
1318+
tt.beforeFunc(obj)
1319+
1320+
g.Expect(r.garbageCollect(context.TODO(), obj)).To(Succeed())
1321+
g.Expect(obj.GetArtifact()).To(BeNil())
1322+
g.Expect(c.ItemCount()).To(Equal(0))
1323+
})
1324+
}
1325+
}
1326+
12271327
func TestHelmRepositoryReconciler_reconcileSubRecs(t *testing.T) {
12281328
// Helper to build simple helmRepositoryReconcileFunc with result and error.
12291329
buildReconcileFuncs := func(r sreconcile.Result, e error) helmRepositoryReconcileFunc {

0 commit comments

Comments
 (0)