Skip to content

Commit d7af223

Browse files
committed
Annotate CRDs that are installed alongside CSVs.
In the absence of a way to correlate installed resources back to their bundle of origin, it's possible for upgrades to complete early. Upgrades begin when the catalog operator creates a new CSV (with .spec.replaces set) as part of InstallPlan execution, and complete when the olm operator sees that the new CSV has .status.phase Succeeded. When both versions of the operator being upgraded own the same CRD, the new CSV may use the existing CRD to satisfy its CRD ownership requirement, thereby transitioning to Succeeded and deleting the old CSV. This problem can be avoided for namespace-scoped bundle resources, which perform the transfer by adding the new CSV as an owner reference and allowing garbage collection to remove the old owner reference post-upgrade. This is not possible for CRDs because cluster-scoped resources cannot have owner references to a namespace-scoped resource like ClusterServiceVersion. It may be necessary to make a similar change affecting other cluster-scoped resources, but CRDs are a common culprit of early upgrade completion. Ideally, a first-class, cluster-scoped Bundle resource will obsolete the need to use an annotation to track bundle of origin. Signed-off-by: Ben Luddy <[email protected]>
1 parent 85be6e5 commit d7af223

File tree

12 files changed

+1272
-35
lines changed

12 files changed

+1272
-35
lines changed

pkg/controller/operators/catalog/operator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1642,7 +1642,7 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
16421642
o.logger.Errorf("failed to get a client for plan execution- %v", err)
16431643
return err
16441644
}
1645-
b := newBuilder(builderKubeClient, builderDynamicClient, o.csvProvidedAPIsIndexer, r, o.logger)
1645+
b := newBuilder(plan, o.lister.OperatorsV1alpha1().ClusterServiceVersionLister(), builderKubeClient, builderDynamicClient, o.csvProvidedAPIsIndexer, r, o.logger)
16461646

16471647
for i, step := range plan.Status.Plan {
16481648
doStep := true

pkg/controller/operators/catalog/step.go

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,27 @@ package catalog
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

7-
"k8s.io/client-go/dynamic"
8-
"k8s.io/client-go/tools/cache"
8+
"github.com/pkg/errors"
9+
"github.com/sirupsen/logrus"
910

10-
"github.com/operator-framework/api/pkg/operators/v1alpha1"
11-
crdlib "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/crd"
12-
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
13-
errorwrap "github.com/pkg/errors"
14-
logger "github.com/sirupsen/logrus"
1511
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1612
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
1713
apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
1814
apiextensionsv1beta1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
19-
2015
k8serrors "k8s.io/apimachinery/pkg/api/errors"
2116
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
18+
"k8s.io/apimachinery/pkg/util/yaml"
19+
"k8s.io/client-go/dynamic"
20+
"k8s.io/client-go/tools/cache"
21+
22+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
23+
listersv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1"
24+
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/internal/alongside"
25+
crdlib "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/crd"
26+
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
2227
)
2328

