Skip to content

Commit ad49491

Browse files
author
Jeff Peeler
committed
fix(olm): defer provided api update in operator groups
This is the case of a bug where the CSV status wasn't updated due to an invalid CSV. The cause for this was twofold: 1) The CSV and OperatorGroup reconcile loops were undoing each others changes continously, giving no chance for the CSV to get synced beyond provided api conflict detection. 2) Due to the way the provided APIs are produced differently for operator groups (uses GVKSTringToProvidedAPISet) and OLM (uses NewOperatorFromV1Alpha1CSV) when an invalid CSV is in use, the resolver attempts to continue to instruct that an operator group annotation update is needed beyond the point of that being true. The changes are to ensure that OLM does not get stuck attempting to update the operator group annotations when they are no longer needed combined with ensuring that the operator group sync loop does not try to prematurely dismiss a provided api before the CSV has a chance to check the requirements and produce status. In order to accomplish "premature detection", providedAPIsFromCSVs was modified to not only return the APIs but also the CSV that provides a given API.
1 parent b41bd08 commit ad49491

File tree

4 files changed

+155
-8
lines changed

4 files changed

+155
-8
lines changed

pkg/controller/operators/olm/operator.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,12 @@ func (a *Operator) transitionCSVState(in v1alpha1.ClusterServiceVersion) (out *v
12521252
if unionedAnnotations == nil {
12531253
unionedAnnotations = make(map[string]string)
12541254
}
1255+
if unionedAnnotations[v1.OperatorGroupProvidedAPIsAnnotationKey] == union.String() {
1256+
// resolver may think apis need adding with invalid input, so continue when there's no work
1257+
// to be done so that the CSV can progress far enough to get requirements checked
1258+
a.logger.Debug("operator group annotations up to date, continuing")
1259+
break
1260+
}
12551261
unionedAnnotations[v1.OperatorGroupProvidedAPIsAnnotationKey] = union.String()
12561262
operatorGroup.SetAnnotations(unionedAnnotations)
12571263
if _, err := a.client.OperatorsV1().OperatorGroups(operatorGroup.GetNamespace()).Update(operatorGroup); err != nil && !k8serrors.IsNotFound(err) {

pkg/controller/operators/olm/operator_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4264,7 +4264,7 @@ func TestSyncOperatorGroups(t *testing.T) {
42644264
require.NoError(t, err)
42654265

42664266
// Sync csvs enough to get them back to succeeded state
4267-
for i := 0; i < 8; i++ {
4267+
for i := 0; i < 16; i++ {
42684268
opGroupCSVs, err := op.client.OperatorsV1alpha1().ClusterServiceVersions(operatorNamespace).List(metav1.ListOptions{})
42694269
require.NoError(t, err)
42704270

@@ -4292,7 +4292,7 @@ func TestSyncOperatorGroups(t *testing.T) {
42924292
require.NoError(t, err)
42934293
}
42944294

4295-
if i == 8 {
4295+
if i == 16 {
42964296
err = wait.PollImmediate(1*time.Millisecond, 10*time.Second, func() (bool, error) {
42974297
for namespace, objects := range tt.final.objects {
42984298
if err := RequireObjectsInCache(t, op.lister, namespace, objects, true); err != nil {

pkg/controller/operators/olm/operatorgroup.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install"
2020
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver"
2121
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil"
22+
opregistry "github.com/operator-framework/operator-registry/pkg/registry"
2223
)
2324

2425
const (
@@ -126,7 +127,13 @@ func (a *Operator) syncOperatorGroups(obj interface{}) error {
126127
groupSurface := resolver.NewOperatorGroup(op)
127128
groupProvidedAPIs := groupSurface.ProvidedAPIs()
128129
providedAPIsForCSVs := a.providedAPIsFromCSVs(op, logger)
129-
providedAPIsForGroup := providedAPIsForCSVs.Union(groupProvidedAPIs)
130+
providedAPIsForGroup := make(resolver.APISet)
131+
for api := range providedAPIsForCSVs {
132+
providedAPIsForGroup[api] = struct{}{}
133+
}
134+
for api := range groupProvidedAPIs {
135+
providedAPIsForGroup[api] = struct{}{}
136+
}
130137

131138
csvs, err := a.findCSVsThatProvideAnyOf(providedAPIsForGroup)
132139
if err != nil {
@@ -212,9 +219,9 @@ func (a *Operator) annotateCSVs(group *v1.OperatorGroup, targetNamespaces []stri
212219
return errors.NewAggregate(updateErrs)
213220
}
214221

215-
func (a *Operator) providedAPIsFromCSVs(group *v1.OperatorGroup, logger *logrus.Entry) resolver.APISet {
222+
func (a *Operator) providedAPIsFromCSVs(group *v1.OperatorGroup, logger *logrus.Entry) map[opregistry.APIKey]*v1alpha1.ClusterServiceVersion {
216223
set := a.csvSet(group.Namespace, v1alpha1.CSVPhaseAny)
217-
providedAPIsFromCSVs := make(resolver.APISet)
224+
providedAPIsFromCSVs := make(map[opregistry.APIKey]*v1alpha1.ClusterServiceVersion)
218225
for _, csv := range set {
219226
// Don't union providedAPIsFromCSVs if the CSV is copied (member of another OperatorGroup)
220227
if csv.IsCopied() {
@@ -230,20 +237,40 @@ func (a *Operator) providedAPIsFromCSVs(group *v1.OperatorGroup, logger *logrus.
230237
logger.WithError(err).Warn("could not create OperatorSurface from csv")
231238
continue
232239
}
233-
providedAPIsFromCSVs = providedAPIsFromCSVs.Union(operatorSurface.ProvidedAPIs().StripPlural())
240+
for providedAPI := range operatorSurface.ProvidedAPIs().StripPlural() {
241+
providedAPIsFromCSVs[providedAPI] = csv
242+
}
234243
}
235244
return providedAPIsFromCSVs
236245
}
237246

238-
func (a *Operator) pruneProvidedAPIs(group *v1.OperatorGroup, groupProvidedAPIs, providedAPIsFromCSVs resolver.APISet, logger *logrus.Entry) {
247+
func (a *Operator) pruneProvidedAPIs(group *v1.OperatorGroup, groupProvidedAPIs resolver.APISet, providedAPIsFromCSVs map[opregistry.APIKey]*v1alpha1.ClusterServiceVersion, logger *logrus.Entry) {
239248
// Don't prune providedAPIsFromCSVs if static
240249
if group.Spec.StaticProvidedAPIs {
241250
a.logger.Debug("group has static provided apis. skipping provided api pruning")
242251
return
243252
}
244253

254+
intersection := make(resolver.APISet)
255+
for api := range providedAPIsFromCSVs {
256+
if _, ok := groupProvidedAPIs[api]; ok {
257+
intersection[api] = struct{}{}
258+
} else {
259+
csv := providedAPIsFromCSVs[api]
260+
_, err := a.lister.OperatorsV1alpha1().ClusterServiceVersionLister().ClusterServiceVersions(csv.GetNamespace()).Get(csv.GetName())
261+
if k8serrors.IsNotFound(err) {
262+
continue
263+
}
264+
if csv.DeletionTimestamp == nil && (csv.Status.Phase == v1alpha1.CSVPhaseNone || csv.Status.Phase == v1alpha1.CSVPhasePending) {
265+
logger.Debugf("aborting operator group provided API update due to CSV %v in phase %v", csv.GetName(), csv.Status.Phase)
266+
return
267+
}
268+
}
269+
}
270+
245271
// Prune providedAPIs annotation if the cluster has fewer providedAPIs (handles CSV deletion)
246-
if intersection := groupProvidedAPIs.Intersection(providedAPIsFromCSVs); len(intersection) < len(groupProvidedAPIs) {
272+
//if intersection := groupProvidedAPIs.Intersection(providedAPIsFromCSVs); len(intersection) < len(groupProvidedAPIs) {
273+
if len(intersection) < len(groupProvidedAPIs) {
247274
difference := groupProvidedAPIs.Difference(intersection)
248275
logger := logger.WithFields(logrus.Fields{
249276
"providedAPIsOnCluster": providedAPIsFromCSVs,

test/e2e/csv_e2e_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3212,3 +3212,117 @@ func TestCreateCSVRequirementsEvents(t *testing.T) {
32123212
}
32133213

32143214
// TODO: test behavior when replaces field doesn't point to existing CSV
3215+
3216+
func TestCSVStatusInvalidCSV(t *testing.T) {
3217+
defer cleaner.NotifyTestComplete(t, true)
3218+
3219+
c := newKubeClient(t)
3220+
crc := newCRClient(t)
3221+
3222+
// Create CRD
3223+
crdPlural := genName("ins")
3224+
crdName := crdPlural + ".cluster.com"
3225+
cleanupCRD, err := createCRD(c, apiextensions.CustomResourceDefinition{
3226+
ObjectMeta: metav1.ObjectMeta{
3227+
Name: crdName,
3228+
},
3229+
Spec: apiextensions.CustomResourceDefinitionSpec{
3230+
Group: "cluster.com",
3231+
Version: "v1alpha1",
3232+
Names: apiextensions.CustomResourceDefinitionNames{
3233+
Plural: crdPlural,
3234+
Singular: crdPlural,
3235+
Kind: crdPlural,
3236+
ListKind: "list" + crdPlural,
3237+
},
3238+
Scope: "Namespaced",
3239+
},
3240+
})
3241+
require.NoError(t, err)
3242+
defer cleanupCRD()
3243+
3244+
// create CSV
3245+
strategy := install.StrategyDetailsDeployment{
3246+
DeploymentSpecs: []install.StrategyDeploymentSpec{
3247+
{
3248+
Name: genName("dep-"),
3249+
Spec: newNginxDeployment(genName("nginx-")),
3250+
},
3251+
},
3252+
}
3253+
strategyRaw, err := json.Marshal(strategy)
3254+
require.NoError(t, err)
3255+
3256+
csv := v1alpha1.ClusterServiceVersion{
3257+
TypeMeta: metav1.TypeMeta{
3258+
Kind: v1alpha1.ClusterServiceVersionKind,
3259+
APIVersion: v1alpha1.ClusterServiceVersionAPIVersion,
3260+
},
3261+
ObjectMeta: metav1.ObjectMeta{
3262+
Name: genName("csv"),
3263+
},
3264+
Spec: v1alpha1.ClusterServiceVersionSpec{
3265+
InstallModes: []v1alpha1.InstallMode{
3266+
{
3267+
Type: v1alpha1.InstallModeTypeOwnNamespace,
3268+
Supported: true,
3269+
},
3270+
{
3271+
Type: v1alpha1.InstallModeTypeSingleNamespace,
3272+
Supported: true,
3273+
},
3274+
{
3275+
Type: v1alpha1.InstallModeTypeMultiNamespace,
3276+
Supported: true,
3277+
},
3278+
{
3279+
Type: v1alpha1.InstallModeTypeAllNamespaces,
3280+
Supported: true,
3281+
},
3282+
},
3283+
InstallStrategy: v1alpha1.NamedInstallStrategy{
3284+
StrategyName: install.InstallStrategyNameDeployment,
3285+
StrategySpecRaw: strategyRaw,
3286+
},
3287+
CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{
3288+
Owned: []v1alpha1.CRDDescription{
3289+
{
3290+
Name: crdName,
3291+
Version: "apiextensions.k8s.io/v1alpha1", // purposely invalid, should be just v1alpha1 to match CRD
3292+
Kind: crdPlural,
3293+
DisplayName: crdName,
3294+
Description: "In the cluster2",
3295+
},
3296+
},
3297+
},
3298+
},
3299+
}
3300+
3301+
cleanupCSV, err := createCSV(t, c, crc, csv, testNamespace, true, false)
3302+
require.NoError(t, err)
3303+
defer cleanupCSV()
3304+
3305+
notServedStatus := v1alpha1.RequirementStatus{
3306+
Group: "apiextensions.k8s.io",
3307+
Version: "v1beta1",
3308+
Kind: "CustomResourceDefinition",
3309+
Name: crdName,
3310+
Status: v1alpha1.RequirementStatusReasonNotPresent,
3311+
Message: "CRD version not served",
3312+
}
3313+
csvCheckPhaseAndRequirementStatus := func(csv *v1alpha1.ClusterServiceVersion) bool {
3314+
if csv.Status.Phase == v1alpha1.CSVPhasePending {
3315+
for _, status := range csv.Status.RequirementStatus {
3316+
if status.Message == notServedStatus.Message {
3317+
return true
3318+
}
3319+
}
3320+
}
3321+
return false
3322+
}
3323+
3324+
fetchedCSV, err := fetchCSV(t, crc, csv.Name, testNamespace, csvCheckPhaseAndRequirementStatus)
3325+
require.NoError(t, err)
3326+
3327+
require.Contains(t, fetchedCSV.Status.RequirementStatus, notServedStatus)
3328+
}

0 commit comments

Comments
 (0)