Skip to content

Commit b1ee050

Browse files
committed
fix(installplans): GC older installplans
1 parent 41f8271 commit b1ee050

File tree

2 files changed

+157
-0
lines changed

2 files changed

+157
-0
lines changed

pkg/controller/operators/catalog/operator.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"reflect"
9+
"sort"
910
"strings"
1011
"sync"
1112
"time"
@@ -22,6 +23,7 @@ import (
2223
k8serrors "k8s.io/apimachinery/pkg/api/errors"
2324
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2425
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
26+
"k8s.io/apimachinery/pkg/labels"
2527
"k8s.io/apimachinery/pkg/runtime/schema"
2628
utilclock "k8s.io/apimachinery/pkg/util/clock"
2729
utilerrors "k8s.io/apimachinery/pkg/util/errors"
@@ -66,6 +68,8 @@ const (
6668
roleKind = "Role"
6769
roleBindingKind = "RoleBinding"
6870
generatedByKey = "olm.generated-by"
71+
maxInstallPlanCount = 5
72+
maxDeletesPerSweep = 5
6973
)
7074

7175
// Operator represents a Kubernetes operator that executes InstallPlans by
@@ -770,6 +774,8 @@ func (o *Operator) syncResolvingNamespace(obj interface{}) error {
770774
"id": queueinformer.NewLoopID(),
771775
})
772776

777+
o.gcInstallPlans(logger, namespace)
778+
773779
// get the set of sources that should be used for resolution and best-effort get their connections working
774780
logger.Debug("resolving sources")
775781

@@ -1173,6 +1179,77 @@ func (o *Operator) unpackBundles(plan *v1alpha1.InstallPlan) (bool, *v1alpha1.In
11731179
return unpacked, out, nil
11741180
}
11751181

