Skip to content

Commit 79e4d18

Browse files
Merge pull request #1669 from ecordell/gc-installplans
Bug 1859178: fix(installplans): GC older installplans
2 parents cc2112c + 91d4f2a commit 79e4d18

File tree

2 files changed

+158
-1
lines changed

2 files changed

+158
-1
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
extinf "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
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"
@@ -67,6 +69,8 @@ const (
6769
roleKind = "Role"
6870
roleBindingKind = "RoleBinding"
6971
generatedByKey = "olm.generated-by"
72+
maxInstallPlanCount = 5
73+
maxDeletesPerSweep = 5
7074
)
7175

7276
// Operator represents a Kubernetes operator that executes InstallPlans by
@@ -777,6 +781,8 @@ func (o *Operator) syncResolvingNamespace(obj interface{}) error {
777781
"id": queueinformer.NewLoopID(),
778782
})
779783

784+
o.gcInstallPlans(logger, namespace)
785+
780786
// get the set of sources that should be used for resolution and best-effort get their connections working
781787
logger.Debug("resolving sources")
782788

@@ -1180,6 +1186,77 @@ func (o *Operator) unpackBundles(plan *v1alpha1.InstallPlan) (bool, *v1alpha1.In
11801186
return unpacked, out, nil
11811187
}
11821188

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

pkg/controller/operators/catalog/operator_test.go

Lines changed: 81 additions & 1 deletion
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"
@@ -28,6 +31,7 @@ import (
2831
"k8s.io/apimachinery/pkg/types"
2932
utilclock "k8s.io/apimachinery/pkg/util/clock"
3033
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
34+
"k8s.io/apiserver/pkg/storage/names"
3135
fakedynamic "k8s.io/client-go/dynamic/fake"
3236
"k8s.io/client-go/informers"
3337
k8sfake "k8s.io/client-go/kubernetes/fake"
@@ -168,7 +172,7 @@ func TestSyncInstallPlanUnhappy(t *testing.T) {
168172
},
169173
},
170174
),
171-
err: fmt.Errorf("attenuated service account query failed - no operator group found that is managing this namespace"),
175+
err: fmt.Errorf("attenuated service account query failed - no operator group found that is managing this namespace"),
172176
},
173177
}
174178

@@ -186,6 +190,82 @@ func TestSyncInstallPlanUnhappy(t *testing.T) {
186190
}
187191
}
188192

193+
type ipSet []v1alpha1.InstallPlan
194+
195+
func (ipSet) Generate(rand *rand.Rand, size int) reflect.Value {
196+
ips := []v1alpha1.InstallPlan{}
197+
198+
// each i is the generation value
199+
for i := 0; i < rand.Intn(size)+1; i++ {
200+
201+
// generate a few at each generation to account for bugs that don't increment the generation
202+
for j := 0; j < rand.Intn(3); j++ {
203+
ips = append(ips, v1alpha1.InstallPlan{
204+
ObjectMeta: metav1.ObjectMeta{
205+
Namespace: "ns",
206+
Name: names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%d", i)),
207+
},
208+
Spec: v1alpha1.InstallPlanSpec{
209+
Generation: i,
210+
},
211+
})
212+
}
213+
}
214+
return reflect.ValueOf(ipSet(ips))
215+
}
216+
217+
func TestGCInstallPlans(t *testing.T) {
218+
f := func(ips ipSet) bool {
219+
if len(ips) == 0 {
220+
return true
221+
}
222+
ctx, cancel := context.WithCancel(context.TODO())
223+
defer cancel()
224+
225+
var maxGen int64 = 0
226+
for _, i := range ips {
227+
if g := i.Generation; g > maxGen {
228+
maxGen = g
229+
}
230+
}
231+
objs := make([]runtime.Object, 0)
232+
for _, i := range ips {
233+
objs = append(objs, i.DeepCopy())
234+
}
235+
op, err := NewFakeOperator(ctx, "ns", []string{"ns"}, withClientObjs(objs...))
236+
require.NoError(t, err)
237+
238+
out := make([]v1alpha1.InstallPlan, 0)
239+
for {
240+
op.gcInstallPlans(logrus.New(), "ns")
241+
require.NoError(t, err)
242+
243+
outList, err := op.client.OperatorsV1alpha1().InstallPlans("ns").List(ctx, metav1.ListOptions{})
244+
require.NoError(t, err)
245+
out = outList.Items
246+
247+
if len(out) <= maxInstallPlanCount {
248+
break
249+
}
250+
}
251+
252+
keptMax := false
253+
for _, o := range out {
254+
if o.Generation == maxGen {
255+
keptMax = true
256+
break
257+
}
258+
}
259+
require.True(t, keptMax)
260+
261+
if len(ips) < maxInstallPlanCount {
262+
return len(out) == len(ips)
263+
}
264+
return len(out) == maxInstallPlanCount
265+
}
266+
require.NoError(t, quick.Check(f, nil))
267+
}
268+
189269
func TestExecutePlan(t *testing.T) {
190270
namespace := "ns"
191271

0 commit comments

Comments
 (0)