Skip to content

Commit d18f29c

Browse files
authored
Merge pull request #2201 from SebTardif/fix/helm-index-temp-leak
Remove leaked Helm index cache on error
2 parents bf445d6 + 577d2a2 commit d18f29c

2 files changed

Lines changed: 141 additions & 0 deletions

File tree

‎internal/controller/helmrepository_controller.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,13 @@ func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patc
461461
}
462462
*chartRepo = *newChartRepo
463463

464+
// reconcileArtifact removes this file. Error returns never reach it.
465+
removeCachedIndex := func() {
466+
if err := chartRepo.Clear(); err != nil {
467+
ctrl.LoggerFrom(ctx).Error(err, "failed to remove temporary cached index file")
468+
}
469+
}
470+
464471
// Early comparison to current Artifact.
465472
if curArtifact := obj.GetArtifact(); curArtifact != nil {
466473
curRev := digest.Digest(curArtifact.Revision)
@@ -477,6 +484,7 @@ func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patc
477484

478485
// Load the cached repository index to ensure it passes validation.
479486
if err := chartRepo.LoadFromPath(); err != nil {
487+
removeCachedIndex()
480488
e := serror.NewGeneric(
481489
fmt.Errorf("failed to load Helm repository from index YAML: %w", err),
482490
sourcev1.IndexationFailedReason,
@@ -490,6 +498,7 @@ func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patc
490498
// Calculate revision.
491499
revision := chartRepo.Digest(intdigest.Canonical)
492500
if revision.Validate() != nil {
501+
removeCachedIndex()
493502
e := serror.NewGeneric(
494503
fmt.Errorf("failed to calculate revision: %w", err),
495504
sourcev1.IndexationFailedReason,
@@ -505,6 +514,7 @@ func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patc
505514
}
506515
rreconcile.ProgressiveStatus(true, obj, meta.ProgressingReason, "building artifact: %s", message)
507516
if err := sp.Patch(ctx, obj, r.patchOptions...); err != nil {
517+
removeCachedIndex()
508518
return sreconcile.ResultEmpty, serror.NewGeneric(err, sourcev1.PatchOperationFailedReason)
509519
}
510520

‎internal/controller/helmrepository_controller_test.go‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
ctrl "sigs.k8s.io/controller-runtime"
3939
"sigs.k8s.io/controller-runtime/pkg/client"
4040
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
41+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
4142
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
4243

4344
kstatus "github.com/fluxcd/cli-utils/pkg/kstatus/status"
@@ -1089,6 +1090,136 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
10891090
}
10901091
}
10911092

1093+
func TestHelmRepositoryReconciler_reconcileSource_cachedIndexFile(t *testing.T) {
1094+
newReconciler := func(g *WithT, c client.Client) *HelmRepositoryReconciler {
1095+
return &HelmRepositoryReconciler{
1096+
Recorder: events.NewFakeRecorder(32, false),
1097+
Client: c,
1098+
Storage: testStorage,
1099+
Getters: testGetters,
1100+
patchOptions: getPatchOptions(helmRepositoryReadyCondition.Owned, "sc"),
1101+
}
1102+
}
1103+
1104+
startServer := func(g *WithT, index []byte) *helmtestserver.HelmServer {
1105+
server, err := helmtestserver.NewTempHelmServer()
1106+
g.Expect(err).NotTo(HaveOccurred())
1107+
if index == nil {
1108+
g.Expect(server.PackageChart("testdata/charts/helmchart")).To(Succeed())
1109+
g.Expect(server.GenerateIndex()).To(Succeed())
1110+
} else {
1111+
g.Expect(os.WriteFile(filepath.Join(server.Root(), "index.yaml"), index, 0o600)).To(Succeed())
1112+
}
1113+
server.Start()
1114+
return server
1115+
}
1116+
1117+
newObject := func(url string) *sourcev1.HelmRepository {
1118+
return &sourcev1.HelmRepository{
1119+
ObjectMeta: metav1.ObjectMeta{
1120+
Name: "index-cache",
1121+
Namespace: "default",
1122+
Generation: 1,
1123+
},
1124+
Spec: sourcev1.HelmRepositorySpec{
1125+
Interval: metav1.Duration{Duration: interval},
1126+
Timeout: &metav1.Duration{Duration: timeout},
1127+
URL: url,
1128+
},
1129+
}
1130+
}
1131+
1132+
chartIndexFiles := func(g *WithT, dir string) []string {
1133+
matches, err := filepath.Glob(filepath.Join(dir, "chart-index-*.yaml"))
1134+
g.Expect(err).NotTo(HaveOccurred())
1135+
return matches
1136+
}
1137+
1138+
t.Run("invalid index removes cached file", func(t *testing.T) {
1139+
g := NewWithT(t)
1140+
dir := t.TempDir()
1141+
t.Setenv("TMPDIR", dir)
1142+
1143+
// HTTP 200 with a body that is not a Helm index. Download succeeds
1144+
// and LoadFromPath fails.
1145+
server := startServer(g, []byte("{}\n"))
1146+
defer server.Stop()
1147+
defer os.RemoveAll(server.Root())
1148+
1149+
c := fakeclient.NewClientBuilder().
1150+
WithScheme(testEnv.GetScheme()).
1151+
WithStatusSubresource(&sourcev1.HelmRepository{}).
1152+
Build()
1153+
obj := newObject(server.URL())
1154+
g.Expect(c.Create(context.TODO(), obj)).To(Succeed())
1155+
1156+
var chartRepo repository.ChartRepository
1157+
got, err := newReconciler(g, c).reconcileSource(context.TODO(), patch.NewSerialPatcher(obj, c), obj, &meta.Artifact{}, &chartRepo)
1158+
1159+
g.Expect(err).To(HaveOccurred())
1160+
g.Expect(err.Error()).To(ContainSubstring("failed to load Helm repository from index YAML"))
1161+
g.Expect(got).To(Equal(sreconcile.ResultEmpty))
1162+
g.Expect(conditions.IsTrue(obj, sourcev1.FetchFailedCondition)).To(BeTrue())
1163+
g.Expect(chartIndexFiles(g, dir)).To(BeEmpty())
1164+
})
1165+
1166+
t.Run("status patch failure removes cached file", func(t *testing.T) {
1167+
g := NewWithT(t)
1168+
dir := t.TempDir()
1169+
t.Setenv("TMPDIR", dir)
1170+
1171+
server := startServer(g, nil)
1172+
defer server.Stop()
1173+
defer os.RemoveAll(server.Root())
1174+
1175+
base := fakeclient.NewClientBuilder().
1176+
WithScheme(testEnv.GetScheme()).
1177+
WithStatusSubresource(&sourcev1.HelmRepository{}).
1178+
Build()
1179+
c := interceptor.NewClient(base, interceptor.Funcs{
1180+
SubResourcePatch: func(context.Context, client.Client, string, client.Object, client.Patch, ...client.SubResourcePatchOption) error {
1181+
return errors.New("injected patch failure")
1182+
},
1183+
})
1184+
obj := newObject(server.URL())
1185+
g.Expect(c.Create(context.TODO(), obj)).To(Succeed())
1186+
1187+
var chartRepo repository.ChartRepository
1188+
got, err := newReconciler(g, c).reconcileSource(context.TODO(), patch.NewSerialPatcher(obj, c), obj, &meta.Artifact{}, &chartRepo)
1189+
1190+
g.Expect(err).To(HaveOccurred())
1191+
g.Expect(err.Error()).To(ContainSubstring("injected patch failure"))
1192+
g.Expect(got).To(Equal(sreconcile.ResultEmpty))
1193+
g.Expect(chartIndexFiles(g, dir)).To(BeEmpty())
1194+
})
1195+
1196+
t.Run("successful fetch keeps cached file", func(t *testing.T) {
1197+
g := NewWithT(t)
1198+
dir := t.TempDir()
1199+
t.Setenv("TMPDIR", dir)
1200+
1201+
server := startServer(g, nil)
1202+
defer server.Stop()
1203+
defer os.RemoveAll(server.Root())
1204+
1205+
c := fakeclient.NewClientBuilder().
1206+
WithScheme(testEnv.GetScheme()).
1207+
WithStatusSubresource(&sourcev1.HelmRepository{}).
1208+
Build()
1209+
obj := newObject(server.URL())
1210+
g.Expect(c.Create(context.TODO(), obj)).To(Succeed())
1211+
1212+
var chartRepo repository.ChartRepository
1213+
got, err := newReconciler(g, c).reconcileSource(context.TODO(), patch.NewSerialPatcher(obj, c), obj, &meta.Artifact{}, &chartRepo)
1214+
defer os.Remove(chartRepo.Path)
1215+
1216+
g.Expect(err).NotTo(HaveOccurred())
1217+
g.Expect(got).To(Equal(sreconcile.ResultSuccess))
1218+
matches := chartIndexFiles(g, dir)
1219+
g.Expect(matches).To(Equal([]string{chartRepo.Path}))
1220+
})
1221+
}
1222+
10921223
func TestHelmRepositoryReconciler_reconcileArtifact(t *testing.T) {
10931224
// The cache is at capacity with the previous revision and an index of
10941225
// another repository, the new revision can only be added after eviction.

0 commit comments

Comments
 (0)