1182+
// gcInstallPlans garbage collects installplans that are too old
1183+
// installplans are ownerrefd to all subscription inputs, so they will not otherwise
1184+
// be GCd unless all inputs have been deleted.
1185+
func (o *Operator) gcInstallPlans(log logrus.FieldLogger, namespace string) {
1186+
allIps, err := o.lister.OperatorsV1alpha1().InstallPlanLister().InstallPlans(namespace).List(labels.Everything())
1187+
if err != nil {
1188+
log.Warn("unable to list installplans for GC")
1189+
}
1190+
1191+
if len(allIps) <= maxInstallPlanCount {
1192+
return
1193+
}
1194+
1195+
// we only consider maxDeletesPerSweep more than the allowed number of installplans for delete at one time
1196+
ips := allIps
1197+
if len(ips) > maxInstallPlanCount + maxDeletesPerSweep {
1198+
ips = allIps[:maxInstallPlanCount+maxDeletesPerSweep]
1199+
}
1200+
1201+
byGen := map[int][]*v1alpha1.InstallPlan{}
1202+
for _, ip := range ips {
1203+
gen, ok := byGen[ip.Spec.Generation]
1204+
if !ok {
1205+
gen = make([]*v1alpha1.InstallPlan, 0)
1206+
}
1207+
byGen[ip.Spec.Generation] = append(gen, ip)
1208+
}
1209+
1210+
gens := make([]int, 0)
1211+
for i := range byGen {
1212+
gens = append(gens, i)
1213+
}
1214+
1215+
sort.Ints(gens)
1216+
1217+
toDelete := make([]*v1alpha1.InstallPlan, 0)
1218+
1219+
for _, i := range gens {
1220+
g := byGen[i]
1221+
1222+
if len(ips)-len(toDelete) <= maxInstallPlanCount {
1223+
break
1224+
}
1225+
1226+
// if removing all installplans at this generation doesn't dip below the max, safe to delete all of them
1227+
if len(ips)-len(toDelete)-len(g) >= maxInstallPlanCount {
1228+
toDelete = append(toDelete, g...)
1229+
continue
1230+
}
1231+
1232+
// CreationTimestamp sorting shouldn't ever be hit unless there is a bug that causes installplans to be
1233+
// generated without bumping the generation. It is here as a safeguard only.
1234+
1235+
// sort by creation time
1236+
sort.Slice(g, func(i, j int) bool {
1237+
if !g[i].CreationTimestamp.Equal(&g[j].CreationTimestamp) {
1238+
return g[i].CreationTimestamp.Before(&g[j].CreationTimestamp)
1239+
}
1240+
// final fallback to lexicographic sort, in case many installplans are created with the same timestamp
1241+
return g[i].GetName() < g[j].GetName()
1242+
})
1243+
toDelete = append(toDelete, g[:len(ips)-len(toDelete)-maxInstallPlanCount]...)
1244+
}
1245+
1246+
for _, i := range toDelete {
1247+
if err := o.client.OperatorsV1alpha1().InstallPlans(namespace).Delete(i.GetName(), &metav1.DeleteOptions{}); err != nil {
1248+
log.WithField("deleting", i.GetName()).WithError(err).Warn("error GCing old installplan - may have already been deleted")
1249+
}
1250+
}
1251+
}
1252+
11761253
func (o *Operator) syncInstallPlans(obj interface{}) (syncError error) {
11771254
plan, ok := obj.(*v1alpha1.InstallPlan)
11781255
if !ok {

pkg/controller/operators/catalog/operator_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import (
66
"errors"
77
"fmt"
88
"io/ioutil"
9+
"math/rand"
10+
"reflect"
911
"strings"
1012
"testing"
13+
"testing/quick"
1114
"time"
1215

1316
"github.com/sirupsen/logrus"
@@ -27,6 +30,7 @@ import (
2730
"k8s.io/apimachinery/pkg/types"
2831
utilclock "k8s.io/apimachinery/pkg/util/clock"
2932
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
33+
"k8s.io/apiserver/pkg/storage/names"
3034
fakedynamic "k8s.io/client-go/dynamic/fake"
3135
"k8s.io/client-go/informers"
3236
k8sfake "k8s.io/client-go/kubernetes/fake"
@@ -342,6 +346,82 @@ func TestRemoveDeprecatedStoredVersions(t *testing.T) {
342346
}
343347
}
344348

349+
type ipSet []v1alpha1.InstallPlan
350+
351+
func (ipSet) Generate(rand *rand.Rand, size int) reflect.Value {
352+
ips := []v1alpha1.InstallPlan{}
353+
354+
// each i is the generation value
355+
for i := 0; i < rand.Intn(size)+1; i++ {
356+
357+
// generate a few at each generation to account for bugs that don't increment the generation
358+
for j := 0; j < rand.Intn(3); j++ {
359+
ips = append(ips, v1alpha1.InstallPlan{
360+
ObjectMeta: metav1.ObjectMeta{
361+
Namespace: "ns",
362+
Name: names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%d", i)),
363+
},
364+
Spec: v1alpha1.InstallPlanSpec{
365+
Generation: i,
366+
},
367+
})
368+
}
369+
}
370+
return reflect.ValueOf(ipSet(ips))
371+
}
372+
373+
func TestGCInstallPlans(t *testing.T) {
374+
f := func(ips ipSet) bool {
375+
if len(ips) == 0 {
376+
return true
377+
}
378+
ctx, cancel := context.WithCancel(context.TODO())
379+
defer cancel()
380+
381+
var maxGen int64 = 0
382+
for _, i := range ips {
383+
if g := i.Generation; g > maxGen {
384+
maxGen = g
385+
}
386+
}
387+
objs := make([]runtime.Object, 0)
388+
for _, i := range ips {
389+
objs = append(objs, i.DeepCopy())
390+
}
391+
op, err := NewFakeOperator(ctx, "ns", []string{"ns"}, withClientObjs(objs...))
392+
require.NoError(t, err)
393+
394+
out := make([]v1alpha1.InstallPlan, 0)
395+
for {
396+
op.gcInstallPlans(logrus.New(), "ns")
397+
require.NoError(t, err)
398+
399+
outList, err := op.client.OperatorsV1alpha1().InstallPlans("ns").List(metav1.ListOptions{})
400+
require.NoError(t, err)
401+
out = outList.Items
402+
403+
if len(out) <= maxInstallPlanCount {
404+
break
405+
}
406+
}
407+
408+
keptMax := false
409+
for _, o := range out {
410+
if o.Generation == maxGen {
411+
keptMax = true
412+
break
413+
}
414+
}
415+
require.True(t, keptMax)
416+
417+
if len(ips) < maxInstallPlanCount {
418+
return len(out) == len(ips)
419+
}
420+
return len(out) == maxInstallPlanCount
421+
}
422+
require.NoError(t, quick.Check(f, nil))
423+
}
424+
345425
func TestExecutePlan(t *testing.T) {
346426
namespace := "ns"
347427

0 commit comments

Comments
 (0)