2429
// Stepper manages cluster interactions based on the step.
@@ -36,16 +41,22 @@ func (s StepperFunc) Status() (v1alpha1.StepStatus, error) {
3641
// Builder holds clients and data structures required for the StepBuilder to work
3742
// Builder attributes are not to meant to be accessed outside the StepBuilder method
3843
type builder struct {
44+
plan *v1alpha1.InstallPlan
45+
csvLister listersv1alpha1.ClusterServiceVersionLister
3946
opclient operatorclient.ClientInterface
4047
dynamicClient dynamic.Interface
4148
manifestResolver ManifestResolver
4249
csvToProvidedAPIs map[string]cache.Indexer
43-
logger logger.FieldLogger
50+
logger logrus.FieldLogger
51+
52+
annotator alongside.Annotator
4453
}
4554

46-
func newBuilder(opclient operatorclient.ClientInterface, dynamicClient dynamic.Interface, csvToProvidedAPIs map[string]cache.Indexer,
47-
manifestResolver ManifestResolver, logger logger.FieldLogger) *builder {
55+
func newBuilder(plan *v1alpha1.InstallPlan, csvLister listersv1alpha1.ClusterServiceVersionLister, opclient operatorclient.ClientInterface, dynamicClient dynamic.Interface, csvToProvidedAPIs map[string]cache.Indexer,
56+
manifestResolver ManifestResolver, logger logrus.FieldLogger) *builder {
4857
return &builder{
58+
plan: plan,
59+
csvLister: csvLister,
4960
opclient: opclient,
5061
dynamicClient: dynamicClient,
5162
manifestResolver: manifestResolver,
@@ -62,6 +73,29 @@ func (n notSupportedStepperErr) Error() string {
6273
return n.message
6374
}
6475

76+
func setInstalledAlongsideAnnotation(a alongside.Annotator, o metav1.Object, namespace string, name string, lister listersv1alpha1.ClusterServiceVersionLister) {
77+
var (
78+
nns []alongside.NamespacedName
79+
)
80+
81+
// Only keep references to existing and non-copied CSVs to
82+
// avoid unbounded growth.
83+
for _, nn := range a.FromObject(o) {
84+
if csv, err := lister.ClusterServiceVersions(nn.Namespace).Get(nn.Name); k8serrors.IsNotFound(err) {
85+
continue
86+
} else if err == nil && csv.IsCopied() {
87+
continue
88+
}
89+
// CSV exists and is not copied OR err is non-nil, but
90+
// not 404. Safer to assume it exists in unhandled
91+
// error cases and try again next time.
92+
nns = append(nns, nn)
93+
}
94+
nns = append(nns, alongside.NamespacedName{Namespace: namespace, Name: name})
95+
96+
a.ToObject(o, nns)
97+
}
98+
6599
// step is a factory that creates StepperFuncs based on the install plan step Kind.
66100
func (b *builder) create(step v1alpha1.Step) (Stepper, error) {
67101
manifest, err := b.manifestResolver.ManifestForStep(&step)
@@ -75,6 +109,20 @@ func (b *builder) create(step v1alpha1.Step) (Stepper, error) {
75109
if err != nil {
76110
return nil, err
77111
}
112+
113+
if step.Resolving != "" {
114+
var u unstructured.Unstructured
115+
if err := yaml.NewYAMLOrJSONDecoder(strings.NewReader(manifest), 32).Decode(&u); err != nil {
116+
return nil, err
117+
}
118+
setInstalledAlongsideAnnotation(b.annotator, &u, b.plan.GetNamespace(), step.Resolving, b.csvLister)
119+
if b, err := u.MarshalJSON(); err != nil {
120+
return nil, err
121+
} else {
122+
manifest = string(b)
123+
}
124+
}
125+
78126
switch version {
79127
case crdlib.V1Version:
80128
return b.NewCRDV1Step(b.opclient.ApiextensionsInterface().ApiextensionsV1(), &step, manifest), nil
@@ -98,7 +146,7 @@ func (b *builder) NewCRDV1Step(client apiextensionsv1client.ApiextensionsV1Inter
98146
if k8serrors.IsNotFound(err) {
99147
return v1alpha1.StepStatusNotPresent, nil
100148
} else {
101-
return v1alpha1.StepStatusNotPresent, errorwrap.Wrapf(err, "error finding the %s CRD", crd.Name)
149+
return v1alpha1.StepStatusNotPresent, errors.Wrapf(err, "error finding the %s CRD", crd.Name)
102150
}
103151
}
104152
established, namesAccepted := false, false
@@ -128,23 +176,23 @@ func (b *builder) NewCRDV1Step(client apiextensionsv1client.ApiextensionsV1Inter
128176
currentCRD, _ := client.CustomResourceDefinitions().Get(context.TODO(), crd.GetName(), metav1.GetOptions{})
129177
crd.SetResourceVersion(currentCRD.GetResourceVersion())
130178
if err = validateV1CRDCompatibility(b.dynamicClient, currentCRD, crd); err != nil {
131-
return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error validating existing CRs against new CRD's schema: %s", step.Resource.Name)
179+
return v1alpha1.StepStatusUnknown, errors.Wrapf(err, "error validating existing CRs against new CRD's schema: %s", step.Resource.Name)
132180
}
133181

134182
// check to see if stored versions changed and whether the upgrade could cause potential data loss
135183
safe, err := crdlib.SafeStorageVersionUpgrade(currentCRD, crd)
136184
if !safe {
137185
b.logger.Errorf("risk of data loss updating %s: %s", step.Resource.Name, err)
138-
return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "risk of data loss updating %s", step.Resource.Name)
186+
return v1alpha1.StepStatusUnknown, errors.Wrapf(err, "risk of data loss updating %s", step.Resource.Name)
139187
}
140188
if err != nil {
141-
return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "checking CRD for potential data loss updating %s", step.Resource.Name)
189+
return v1alpha1.StepStatusUnknown, errors.Wrapf(err, "checking CRD for potential data loss updating %s", step.Resource.Name)
142190
}
143191

144192
// Update CRD to new version
145193
_, err = client.CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{})
146194
if err != nil {
147-
return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error updating CRD: %s", step.Resource.Name)
195+
return v1alpha1.StepStatusUnknown, errors.Wrapf(err, "error updating CRD: %s", step.Resource.Name)
148196
}
149197
// If it already existed, mark the step as Present.
150198
// they were equal - mark CRD as present
@@ -173,7 +221,7 @@ func (b *builder) NewCRDV1Beta1Step(client apiextensionsv1beta1client.Apiextensi
173221
if k8serrors.IsNotFound(err) {
174222
return v1alpha1.StepStatusNotPresent, nil
175223
} else {
176-
return v1alpha1.StepStatusNotPresent, errorwrap.Wrapf(err, "error finding the %s CRD", crd.Name)
224+
return v1alpha1.StepStatusNotPresent, errors.Wrapf(err, "error finding the %s CRD", crd.Name)
177225
}
178226
}
179227
established, namesAccepted := false, false
@@ -204,23 +252,23 @@ func (b *builder) NewCRDV1Beta1Step(client apiextensionsv1beta1client.Apiextensi
204252
crd.SetResourceVersion(currentCRD.GetResourceVersion())
205253

206254
if err = validateV1Beta1CRDCompatibility(b.dynamicClient, currentCRD, crd); err != nil {
207-
return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error validating existing CRs against new CRD's schema: %s", step.Resource.Name)
255+
return v1alpha1.StepStatusUnknown, errors.Wrapf(err, "error validating existing CRs against new CRD's schema: %s", step.Resource.Name)
208256
}
209257

210258
// check to see if stored versions changed and whether the upgrade could cause potential data loss
211259
safe, err := crdlib.SafeStorageVersionUpgrade(currentCRD, crd)
212260
if !safe {
213261
b.logger.Errorf("risk of data loss updating %s: %s", step.Resource.Name, err)
214-
return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "risk of data loss updating %s", step.Resource.Name)
262+
return v1alpha1.StepStatusUnknown, errors.Wrapf(err, "risk of data loss updating %s", step.Resource.Name)
215263
}
216264
if err != nil {
217-
return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "checking CRD for potential data loss updating %s", step.Resource.Name)
265+
return v1alpha1.StepStatusUnknown, errors.Wrapf(err, "checking CRD for potential data loss updating %s", step.Resource.Name)
218266
}
219267

220268
// Update CRD to new version
221269
_, err = client.CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{})
222270
if err != nil {
223-
return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error updating CRD: %s", step.Resource.Name)
271+
return v1alpha1.StepStatusUnknown, errors.Wrapf(err, "error updating CRD: %s", step.Resource.Name)
224272
}
225273
// If it already existed, mark the step as Present.
226274
// they were equal - mark CRD as present
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package catalog
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
8+
"k8s.io/apimachinery/pkg/api/errors"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/runtime/schema"
11+
12+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
13+
v1alpha1listers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1"
14+
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/internal/alongside"
15+
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister/operatorlisterfakes"
16+
)
17+
18+
func TestSetInstalledAlongsideAnnotation(t *testing.T) {
19+
for _, tc := range []struct {
20+
Name string
21+
NewNamespace string
22+
NewName string
23+
CSVs []v1alpha1.ClusterServiceVersion
24+
Before []alongside.NamespacedName
25+
After []alongside.NamespacedName
26+
}{
27+
{
28+
Name: "object annotated with bundle name",
29+
NewNamespace: "test-namespace",
30+
NewName: "test-name",
31+
After: []alongside.NamespacedName{
32+
{Namespace: "test-namespace", Name: "test-name"},
33+
},
34+
},
35+
{
36+
Name: "annotations referencing missing bundles removed",
37+
NewNamespace: "test-namespace",
38+
NewName: "test-name",
39+
Before: []alongside.NamespacedName{
40+
{Namespace: "missing-namespace", Name: "missing-name"},
41+
},
42+
After: []alongside.NamespacedName{
43+
{Namespace: "test-namespace", Name: "test-name"},
44+
},
45+
},
46+
{
47+
Name: "annotations referencing copied csv removed",
48+
NewNamespace: "test-namespace",
49+
NewName: "test-name",
50+
CSVs: []v1alpha1.ClusterServiceVersion{
51+
{
52+
ObjectMeta: metav1.ObjectMeta{
53+
Namespace: "copied-namespace",
54+
Name: "copied-name",
55+
},
56+
Status: v1alpha1.ClusterServiceVersionStatus{
57+
Reason: v1alpha1.CSVReasonCopied,
58+
},
59+
},
60+
},
61+
Before: []alongside.NamespacedName{
62+
{Namespace: "copied-namespace", Name: "copied-name"},
63+
},
64+
After: []alongside.NamespacedName{
65+
{Namespace: "test-namespace", Name: "test-name"},
66+
},
67+
},
68+
{
69+
Name: "annotations referencing found bundles preserved",
70+
NewNamespace: "test-namespace",
71+
NewName: "test-name",
72+
CSVs: []v1alpha1.ClusterServiceVersion{
73+
{
74+
ObjectMeta: metav1.ObjectMeta{
75+
Namespace: "found-namespace",
76+
Name: "found-name",
77+
},
78+
},
79+
},
80+
Before: []alongside.NamespacedName{
81+
{Namespace: "found-namespace", Name: "found-name"},
82+
},
83+
After: []alongside.NamespacedName{
84+
{Namespace: "found-namespace", Name: "found-name"},
85+
{Namespace: "test-namespace", Name: "test-name"},
86+
},
87+
},
88+
} {
89+
t.Run(tc.Name, func(t *testing.T) {
90+
csvsByNamespace := make(map[string][]*v1alpha1.ClusterServiceVersion)
91+
for _, csv := range tc.CSVs {
92+
csvsByNamespace[csv.GetNamespace()] = append(csvsByNamespace[csv.GetNamespace()], csv.DeepCopy())
93+
}
94+
95+
nsListers := make(map[string]v1alpha1listers.ClusterServiceVersionNamespaceLister)
96+
for ns, csvs := range csvsByNamespace {
97+
ns := ns
98+
csvs := csvs
99+
nslister := &operatorlisterfakes.FakeClusterServiceVersionNamespaceLister{}
100+
nslister.GetCalls(func(name string) (*v1alpha1.ClusterServiceVersion, error) {
101+
for _, csv := range csvs {
102+
if csv.GetName() == name {
103+
return csv, nil
104+
}
105+
}
106+
return nil, errors.NewNotFound(schema.GroupResource{}, name)
107+
})
108+
nsListers[ns] = nslister
109+
}
110+
111+
emptyLister := &operatorlisterfakes.FakeClusterServiceVersionNamespaceLister{}
112+
emptyLister.GetCalls(func(name string) (*v1alpha1.ClusterServiceVersion, error) {
113+
return nil, errors.NewNotFound(schema.GroupResource{}, name)
114+
})
115+
116+
csvLister := &operatorlisterfakes.FakeClusterServiceVersionLister{}
117+
csvLister.ClusterServiceVersionsCalls(func(namespace string) v1alpha1listers.ClusterServiceVersionNamespaceLister {
118+
if lister, ok := nsListers[namespace]; ok {
119+
return lister
120+
}
121+
return emptyLister
122+
})
123+
124+
var (
125+
o metav1.ObjectMeta
126+
a alongside.Annotator
127+
)
128+
a.ToObject(&o, tc.Before)
129+
setInstalledAlongsideAnnotation(a, &o, tc.NewNamespace, tc.NewName, csvLister)
130+
after := a.FromObject(&o)
131+
assert.Equal(t, tc.After, after)
132+
})
133+
}
134+
}

0 commit comments

Comments
 (0)