Skip to content

Commit 4d580eb

Browse files
committed
fix(installplan): don't store the manifest contents on the installplan
the installplan status.plan field stores a list of all manifests that will be installed when the installplan is approved. if the installplan is resolving a large set of dependencies, it is possible for it to exceed various limits in kube (grpc request size limit, etcd value size limit, etc). this commit changes the way objects are tracked if they come from bundle images. in that case, the manifests are already unpacked onto the cluster into a configmap. the installplan now stores a reference to the source configmap for each step, rather than storing the manifest itself. this allows the installplan to scale to a large number of resolved objects.
1 parent 759ff45 commit 4d580eb

File tree

8 files changed

+346
-30
lines changed

8 files changed

+346
-30
lines changed

pkg/controller/bundle/bundle_unpacker.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,27 @@ func (b *BundleUnpackResult) Bundle() *api.Bundle {
3333
return b.bundle
3434
}
3535

36+
func (b *BundleUnpackResult) Name() string {
37+
return b.name
38+
}
39+
40+
// SetCondition replaces the existing BundleLookupCondition of the same type, or adds it if it was not found.
41+
func (b *BundleUnpackResult) SetCondition(cond operatorsv1alpha1.BundleLookupCondition) operatorsv1alpha1.BundleLookupCondition {
42+
for i, existing := range b.Conditions {
43+
if existing.Type != cond.Type {
44+
continue
45+
}
46+
if existing.Status == cond.Status && existing.Reason == cond.Reason {
47+
cond.LastTransitionTime = existing.LastTransitionTime
48+
}
49+
b.Conditions[i] = cond
50+
return cond
51+
}
52+
b.Conditions = append(b.Conditions, cond)
53+
54+
return cond
55+
}
56+
3657
var catalogSourceGVK = operatorsv1alpha1.SchemeGroupVersion.WithKind(operatorsv1alpha1.CatalogSourceKind)
3758

