Skip to content

Commit 4a2c5c0

Browse files
committed
feat(sub): Indicate resolution conflicts on Subscription statuses
Resolution considers everything in a given namespace as a unit, so conflicts aren't generated on a per-Subscription basis. However, as the main entry point to resolution, users first look at the Subscription to understand resolution failures. The set of conflicts today is written to Events with type ResolutionFailed. This PR projects resolution errors onto all subscriptions on the namespace for any resolution failure. Signed-off-by: Anik Bhattacharjee <[email protected]>
1 parent ac82fec commit 4a2c5c0

File tree

2 files changed

+116
-0
lines changed

2 files changed

+116
-0
lines changed

pkg/controller/operators/catalog/operator.go

+62
Original file line numberDiff line numberDiff line change
@@ -909,9 +909,22 @@ func (o *Operator) syncResolvingNamespace(obj interface{}) error {
909909
// not-satisfiable error
910910
if _, ok := err.(solver.NotSatisfiable); ok {
911911
logger.WithError(err).Debug("resolution failed")
912+
updateErr := o.setSubsCond(subs, v1alpha1.SubscriptionResolutionFailed, "ConstraintsNotSatisfiable", err.Error(), true)
913+
if updateErr != nil {
914+
logger.WithError(updateErr).Debug("failed to update subs conditions")
915+
}
912916
return nil
913917
}
918+
updateErr := o.setSubsCond(subs, v1alpha1.SubscriptionResolutionFailed, "ErrorPreventedResolution", err.Error(), true)
919+
if updateErr != nil {
920+
logger.WithError(updateErr).Debug("failed to update subs conditions")
921+
}
914922
return err
923+
} else {
924+
updateErr := o.setSubsCond(subs, v1alpha1.SubscriptionResolutionFailed, "", "", false)
925+
if updateErr != nil {
926+
logger.WithError(updateErr).Debug("failed to update subs conditions")
927+
}
915928
}
916929

917930
// create installplan if anything updated
@@ -1189,6 +1202,55 @@ func (o *Operator) createInstallPlan(namespace string, gen int, subs []*v1alpha1
11891202
return reference.GetReference(res)
11901203
}
11911204

1205+
func (o *Operator) setSubsCond(subs []*v1alpha1.Subscription, condType v1alpha1.SubscriptionConditionType, reason, message string, setTrue bool) error {
1206+
var (
1207+
errs []error
1208+
mu sync.Mutex
1209+
wg sync.WaitGroup
1210+
getOpts = metav1.GetOptions{}
1211+
updateOpts = metav1.UpdateOptions{}
1212+
lastUpdated = o.now()
1213+
)
1214+
for _, sub := range subs {
1215+
sub.Status.LastUpdated = lastUpdated
1216+
cond := sub.Status.GetCondition(condType)
1217+
cond.Reason = reason
1218+
cond.Message = message
1219+
if setTrue {
1220+
cond.Status = corev1.ConditionTrue
1221+
} else {
1222+
cond.Status = corev1.ConditionFalse
1223+
}
1224+
sub.Status.SetCondition(cond)
1225+
1226+
wg.Add(1)
1227+
go func(s v1alpha1.Subscription) {
1228+
defer wg.Done()
1229+
1230+
update := func() error {
1231+
// Update the status of the latest revision
1232+
latest, err := o.client.OperatorsV1alpha1().Subscriptions(s.GetNamespace()).Get(context.TODO(), s.GetName(), getOpts)
1233+
if err != nil {
1234+
return err
1235+
}
1236+
1237+
latest.Status = s.Status
1238+
_, err = o.client.OperatorsV1alpha1().Subscriptions(sub.Namespace).UpdateStatus(context.TODO(), latest, updateOpts)
1239+
1240+
return err
1241+
}
1242+
if err := retry.RetryOnConflict(retry.DefaultRetry, update); err != nil {
1243+
mu.Lock()
1244+
defer mu.Unlock()
1245+
errs = append(errs, err)
1246+
}
1247+
}(*sub)
1248+
}
1249+
wg.Wait()
1250+
1251+
return utilerrors.NewAggregate(errs)
1252+
}
1253+
11921254
type UnpackedBundleReference struct {
11931255
Kind string `json:"kind"`
11941256
Name string `json:"name"`

test/e2e/subscription_e2e_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -2105,6 +2105,60 @@ var _ = Describe("Subscription", func() {
21052105
require.NoError(GinkgoT(), err)
21062106
})
21072107

2108+
When("A subscription is created for an operator that requires an API that is not available", func() {
2109+
2110+
var (
2111+
c operatorclient.ClientInterface
2112+
crc versioned.Interface
2113+
teardown func()
2114+
cleanup func()
2115+
packages []registry.PackageManifest
2116+
subName = genName("test-subscription")
2117+
catSrcName = genName("test-catalog")
2118+
)
2119+
2120+
BeforeEach(func() {
2121+
c = newKubeClient()
2122+
crc = newCRClient()
2123+
2124+
packages = []registry.PackageManifest{
2125+
{
2126+
PackageName: "packageA",
2127+
Channels: []registry.PackageChannel{
2128+
{Name: "alpha", CurrentCSVName: "csvA"},
2129+
},
2130+
DefaultChannelName: "alpha",
2131+
},
2132+
}
2133+
crd := newCRD(genName("foo"))
2134+
csv := newCSV("csvA", testNamespace, "", semver.MustParse("1.0.0"), nil, []apiextensions.CustomResourceDefinition{crd}, nil)
2135+
2136+
_, teardown = createInternalCatalogSource(c, ctx.Ctx().OperatorClient(), catSrcName, testNamespace, packages, nil, []operatorsv1alpha1.ClusterServiceVersion{csv})
2137+
2138+
// Ensure that the catalog source is resolved before we create a subscription.
2139+
_, err := fetchCatalogSourceOnStatus(crc, catSrcName, testNamespace, catalogSourceRegistryPodSynced)
2140+
require.NoError(GinkgoT(), err)
2141+
2142+
cleanup = createSubscriptionForCatalog(crc, testNamespace, subName, catSrcName, "packageA", "alpha", "", operatorsv1alpha1.ApprovalAutomatic)
2143+
})
2144+
2145+
AfterEach(func() {
2146+
cleanup()
2147+
teardown()
2148+
})
2149+
2150+
It("the subscription has a condition in it's status that indicates the resolution error", func() {
2151+
Eventually(func() (corev1.ConditionStatus, error) {
2152+
sub, err := crc.OperatorsV1alpha1().Subscriptions(testNamespace).Get(context.Background(), subName, metav1.GetOptions{})
2153+
if err != nil {
2154+
return corev1.ConditionUnknown, err
2155+
}
2156+
return sub.Status.GetCondition(operatorsv1alpha1.SubscriptionResolutionFailed).Status, nil
2157+
}).Should(Equal(corev1.ConditionTrue))
2158+
})
2159+
2160+
})
2161+
21082162
When("an unannotated ClusterServiceVersion exists with an associated Subscription", func() {
21092163
var (
21102164
teardown func()

0 commit comments

Comments
 (0)