3859
func newBundleUnpackResult(lookup *operatorsv1alpha1.BundleLookup) *BundleUnpackResult {
@@ -250,16 +271,26 @@ const (
250271
CatalogSourceMissingMessage = "referenced catalogsource not found"
251272
JobIncompleteReason = "JobIncomplete"
252273
JobIncompleteMessage = "unpack job not completed"
274+
JobNotStartedReason = "JobNotStarted"
275+
JobNotStartedMessage = "unpack job not yet started"
276+
NotUnpackedReason = "BundleNotUnpacked"
277+
NotUnpackedMessage = "bundle contents have not yet been persisted to installplan status"
253278
)
254279

255280
func (c *ConfigMapUnpacker) UnpackBundle(lookup *operatorsv1alpha1.BundleLookup) (result *BundleUnpackResult, err error) {
256281
result = newBundleUnpackResult(lookup)
282+
283+
// if pending condition is missing, bundle has already been unpacked
257284
cond := result.GetCondition(operatorsv1alpha1.BundleLookupPending)
285+
if cond.Status == corev1.ConditionUnknown {
286+
return result, nil
287+
}
288+
258289
now := c.now()
259290

260291
var cs *operatorsv1alpha1.CatalogSource
261292
if cs, err = c.csLister.CatalogSources(result.CatalogSourceRef.Namespace).Get(result.CatalogSourceRef.Name); err != nil {
262-
if apierrors.IsNotFound(err) && cond.Status != corev1.ConditionTrue && cond.Reason != CatalogSourceMissingReason {
293+
if apierrors.IsNotFound(err) && cond.Reason != CatalogSourceMissingReason {
263294
cond.Status = corev1.ConditionTrue
264295
cond.Reason = CatalogSourceMissingReason
265296
cond.Message = CatalogSourceMissingMessage
@@ -303,7 +334,7 @@ func (c *ConfigMapUnpacker) UnpackBundle(lookup *operatorsv1alpha1.BundleLookup)
303334
return
304335
}
305336

306-
if !jobConditionTrue(job, batchv1.JobComplete) && cond.Status != corev1.ConditionTrue && cond.Reason != JobIncompleteReason {
337+
if !jobConditionTrue(job, batchv1.JobComplete) && (cond.Status != corev1.ConditionTrue || cond.Reason != JobIncompleteReason) {
307338
cond.Status = corev1.ConditionTrue
308339
cond.Reason = JobIncompleteReason
309340
cond.Message = JobIncompleteMessage
@@ -318,6 +349,10 @@ func (c *ConfigMapUnpacker) UnpackBundle(lookup *operatorsv1alpha1.BundleLookup)
318349
return
319350
}
320351

352+
if result.Bundle() == nil || len(result.Bundle().GetObject()) == 0 {
353+
return
354+
}
355+
321356
// A successful load should remove the pending condition
322357
result.RemoveCondition(operatorsv1alpha1.BundleLookupPending)
323358

pkg/controller/bundle/bundle_unpacker_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ func TestConfigMapUnpacker(t *testing.T) {
7171
Namespace: "ns-a",
7272
Name: "src-a",
7373
},
74+
Conditions: []operatorsv1alpha1.BundleLookupCondition{
75+
{
76+
Type: operatorsv1alpha1.BundleLookupPending,
77+
Status: corev1.ConditionTrue,
78+
Reason: JobNotStartedReason,
79+
Message: JobNotStartedMessage,
80+
},
81+
},
7482
},
7583
},
7684
expected: expected{
@@ -116,6 +124,14 @@ func TestConfigMapUnpacker(t *testing.T) {
116124
Namespace: "ns-a",
117125
Name: "src-a",
118126
},
127+
Conditions: []operatorsv1alpha1.BundleLookupCondition{
128+
{
129+
Type: operatorsv1alpha1.BundleLookupPending,
130+
Status: corev1.ConditionTrue,
131+
Reason: JobNotStartedReason,
132+
Message: JobNotStartedMessage,
133+
},
134+
},
119135
},
120136
},
121137
expected: expected{
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package catalog
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
8+
"github.com/operator-framework/operator-registry/pkg/configmap"
9+
errorwrap "github.com/pkg/errors"
10+
"github.com/sirupsen/logrus"
11+
v1 "k8s.io/client-go/listers/core/v1"
12+
13+
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver"
14+
)
15+
16+
// ManifestResolver can dereference a manifest for a step. Steps may embed manifests directly or reference content
17+
// in configmaps
18+
type ManifestResolver interface {
19+
ManifestForStep(step *v1alpha1.Step) (string, error)
20+
}
21+
22+
// manifestResolver caches manifest from unpacked bundles (via configmaps)
23+
type manifestResolver struct {
24+
configMapLister v1.ConfigMapLister
25+
unpackedSteps map[string][]v1alpha1.StepResource
26+
namespace string
27+
logger logrus.FieldLogger
28+
}
29+
30+
func newManifestResolver(namespace string, configMapLister v1.ConfigMapLister, logger logrus.FieldLogger) *manifestResolver {
31+
return &manifestResolver{
32+
namespace: namespace,
33+
configMapLister: configMapLister,
34+
unpackedSteps: map[string][]v1alpha1.StepResource{},
35+
logger: logger,
36+
}
37+
}
38+
39+
// ManifestForStep always returns the manifest that should be applied to the cluster for a given step
40+
// the manifest field in the installplan status can contain a reference to a configmap instead
41+
func (r *manifestResolver) ManifestForStep(step *v1alpha1.Step) (string, error) {
42+
manifest := step.Resource.Manifest
43+
ref := refForStep(step, r.logger)
44+
if ref == nil {
45+
return manifest, nil
46+
}
47+
48+
log := r.logger.WithFields(logrus.Fields{"resolving": step.Resolving, "step": step.Resource.Name})
49+
log.WithField("ref", ref).Debug("step is a reference to configmap")
50+
51+
usteps, err := r.unpackedStepsForBundle(step.Resolving, ref)
52+
if err != nil {
53+
return "", err
54+
}
55+
56+
log.Debugf("checking cache for unpacked step")
57+
// need to find the real manifest from the unpacked steps
58+
for _, u := range usteps {
59+
if u.Name == step.Resource.Name &&
60+
u.Kind == step.Resource.Kind &&
61+
u.Version == step.Resource.Version &&
62+
u.Group == step.Resource.Group {
63+
manifest = u.Manifest
64+
log.WithField("manifest", manifest).Debug("step replaced with unpacked value")
65+
break
66+
}
67+
}
68+
if manifest == step.Resource.Manifest {
69+
return "", fmt.Errorf("couldn't find unpacked step for %v", step)
70+
}
71+
return manifest, nil
72+
}
73+
74+
func (r *manifestResolver) unpackedStepsForBundle(bundleName string, ref *UnpackedBundleReference) ([]v1alpha1.StepResource, error) {
75+
usteps, ok := r.unpackedSteps[bundleName]
76+
if ok {
77+
return usteps, nil
78+
}
79+
cm, err := r.configMapLister.ConfigMaps(ref.Namespace).Get(ref.Name)
80+
if err != nil {
81+
return nil, errorwrap.Wrapf(err, "error finding unpacked bundle configmap for ref %v", *ref)
82+
}
83+
loader := configmap.NewBundleLoader()
84+
bundle, err := loader.Load(cm)
85+
if err != nil {
86+
return nil, errorwrap.Wrapf(err, "error loading unpacked bundle configmap for ref %v", *ref)
87+
}
88+
steps, err := resolver.NewStepResourceFromBundle(bundle, r.namespace, ref.Replaces, ref.CatalogSourceName, ref.CatalogSourceNamespace)
89+
if err != nil {
90+
return nil, errorwrap.Wrapf(err, "error calculating steps for ref %v", *ref)
91+
}
92+
r.unpackedSteps[bundleName] = steps
93+
return steps, nil
94+
}
95+
96+
func refForStep(step *v1alpha1.Step, log logrus.FieldLogger) *UnpackedBundleReference {
97+
log = log.WithFields(logrus.Fields{"resolving": step.Resolving, "step": step.Resource.Name})
98+
var ref UnpackedBundleReference
99+
if err := json.Unmarshal([]byte(step.Resource.Manifest), &ref); err != nil {
100+
log.Debug("step is not a reference to an unpacked bundle (this is not an error if the step is a manifest)")
101+
return nil
102+
}
103+
log = log.WithField("ref", ref)
104+
if ref.Kind != "ConfigMap" || ref.Name == "" || ref.Namespace == "" || ref.CatalogSourceName == "" || ref.CatalogSourceNamespace == "" {
105+
log.Debug("step is not a reference to an unpacked bundle (this is not an error if the step is a manifest)")
106+
return nil
107+
}
108+
return &ref
109+
}

pkg/controller/operators/catalog/operator.go

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,16 @@ func (o *Operator) createInstallPlan(namespace string, gen int, subs []*v1alpha1
10981098
return reference.GetReference(res)
10991099
}
11001100

1101+
type UnpackedBundleReference struct {
1102+
Kind string `json:"kind"`
1103+
Name string `json:"name"`
1104+
Namespace string `json:"namespace"`
1105+
CatalogSourceName string `json:"catalogSourceName"`
1106+
CatalogSourceNamespace string `json:"catalogSourceNamespace"`
1107+
Replaces string `json:"replaces"`
1108+
}
1109+
1110+
// unpackBundles makes one walk through the bundlelookups and attempts to progress them
11011111
func (o *Operator) unpackBundles(plan *v1alpha1.InstallPlan) (bool, *v1alpha1.InstallPlan, error) {
11021112
out := plan.DeepCopy()
11031113
unpacked := true
@@ -1110,32 +1120,53 @@ func (o *Operator) unpackBundles(plan *v1alpha1.InstallPlan) (bool, *v1alpha1.In
11101120
errs = append(errs, err)
11111121
continue
11121122
}
1123+
out.Status.BundleLookups[i] = *res.BundleLookup
11131124

1114-
if res == nil {
1125+
// if pending condition is present, still waiting for the job to unpack to configmap
1126+
if res.GetCondition(v1alpha1.BundleLookupPending).Status == corev1.ConditionTrue {
11151127
unpacked = false
11161128
continue
11171129
}
11181130

1119-
out.Status.BundleLookups[i] = *res.BundleLookup
1120-
if res.Bundle() == nil || len(res.Bundle().GetObject()) == 0 {
1121-
unpacked = false
1131+
// if packed condition is missing, bundle has already been unpacked into steps, continue
1132+
if res.GetCondition(resolver.BundleLookupConditionPacked).Status == corev1.ConditionUnknown {
11221133
continue
11231134
}
11241135

1136+
// Ensure that bundle can be applied by the current version of OLM by converting to steps
11251137
steps, err := resolver.NewStepsFromBundle(res.Bundle(), out.GetNamespace(), res.Replaces, res.CatalogSourceRef.Name, res.CatalogSourceRef.Namespace)
11261138
if err != nil {
1127-
errs = append(errs, fmt.Errorf("failed to turn bundle into steps: %s", err.Error()))
1139+
errs = append(errs, fmt.Errorf("failed to turn bundle into steps: %v", err))
11281140
unpacked = false
11291141
continue
11301142
}
11311143

1132-
// Add steps and remove resolved bundle lookup
1144+
// step manifests are replaced with references to the configmap containing them
1145+
for i, s := range steps {
1146+
ref := UnpackedBundleReference{
1147+
Kind: "ConfigMap",
1148+
Namespace: res.CatalogSourceRef.Namespace,
1149+
Name: res.Name(),
1150+
CatalogSourceName: res.CatalogSourceRef.Name,
1151+
CatalogSourceNamespace: res.CatalogSourceRef.Namespace,
1152+
Replaces: res.Replaces,
1153+
}
1154+
r, err := json.Marshal(&ref)
1155+
if err != nil {
1156+
errs = append(errs, fmt.Errorf("failed to generate reference for configmap: %v", err))
1157+
unpacked = false
1158+
continue
1159+
}
1160+
s.Resource.Manifest = string(r)
1161+
steps[i] = s
1162+
}
1163+
res.RemoveCondition(resolver.BundleLookupConditionPacked)
1164+
out.Status.BundleLookups[i] = *res.BundleLookup
11331165
out.Status.Plan = append(out.Status.Plan, steps...)
1134-
out.Status.BundleLookups = append(out.Status.BundleLookups[:i], out.Status.BundleLookups[i+1:]...)
1135-
i--
11361166
}
11371167

11381168
if err := utilerrors.NewAggregate(errs); err != nil {
1169+
o.logger.Debugf("failed to unpack bundles: %v", err)
11391170
return false, nil, err
11401171
}
11411172

@@ -1436,6 +1467,7 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
14361467
}
14371468

14381469
ensurer := newStepEnsurer(kubeclient, crclient, dynamicClient)
1470+
r := newManifestResolver(plan.GetNamespace(), o.lister.CoreV1().ConfigMapLister(), o.logger)
14391471

14401472
for i, step := range plan.Status.Plan {
14411473
switch step.Status {
@@ -1474,6 +1506,10 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
14741506
continue
14751507
}
14761508
case v1alpha1.StepStatusUnknown, v1alpha1.StepStatusNotPresent:
1509+
manifest, err := r.ManifestForStep(step)
1510+
if err != nil {
1511+
return err
1512+
}
14771513
o.logger.WithFields(logrus.Fields{"kind": step.Resource.Kind, "name": step.Resource.Name}).Debug("execute resource")
14781514
switch step.Resource.Kind {
14791515
case crdKind:
@@ -1546,7 +1582,7 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
15461582
case v1alpha1.ClusterServiceVersionKind:
15471583
// Marshal the manifest into a CSV instance.
15481584
var csv v1alpha1.ClusterServiceVersion
1549-
err := json.Unmarshal([]byte(step.Resource.Manifest), &csv)
1585+
err := json.Unmarshal([]byte(manifest), &csv)
15501586
if err != nil {
15511587
return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
15521588
}
@@ -1579,7 +1615,7 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
15791615
case v1alpha1.SubscriptionKind:
15801616
// Marshal the manifest into a subscription instance.
15811617
var sub v1alpha1.Subscription
1582-
err := json.Unmarshal([]byte(step.Resource.Manifest), &sub)
1618+
err := json.Unmarshal([]byte(manifest), &sub)
15831619
if err != nil {
15841620
return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
15851621
}
@@ -1612,7 +1648,7 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
16121648
case clusterRoleKind:
16131649
// Marshal the manifest into a ClusterRole instance.
16141650
var cr rbacv1.ClusterRole
1615-
err := json.Unmarshal([]byte(step.Resource.Manifest), &cr)
1651+
err := json.Unmarshal([]byte(manifest), &cr)
16161652
if err != nil {
16171653
return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
16181654
}
@@ -1627,7 +1663,7 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
16271663
case clusterRoleBindingKind:
16281664
// Marshal the manifest into a RoleBinding instance.
16291665
var rb rbacv1.ClusterRoleBinding
1630-
err := json.Unmarshal([]byte(step.Resource.Manifest), &rb)
1666+
err := json.Unmarshal([]byte(manifest), &rb)
16311667
if err != nil {
16321668
return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
16331669
}
@@ -1642,7 +1678,7 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
16421678
case roleKind:
16431679
// Marshal the manifest into a Role instance.
16441680
var r rbacv1.Role
1645-
err := json.Unmarshal([]byte(step.Resource.Manifest), &r)
1681+
err := json.Unmarshal([]byte(manifest), &r)
16461682
if err != nil {
16471683
return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
16481684
}
@@ -1665,7 +1701,7 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
16651701
case roleBindingKind:
16661702
// Marshal the manifest into a RoleBinding instance.
16671703
var rb rbacv1.RoleBinding
1668-
err := json.Unmarshal([]byte(step.Resource.Manifest), &rb)
1704+
err := json.Unmarshal([]byte(manifest), &rb)
16691705
if err != nil {
16701706
return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
16711707
}
@@ -1688,7 +1724,7 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
16881724
case serviceAccountKind:
16891725
// Marshal the manifest into a ServiceAccount instance.
16901726
var sa corev1.ServiceAccount
1691-
err := json.Unmarshal([]byte(step.Resource.Manifest), &sa)
1727+
err := json.Unmarshal([]byte(manifest), &sa)
16921728
if err != nil {
16931729
return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
16941730
}
@@ -1711,7 +1747,7 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
17111747
case serviceKind:
17121748
// Marshal the manifest into a Service instance
17131749
var s corev1.Service
1714-
err := json.Unmarshal([]byte(step.Resource.Manifest), &s)
1750+
err := json.Unmarshal([]byte(manifest), &s)
17151751
if err != nil {
17161752
return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
17171753
}
@@ -1739,7 +1775,7 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
17391775
}
17401776

17411777
// Marshal the manifest into an unstructured object
1742-
dec := yaml.NewYAMLOrJSONDecoder(strings.NewReader(step.Resource.Manifest), 10)
1778+
dec := yaml.NewYAMLOrJSONDecoder(strings.NewReader(manifest), 10)
17431779
unstructuredObject := &unstructured.Unstructured{}
17441780
if err := dec.Decode(unstructuredObject); err != nil {
17451781
return errorwrap.Wrapf(err, "error decoding %s object to an unstructured object", step.Resource.Name)

0 commit comments

Comments
 (0)