diff --git a/pkg/controller/operators/catalog/operator.go b/pkg/controller/operators/catalog/operator.go index ebdd6313a4..23be30f6e6 100644 --- a/pkg/controller/operators/catalog/operator.go +++ b/pkg/controller/operators/catalog/operator.go @@ -332,6 +332,27 @@ func NewOperator(ctx context.Context, kubeconfigPath string, clock utilclock.Clo return nil, err } + // Namespace sync for resolving subscriptions + namespaceInformer := informers.NewSharedInformerFactory(op.opClient.KubernetesInterface(), resyncPeriod()).Core().V1().Namespaces() + op.lister.CoreV1().RegisterNamespaceLister(namespaceInformer.Lister()) + op.nsResolveQueue = workqueue.NewTypedRateLimitingQueueWithConfig[any](workqueue.DefaultTypedControllerRateLimiter[any](), + workqueue.TypedRateLimitingQueueConfig[any]{ + Name: "resolve", + }) + namespaceQueueInformer, err := queueinformer.NewQueueInformer( + ctx, + queueinformer.WithLogger(op.logger), + queueinformer.WithQueue(op.nsResolveQueue), + queueinformer.WithInformer(namespaceInformer.Informer()), + queueinformer.WithSyncer(queueinformer.LegacySyncHandler(op.syncResolvingNamespace).ToSyncer()), + ) + if err != nil { + return nil, err + } + if err := op.RegisterQueueInformer(namespaceQueueInformer); err != nil { + return nil, err + } + // Wire Subscriptions subInformer := crInformerFactory.Operators().V1alpha1().Subscriptions() op.lister.OperatorsV1alpha1().RegisterSubscriptionLister(metav1.NamespaceAll, subInformer.Lister()) @@ -355,7 +376,7 @@ func NewOperator(ctx context.Context, kubeconfigPath string, clock utilclock.Clo subscription.WithCatalogInformer(catsrcInformer.Informer()), subscription.WithInstallPlanInformer(ipInformer.Informer()), subscription.WithSubscriptionQueue(subQueue), - subscription.WithAppendedReconcilers(subscription.ReconcilerFromLegacySyncHandler(op.syncSubscriptions, nil)), + subscription.WithNamespaceResolveQueue(op.nsResolveQueue), subscription.WithRegistryReconcilerFactory(op.reconciler), subscription.WithGlobalCatalogNamespace(op.namespace), subscription.WithSourceProvider(op.resolverSourceProvider), @@ -742,27 +763,6 @@ func NewOperator(ctx context.Context, kubeconfigPath string, clock utilclock.Clo return nil, err } - // Namespace sync for resolving subscriptions - namespaceInformer := informers.NewSharedInformerFactory(op.opClient.KubernetesInterface(), resyncPeriod()).Core().V1().Namespaces() - op.lister.CoreV1().RegisterNamespaceLister(namespaceInformer.Lister()) - op.nsResolveQueue = workqueue.NewTypedRateLimitingQueueWithConfig[any](workqueue.DefaultTypedControllerRateLimiter[any](), - workqueue.TypedRateLimitingQueueConfig[any]{ - Name: "resolve", - }) - namespaceQueueInformer, err := queueinformer.NewQueueInformer( - ctx, - queueinformer.WithLogger(op.logger), - queueinformer.WithQueue(op.nsResolveQueue), - queueinformer.WithInformer(namespaceInformer.Informer()), - queueinformer.WithSyncer(queueinformer.LegacySyncHandler(op.syncResolvingNamespace).ToSyncer()), - ) - if err != nil { - return nil, err - } - if err := op.RegisterQueueInformer(namespaceQueueInformer); err != nil { - return nil, err - } - op.sources.Start(context.Background()) return op, nil @@ -1499,18 +1499,6 @@ func (o *Operator) syncResolvingNamespace(obj interface{}) error { return nil } -func (o *Operator) syncSubscriptions(obj interface{}) error { - sub, ok := obj.(*v1alpha1.Subscription) - if !ok { - o.logger.Infof("wrong type: %#v", obj) - return fmt.Errorf("casting Subscription failed") - } - - o.nsResolveQueue.Add(sub.GetNamespace()) - - return nil -} - // syncOperatorGroups requeues the namespace resolution queue on changes to an operatorgroup // This is because the operatorgroup is now an input to resolution via the global catalog exclusion annotation func (o *Operator) syncOperatorGroups(obj interface{}) error { diff --git a/pkg/controller/operators/catalog/subscription/catalogsource.go b/pkg/controller/operators/catalog/subscription/catalogsource.go new file mode 100644 index 0000000000..a7af31db3f --- /dev/null +++ b/pkg/controller/operators/catalog/subscription/catalogsource.go @@ -0,0 +1,313 @@ +package subscription + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + + "github.com/operator-framework/api/pkg/operators/reference" + "github.com/operator-framework/api/pkg/operators/v1alpha1" + listers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/reconciler" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache" + "github.com/operator-framework/operator-registry/pkg/api" + "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + utilerrors "k8s.io/apimachinery/pkg/util/errors" +) + +// catalogHealthReconciler reconciles catalog health status for subscriptions. +type catalogHealthReconciler struct { + now func() *metav1.Time + catalogLister listers.CatalogSourceLister + registryReconcilerFactory reconciler.RegistryReconcilerFactory + globalCatalogNamespace string + sourceProvider cache.SourceProvider +} + +// Reconcile reconciles subscription catalog health conditions. +func (c *catalogHealthReconciler) Reconcile(ctx context.Context, sub *v1alpha1.Subscription) (*v1alpha1.Subscription, error) { + out := sub.DeepCopy() + now := c.now() + + // Gather catalog health and transition state + ns := sub.GetNamespace() + var catalogHealth []v1alpha1.SubscriptionCatalogHealth + catalogHealth, err := c.catalogHealth(ns) + if err != nil { + return nil, err + } + + out, err = c.updateHealth(out, now, catalogHealth...) + if err != nil { + return nil, err + } + + return c.updateDeprecatedStatus(ctx, out, now) +} + +// updateDeprecatedStatus adds deprecation status conditions to the subscription when present in the cache entry then +// returns a bool value of true if any changes to the existing subscription have occurred. +func (c *catalogHealthReconciler) updateDeprecatedStatus(ctx context.Context, sub *v1alpha1.Subscription, now *metav1.Time) (*v1alpha1.Subscription, error) { + if c.sourceProvider == nil { + return sub, nil + } + + source, ok := c.sourceProvider.Sources(sub.Spec.CatalogSourceNamespace)[cache.SourceKey{ + Name: sub.Spec.CatalogSource, + Namespace: sub.Spec.CatalogSourceNamespace, + }] + + if !ok { + return sub, nil + } + snapshot, err := source.Snapshot(ctx) + if err != nil { + return sub, nil + } + if len(snapshot.Entries) == 0 { + return sub, nil + } + + var rollupMessages []string + var deprecations *cache.Deprecations + + found := false + for _, entry := range snapshot.Entries { + // Find the cache entry that matches this subscription + if entry.SourceInfo == nil || entry.Package() != sub.Spec.Package { + continue + } + if sub.Spec.Channel != "" && entry.Channel() != sub.Spec.Channel { + continue + } + if sub.Status.InstalledCSV != entry.Name { + continue + } + deprecations = entry.SourceInfo.Deprecations + found = true + break + } + if !found { + // No matching entry found + return sub, nil + } + out := sub.DeepCopy() + conditionTypes := []v1alpha1.SubscriptionConditionType{ + v1alpha1.SubscriptionPackageDeprecated, + v1alpha1.SubscriptionChannelDeprecated, + v1alpha1.SubscriptionBundleDeprecated, + } + for _, conditionType := range conditionTypes { + oldCondition := sub.Status.GetCondition(conditionType) + var deprecation *api.Deprecation + if deprecations != nil { + switch conditionType { + case v1alpha1.SubscriptionPackageDeprecated: + deprecation = deprecations.Package + case v1alpha1.SubscriptionChannelDeprecated: + deprecation = deprecations.Channel + case v1alpha1.SubscriptionBundleDeprecated: + deprecation = deprecations.Bundle + } + } + if deprecation != nil { + if conditionType == v1alpha1.SubscriptionChannelDeprecated && sub.Spec.Channel == "" { + // Special case: If optional field sub.Spec.Channel is unset do not apply a channel + // deprecation message and remove them if any exist. + out.Status.RemoveConditions(conditionType) + continue + } + newCondition := v1alpha1.SubscriptionCondition{ + Type: conditionType, + Message: deprecation.Message, + Status: corev1.ConditionTrue, + LastTransitionTime: c.now(), + } + rollupMessages = append(rollupMessages, deprecation.Message) + if oldCondition.Message != newCondition.Message { + // oldCondition's message was empty or has changed; add or update the condition + out.Status.SetCondition(newCondition) + } + } else if oldCondition.Status == corev1.ConditionTrue { + // No longer deprecated at this level; remove the condition + out.Status.RemoveConditions(conditionType) + } + } + + if len(rollupMessages) > 0 { + rollupCondition := v1alpha1.SubscriptionCondition{ + Type: v1alpha1.SubscriptionDeprecated, + Message: strings.Join(rollupMessages, "; "), + Status: corev1.ConditionTrue, + LastTransitionTime: now, + } + out.Status.SetCondition(rollupCondition) + } else { + // No rollup message means no deprecation conditions were set; remove the rollup if it exists + out.Status.RemoveConditions(v1alpha1.SubscriptionDeprecated) + } + + return out, nil +} + +// catalogHealth gets the health of catalogs that can affect Susbcriptions in the given namespace. +// This means all catalogs in the given namespace, as well as any catalogs in the operator's global catalog namespace. +func (c *catalogHealthReconciler) catalogHealth(namespace string) ([]v1alpha1.SubscriptionCatalogHealth, error) { + catalogs, err := c.catalogLister.CatalogSources(namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + if namespace != c.globalCatalogNamespace { + globals, err := c.catalogLister.CatalogSources(c.globalCatalogNamespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + catalogs = append(catalogs, globals...) + } + + // Sort to ensure ordering + sort.Slice(catalogs, func(i, j int) bool { + return catalogs[i].GetNamespace()+catalogs[i].GetName() < catalogs[j].GetNamespace()+catalogs[j].GetName() + }) + + catalogHealth := make([]v1alpha1.SubscriptionCatalogHealth, len(catalogs)) + now := c.now() + var errs []error + for i, catalog := range catalogs { + h, err := c.health(now, catalog) + if err != nil { + errs = append(errs, err) + continue + } + + // Prevent assignment when any error has been encountered since the results will be discarded + if errs == nil { + catalogHealth[i] = *h + } + } + + if errs != nil || len(catalogHealth) == 0 { + // Assign meaningful zero value + catalogHealth = nil + } + + return catalogHealth, utilerrors.NewAggregate(errs) +} + +// health returns a SusbcriptionCatalogHealth for the given catalog with the given now. +func (c *catalogHealthReconciler) health(now *metav1.Time, catalog *v1alpha1.CatalogSource) (*v1alpha1.SubscriptionCatalogHealth, error) { + healthy, err := c.healthy(catalog) + if err != nil { + return nil, err + } + + ref, err := reference.GetReference(catalog) + if err != nil { + return nil, err + } + if ref == nil { + return nil, errors.New("nil reference") + } + + h := &v1alpha1.SubscriptionCatalogHealth{ + CatalogSourceRef: ref, + // TODO: Should LastUpdated be set here, or at time of subscription update? + LastUpdated: now, + Healthy: healthy, + } + + return h, nil +} + +// healthy returns true if the given catalog is healthy, false otherwise, and any error encountered +// while checking the catalog's registry server. +func (c *catalogHealthReconciler) healthy(catalog *v1alpha1.CatalogSource) (bool, error) { + if catalog.Status.Reason == v1alpha1.CatalogSourceSpecInvalidError { + // The catalog's spec is bad, mark unhealthy + return false, nil + } + + // Check connection health + rec := c.registryReconcilerFactory.ReconcilerForSource(catalog) + if rec == nil { + return false, fmt.Errorf("could not get reconciler for catalog: %#v", catalog) + } + + return rec.CheckRegistryServer(logrus.NewEntry(logrus.New()), catalog) +} + +func (c *catalogHealthReconciler) updateHealth(sub *v1alpha1.Subscription, now *metav1.Time, catalogHealth ...v1alpha1.SubscriptionCatalogHealth) (*v1alpha1.Subscription, error) { + out := sub.DeepCopy() + + healthSet := make(map[types.UID]v1alpha1.SubscriptionCatalogHealth, len(catalogHealth)) + healthy := true + missingTargeted := true + + cond := sub.Status.GetCondition(v1alpha1.SubscriptionCatalogSourcesUnhealthy) + for _, h := range catalogHealth { + ref := h.CatalogSourceRef + healthSet[ref.UID] = h + healthy = healthy && h.Healthy + + if ref.Namespace == sub.Spec.CatalogSourceNamespace && ref.Name == sub.Spec.CatalogSource { + missingTargeted = false + if !h.Healthy { + cond.Message = fmt.Sprintf("targeted catalogsource %s/%s unhealthy", ref.Namespace, ref.Name) + } + } + } + + switch { + case missingTargeted: + cond.Message = fmt.Sprintf("targeted catalogsource %s/%s missing", sub.Spec.CatalogSourceNamespace, sub.Spec.CatalogSource) + fallthrough + case !healthy: + cond.Status = corev1.ConditionTrue + cond.Reason = v1alpha1.UnhealthyCatalogSourceFound + default: + cond.Status = corev1.ConditionFalse + cond.Reason = v1alpha1.AllCatalogSourcesHealthy + cond.Message = "all available catalogsources are healthy" + } + + // Check for changes in CatalogHealth + updated := false + switch numNew, numOld := len(healthSet), len(sub.Status.CatalogHealth); { + case numNew > numOld: + cond.Reason = v1alpha1.CatalogSourcesAdded + case numNew < numOld: + cond.Reason = v1alpha1.CatalogSourcesDeleted + case numNew == 0 && numNew == numOld: + cond.Reason = v1alpha1.NoCatalogSourcesFound + cond.Message = "dependency resolution requires at least one catalogsource" + case numNew == numOld: + // Check against existing subscription + for _, oldHealth := range sub.Status.CatalogHealth { + uid := oldHealth.CatalogSourceRef.UID + if newHealth, ok := healthSet[uid]; !ok || !newHealth.Equals(oldHealth) { + cond.Reason = v1alpha1.CatalogSourcesUpdated + updated = true + break + } + } + } + + if !updated && cond.Equals(sub.Status.GetCondition(v1alpha1.SubscriptionCatalogSourcesUnhealthy)) { + return out, nil + } + + cond.LastTransitionTime = now + out.Status.SetCondition(cond) + out.Status.LastUpdated = *now + out.Status.CatalogHealth = catalogHealth + + return out, nil +} diff --git a/pkg/controller/operators/catalog/subscription/config.go b/pkg/controller/operators/catalog/subscription/config.go index c4c1877b64..e7a64ed5f6 100644 --- a/pkg/controller/operators/catalog/subscription/config.go +++ b/pkg/controller/operators/catalog/subscription/config.go @@ -11,7 +11,6 @@ import ( "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/reconciler" resolverCache "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache" - "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubestate" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" ) @@ -24,7 +23,8 @@ type syncerConfig struct { catalogInformer cache.SharedIndexInformer installPlanInformer cache.SharedIndexInformer subscriptionQueue workqueue.TypedRateLimitingInterface[any] - reconcilers kubestate.ReconcilerChain + nsResolveQueue workqueue.TypedRateLimitingInterface[any] + reconcilers ReconcilerChain registryReconcilerFactory reconciler.RegistryReconcilerFactory globalCatalogNamespace string sourceProvider resolverCache.SourceProvider @@ -37,7 +37,7 @@ func defaultSyncerConfig() *syncerConfig { return &syncerConfig{ logger: logrus.New(), clock: utilclock.RealClock{}, - reconcilers: kubestate.ReconcilerChain{}, + reconcilers: ReconcilerChain{}, } } @@ -54,6 +54,12 @@ func WithLogger(logger *logrus.Logger) SyncerOption { } } +func WithNamespaceResolveQueue(queue workqueue.TypedRateLimitingInterface[any]) SyncerOption { + return func(config *syncerConfig) { + config.nsResolveQueue = queue + } +} + // WithClock sets a syncer's clock. func WithClock(clock utilclock.Clock) SyncerOption { return func(config *syncerConfig) { @@ -105,7 +111,7 @@ func WithSubscriptionQueue(subscriptionQueue workqueue.TypedRateLimitingInterfac // WithAppendedReconcilers adds the given reconcilers to the end of a syncer's reconciler chain, to be // invoked after its default reconcilers have been called. -func WithAppendedReconcilers(reconcilers ...kubestate.Reconciler) SyncerOption { +func WithAppendedReconcilers(reconcilers ...Reconciler) SyncerOption { return func(config *syncerConfig) { // Add non-nil reconcilers to the chain for _, rec := range reconcilers { @@ -158,8 +164,8 @@ func (s *syncerConfig) validate() (err error) { err = newInvalidConfigError("nil installplan informer") case s.subscriptionQueue == nil: err = newInvalidConfigError("nil subscription queue") - case len(s.reconcilers) == 0: - err = newInvalidConfigError("no reconcilers") + case s.nsResolveQueue == nil: + err = newInvalidConfigError("no namespace resolve queue") case s.registryReconcilerFactory == nil: err = newInvalidConfigError("nil reconciler factory") case s.globalCatalogNamespace == metav1.NamespaceAll: diff --git a/pkg/controller/operators/catalog/subscription/installplan.go b/pkg/controller/operators/catalog/subscription/installplan.go new file mode 100644 index 0000000000..c56dd2b3f0 --- /dev/null +++ b/pkg/controller/operators/catalog/subscription/installplan.go @@ -0,0 +1,129 @@ +package subscription + +import ( + "bytes" + "context" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + listers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// installPlanReconciler reconciles InstallPlan status for Subscriptions. +type installPlanReconciler struct { + now func() *metav1.Time + installPlanLister listers.InstallPlanLister +} + +func (i *installPlanReconciler) Reconcile(_ context.Context, sub *v1alpha1.Subscription) (*v1alpha1.Subscription, error) { + out := sub.DeepCopy() + + // Check the stated InstallPlan - bail if not set + if sub.Status.InstallPlanRef == nil { + return out, nil + } + ref := sub.Status.InstallPlanRef // Should never be nil in this typestate + plan, err := i.installPlanLister.InstallPlans(ref.Namespace).Get(ref.Name) + now := i.now() + if err != nil { + if apierrors.IsNotFound(err) { + // Remove pending and failed conditions + out.Status.RemoveConditions(v1alpha1.SubscriptionInstallPlanPending, v1alpha1.SubscriptionInstallPlanFailed) + + // If the installplan is missing when subscription is in pending upgrade, + // clear the installplan ref so the resolution can happen again + if sub.Status.State == v1alpha1.SubscriptionStateUpgradePending { + out.Status.InstallPlanRef = nil + out.Status.Install = nil + out.Status.CurrentCSV = "" + out.Status.State = v1alpha1.SubscriptionStateNone + out.Status.LastUpdated = *now + } else { + // Set missing condition to true + origCond := sub.Status.GetCondition(v1alpha1.SubscriptionInstallPlanMissing) + cond := out.Status.GetCondition(v1alpha1.SubscriptionInstallPlanMissing) + cond.Status = corev1.ConditionTrue + cond.Reason = v1alpha1.ReferencedInstallPlanNotFound + cond.LastTransitionTime = i.now() + if cond.Reason != origCond.Reason || cond.Message != origCond.Message || cond.Status != origCond.Status { + out.Status.SetCondition(cond) + out.Status.LastUpdated = *now + } + } + return out, nil + } + return nil, err + } + + // Remove missing, pending, and failed conditions + out.Status.RemoveConditions(v1alpha1.SubscriptionInstallPlanMissing, v1alpha1.SubscriptionInstallPlanPending, v1alpha1.SubscriptionInstallPlanFailed) + + // Build and set the InstallPlan condition, if any + cond := v1alpha1.SubscriptionCondition{ + Status: corev1.ConditionUnknown, + LastTransitionTime: i.now(), + } + + // TODO: Use InstallPlan conditions instead of phases + // Get the status of the InstallPlan and create the appropriate condition and state + switch phase := plan.Status.Phase; phase { + case v1alpha1.InstallPlanPhaseNone: + // Set reason and let the following case fill out the pending condition + cond.Reason = v1alpha1.InstallPlanNotYetReconciled + fallthrough + case v1alpha1.InstallPlanPhasePlanning, v1alpha1.InstallPlanPhaseInstalling, v1alpha1.InstallPlanPhaseRequiresApproval: + if cond.Reason == "" { + cond.Reason = string(phase) + } + cond.Message = extractMessage(&plan.Status) + cond.Type = v1alpha1.SubscriptionInstallPlanPending + cond.Status = corev1.ConditionTrue + oldCond := sub.Status.GetCondition(v1alpha1.SubscriptionInstallPlanPending) + if !cond.Equals(oldCond) { + out.Status.SetCondition(cond) + out.Status.LastUpdated = *now + } else { + out.Status.SetCondition(oldCond) + } + case v1alpha1.InstallPlanPhaseFailed: + // Attempt to project reason from failed InstallPlan condition + if installedCond := plan.Status.GetCondition(v1alpha1.InstallPlanInstalled); installedCond.Status == corev1.ConditionFalse { + cond.Reason = string(installedCond.Reason) + } else { + cond.Reason = v1alpha1.InstallPlanFailed + } + + cond.Type = v1alpha1.SubscriptionInstallPlanFailed + cond.Message = extractMessage(&plan.Status) + cond.Status = corev1.ConditionTrue + oldCond := sub.Status.GetCondition(v1alpha1.SubscriptionInstallPlanFailed) + if !cond.Equals(oldCond) { + out.Status.SetCondition(cond) + out.Status.LastUpdated = *now + } else { + out.Status.SetCondition(oldCond) + } + } + return out, nil +} + +func extractMessage(status *v1alpha1.InstallPlanStatus) string { + if cond := status.GetCondition(v1alpha1.InstallPlanInstalled); cond.Status != corev1.ConditionUnknown && cond.Message != "" { + return cond.Message + } + + var b bytes.Buffer + for _, lookup := range status.BundleLookups { + if cond := lookup.GetCondition(v1alpha1.BundleLookupPending); cond.Status != corev1.ConditionUnknown && cond.Message != "" { + if b.Len() != 0 { + b.WriteString(" ") + } + b.WriteString(cond.Message) + b.WriteString(".") + } + } + + return b.String() +} diff --git a/pkg/controller/operators/catalog/subscription/reconciler.go b/pkg/controller/operators/catalog/subscription/reconciler.go index 2d3ccf9724..f4534e48f6 100644 --- a/pkg/controller/operators/catalog/subscription/reconciler.go +++ b/pkg/controller/operators/catalog/subscription/reconciler.go @@ -2,396 +2,28 @@ package subscription import ( "context" - "errors" - "fmt" - "sort" - "strings" - "github.com/sirupsen/logrus" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - utilerrors "k8s.io/apimachinery/pkg/util/errors" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - - "github.com/operator-framework/api/pkg/operators/reference" "github.com/operator-framework/api/pkg/operators/v1alpha1" - "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" - listers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1" - "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/reconciler" - "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache" - "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubestate" - "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/queueinformer" - "github.com/operator-framework/operator-registry/pkg/api" ) -// ReconcilerFromLegacySyncHandler returns a reconciler that invokes the given legacy sync handler and on delete funcs. -// Since the reconciler does not return an updated kubestate, it MUST be the last reconciler in a given chain. -func ReconcilerFromLegacySyncHandler(sync queueinformer.LegacySyncHandler, onDelete func(obj interface{})) kubestate.Reconciler { - var rec kubestate.ReconcilerFunc = func(ctx context.Context, in kubestate.State) (out kubestate.State, err error) { - out = in - switch s := in.(type) { - case SubscriptionExistsState: - if sync != nil { - err = sync(s.Subscription()) - } - case SubscriptionDeletedState: - if onDelete != nil { - onDelete(s.Subscription()) - } - case SubscriptionState: - if sync != nil { - err = sync(s.Subscription()) - } - default: - utilruntime.HandleError(fmt.Errorf("unexpected subscription state in legacy reconciler: %T", s)) - } - - return - } - - return rec -} - -// catalogHealthReconciler reconciles catalog health status for subscriptions. -type catalogHealthReconciler struct { - now func() *metav1.Time - client versioned.Interface - catalogLister listers.CatalogSourceLister - registryReconcilerFactory reconciler.RegistryReconcilerFactory - globalCatalogNamespace string - sourceProvider cache.SourceProvider -} - -// Reconcile reconciles subscription catalog health conditions. -func (c *catalogHealthReconciler) Reconcile(ctx context.Context, in kubestate.State) (out kubestate.State, err error) { - next := in - var prev kubestate.State - - // loop until this state can no longer transition - for err == nil && next != nil && next != prev && !next.Terminal() { - select { - case <-ctx.Done(): - err = errors.New("subscription catalog health reconciliation context closed") - default: - prev = next - - switch s := next.(type) { - case CatalogHealthKnownState: - // Target state already known, no work to do - next = s - case CatalogHealthState: - // Gather catalog health and transition state - ns := s.Subscription().GetNamespace() - var catalogHealth []v1alpha1.SubscriptionCatalogHealth - if catalogHealth, err = c.catalogHealth(ns); err != nil { - break - } - - var healthUpdated, deprecationUpdated bool - next, healthUpdated = s.UpdateHealth(c.now(), catalogHealth...) - if healthUpdated { - if _, err := c.client.OperatorsV1alpha1().Subscriptions(ns).UpdateStatus(ctx, s.Subscription(), metav1.UpdateOptions{}); err != nil { - return nil, err - } - } - deprecationUpdated, err = c.updateDeprecatedStatus(ctx, s.Subscription()) - if err != nil { - return next, err - } - if deprecationUpdated { - _, err = c.client.OperatorsV1alpha1().Subscriptions(ns).UpdateStatus(ctx, s.Subscription(), metav1.UpdateOptions{}) - } - case SubscriptionExistsState: - if s == nil { - err = errors.New("nil state") - break - } - if s.Subscription() == nil { - err = errors.New("nil subscription in state") - break - } - - // Set up fresh state - next = NewCatalogHealthState(s) - default: - // Ignore all other typestates - next = s - } - } - } - - out = next - - return -} - -// updateDeprecatedStatus adds deprecation status conditions to the subscription when present in the cache entry then -// returns a bool value of true if any changes to the existing subscription have occurred. -func (c *catalogHealthReconciler) updateDeprecatedStatus(ctx context.Context, sub *v1alpha1.Subscription) (bool, error) { - if c.sourceProvider == nil { - return false, nil - } - source, ok := c.sourceProvider.Sources(sub.Spec.CatalogSourceNamespace)[cache.SourceKey{ - Name: sub.Spec.CatalogSource, - Namespace: sub.Spec.CatalogSourceNamespace, - }] - if !ok { - return false, nil - } - snapshot, err := source.Snapshot(ctx) - if err != nil { - return false, err - } - if len(snapshot.Entries) == 0 { - return false, nil - } - - changed := false - rollupMessages := []string{} - var deprecations *cache.Deprecations - - found := false - for _, entry := range snapshot.Entries { - // Find the cache entry that matches this subscription - if entry.SourceInfo == nil || entry.Package() != sub.Spec.Package { - continue - } - if sub.Spec.Channel != "" && entry.Channel() != sub.Spec.Channel { - continue - } - if sub.Status.InstalledCSV != entry.Name { - continue - } - deprecations = entry.SourceInfo.Deprecations - found = true - break - } - if !found { - // No matching entry found - return false, nil - } - conditionTypes := []v1alpha1.SubscriptionConditionType{ - v1alpha1.SubscriptionPackageDeprecated, - v1alpha1.SubscriptionChannelDeprecated, - v1alpha1.SubscriptionBundleDeprecated, - } - for _, conditionType := range conditionTypes { - oldCondition := sub.Status.GetCondition(conditionType) - var deprecation *api.Deprecation - if deprecations != nil { - switch conditionType { - case v1alpha1.SubscriptionPackageDeprecated: - deprecation = deprecations.Package - case v1alpha1.SubscriptionChannelDeprecated: - deprecation = deprecations.Channel - case v1alpha1.SubscriptionBundleDeprecated: - deprecation = deprecations.Bundle - } - } - if deprecation != nil { - if conditionType == v1alpha1.SubscriptionChannelDeprecated && sub.Spec.Channel == "" { - // Special case: If optional field sub.Spec.Channel is unset do not apply a channel - // deprecation message and remove them if any exist. - sub.Status.RemoveConditions(conditionType) - if oldCondition.Status == corev1.ConditionTrue { - changed = true - } - continue - } - newCondition := v1alpha1.SubscriptionCondition{ - Type: conditionType, - Message: deprecation.Message, - Status: corev1.ConditionTrue, - LastTransitionTime: c.now(), - } - rollupMessages = append(rollupMessages, deprecation.Message) - if oldCondition.Message != newCondition.Message { - // oldCondition's message was empty or has changed; add or update the condition - sub.Status.SetCondition(newCondition) - changed = true - } - } else if oldCondition.Status == corev1.ConditionTrue { - // No longer deprecated at this level; remove the condition - sub.Status.RemoveConditions(conditionType) - changed = true - } - } - - if !changed { - // No need to update rollup condition if no other conditions have changed - return false, nil - } - if len(rollupMessages) > 0 { - rollupCondition := v1alpha1.SubscriptionCondition{ - Type: v1alpha1.SubscriptionDeprecated, - Message: strings.Join(rollupMessages, "; "), - Status: corev1.ConditionTrue, - LastTransitionTime: c.now(), - } - sub.Status.SetCondition(rollupCondition) - } else { - // No rollup message means no deprecation conditions were set; remove the rollup if it exists - sub.Status.RemoveConditions(v1alpha1.SubscriptionDeprecated) - } - - return true, nil -} - -// catalogHealth gets the health of catalogs that can affect Susbcriptions in the given namespace. -// This means all catalogs in the given namespace, as well as any catalogs in the operator's global catalog namespace. -func (c *catalogHealthReconciler) catalogHealth(namespace string) ([]v1alpha1.SubscriptionCatalogHealth, error) { - catalogs, err := c.catalogLister.CatalogSources(namespace).List(labels.Everything()) - if err != nil { - return nil, err - } - - if namespace != c.globalCatalogNamespace { - globals, err := c.catalogLister.CatalogSources(c.globalCatalogNamespace).List(labels.Everything()) - if err != nil { - return nil, err - } - - catalogs = append(catalogs, globals...) - } - - // Sort to ensure ordering - sort.Slice(catalogs, func(i, j int) bool { - return catalogs[i].GetNamespace()+catalogs[i].GetName() < catalogs[j].GetNamespace()+catalogs[j].GetName() - }) - - catalogHealth := make([]v1alpha1.SubscriptionCatalogHealth, len(catalogs)) - now := c.now() - var errs []error - for i, catalog := range catalogs { - h, err := c.health(now, catalog) - if err != nil { - errs = append(errs, err) - continue - } - - // Prevent assignment when any error has been encountered since the results will be discarded - if errs == nil { - catalogHealth[i] = *h - } - } - - if errs != nil || len(catalogHealth) == 0 { - // Assign meaningful zero value - catalogHealth = nil - } - - return catalogHealth, utilerrors.NewAggregate(errs) -} - -// health returns a SusbcriptionCatalogHealth for the given catalog with the given now. -func (c *catalogHealthReconciler) health(now *metav1.Time, catalog *v1alpha1.CatalogSource) (*v1alpha1.SubscriptionCatalogHealth, error) { - healthy, err := c.healthy(catalog) - if err != nil { - return nil, err - } - - ref, err := reference.GetReference(catalog) - if err != nil { - return nil, err - } - if ref == nil { - return nil, errors.New("nil reference") - } - - h := &v1alpha1.SubscriptionCatalogHealth{ - CatalogSourceRef: ref, - // TODO: Should LastUpdated be set here, or at time of subscription update? - LastUpdated: now, - Healthy: healthy, - } - - return h, nil +type Reconciler interface { + Reconcile(ctx context.Context, sub *v1alpha1.Subscription) (*v1alpha1.Subscription, error) } -// healthy returns true if the given catalog is healthy, false otherwise, and any error encountered -// while checking the catalog's registry server. -func (c *catalogHealthReconciler) healthy(catalog *v1alpha1.CatalogSource) (bool, error) { - if catalog.Status.Reason == v1alpha1.CatalogSourceSpecInvalidError { - // The catalog's spec is bad, mark unhealthy - return false, nil - } - - // Check connection health - rec := c.registryReconcilerFactory.ReconcilerForSource(catalog) - if rec == nil { - return false, fmt.Errorf("could not get reconciler for catalog: %#v", catalog) - } +type ReconcilerFunc func(ctx context.Context, sub *v1alpha1.Subscription) (*v1alpha1.Subscription, error) - return rec.CheckRegistryServer(logrus.NewEntry(logrus.New()), catalog) +func (r ReconcilerFunc) Reconcile(ctx context.Context, sub *v1alpha1.Subscription) (*v1alpha1.Subscription, error) { + return r(ctx, sub) } -// installPlanReconciler reconciles InstallPlan status for Subscriptions. -type installPlanReconciler struct { - now func() *metav1.Time - client versioned.Interface - installPlanLister listers.InstallPlanLister -} - -// Reconcile reconciles Subscription InstallPlan conditions. -func (i *installPlanReconciler) Reconcile(ctx context.Context, in kubestate.State) (out kubestate.State, err error) { - next := in - var prev kubestate.State - - // loop until this state can no longer transition - for err == nil && next != nil && prev != next && !next.Terminal() { - select { - case <-ctx.Done(): - err = errors.New("subscription installplan reconciliation context closed") - default: - prev = next - - switch s := next.(type) { - case NoInstallPlanReferencedState: - // No InstallPlan was referenced, no work to do - next = s - case InstallPlanKnownState: - // Target state already known, no work to do - next = s - case InstallPlanReferencedState: - // Check the stated InstallPlan - ref := s.Subscription().Status.InstallPlanRef // Should never be nil in this typestate - subClient := i.client.OperatorsV1alpha1().Subscriptions(ref.Namespace) - - var plan *v1alpha1.InstallPlan - if plan, err = i.installPlanLister.InstallPlans(ref.Namespace).Get(ref.Name); err != nil { - if apierrors.IsNotFound(err) { - next, err = s.InstallPlanNotFound(i.now(), subClient) - } +type ReconcilerChain []Reconciler - break - } - - next, err = s.CheckInstallPlanStatus(i.now(), subClient, &plan.Status) - case InstallPlanState: - next = s.CheckReference() - case SubscriptionExistsState: - if s == nil { - err = errors.New("nil state") - break - } - if s.Subscription() == nil { - err = errors.New("nil subscription in state") - break - } - - // Set up fresh state - next = newInstallPlanState(s) - default: - // Ignore all other typestates - utilruntime.HandleError(fmt.Errorf("unexpected subscription state in installplan reconciler %T", next)) - next = s - } +func (r ReconcilerChain) Reconcile(ctx context.Context, sub *v1alpha1.Subscription) (*v1alpha1.Subscription, error) { + var err error + for _, rec := range r { + if sub, err = rec.Reconcile(ctx, sub); err != nil || sub == nil { + break } } - - out = next - - return + return sub, err } diff --git a/pkg/controller/operators/catalog/subscription/reconciler_test.go b/pkg/controller/operators/catalog/subscription/reconciler_test.go index fe6552b1b2..42cb850a0e 100644 --- a/pkg/controller/operators/catalog/subscription/reconciler_test.go +++ b/pkg/controller/operators/catalog/subscription/reconciler_test.go @@ -20,7 +20,6 @@ import ( registryreconciler "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/reconciler" olmfakes "github.com/operator-framework/operator-lifecycle-manager/pkg/fakes" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/clientfake" - "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubestate" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" ) @@ -34,11 +33,11 @@ func TestCatalogHealthReconcile(t *testing.T) { config *fakeReconcilerConfig } type args struct { - in kubestate.State + in *v1alpha1.Subscription } type want struct { err error - out kubestate.State + out *v1alpha1.Subscription } tests := []struct { @@ -67,39 +66,35 @@ func TestCatalogHealthReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState( - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "default", - }, - Spec: &v1alpha1.SubscriptionSpec{}, - Status: v1alpha1.SubscriptionStatus{}, + in: &v1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sub", + Namespace: "default", }, - ), + Spec: &v1alpha1.SubscriptionSpec{}, + Status: v1alpha1.SubscriptionStatus{}, + }, }, want: want{ - out: newCatalogUnhealthyState( - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "default", - }, - Spec: &v1alpha1.SubscriptionSpec{}, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - { - Type: v1alpha1.SubscriptionCatalogSourcesUnhealthy, - Status: corev1.ConditionTrue, - Reason: v1alpha1.NoCatalogSourcesFound, - Message: "dependency resolution requires at least one catalogsource", - LastTransitionTime: &now, - }, + out: &v1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sub", + Namespace: "default", + }, + Spec: &v1alpha1.SubscriptionSpec{}, + Status: v1alpha1.SubscriptionStatus{ + Conditions: []v1alpha1.SubscriptionCondition{ + { + Type: v1alpha1.SubscriptionCatalogSourcesUnhealthy, + Status: corev1.ConditionTrue, + Reason: v1alpha1.NoCatalogSourcesFound, + Message: "dependency resolution requires at least one catalogsource", + LastTransitionTime: &now, }, - LastUpdated: now, }, + LastUpdated: now, }, - ), + }, }, }, { @@ -137,21 +132,19 @@ func TestCatalogHealthReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState( - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, + in: &v1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sub", + Namespace: "ns", }, - ), + Spec: &v1alpha1.SubscriptionSpec{ + CatalogSourceNamespace: "ns", + CatalogSource: "cs-0", + }, + }, }, want: want{ - out: newCatalogUnhealthyState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -169,7 +162,7 @@ func TestCatalogHealthReconcile(t *testing.T) { }, LastUpdated: now, }, - }), + }, }, }, { @@ -208,7 +201,7 @@ func TestCatalogHealthReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -227,10 +220,10 @@ func TestCatalogHealthReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newCatalogUnhealthyState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -249,7 +242,7 @@ func TestCatalogHealthReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, }, { @@ -288,7 +281,7 @@ func TestCatalogHealthReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -307,10 +300,10 @@ func TestCatalogHealthReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newCatalogHealthyState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -329,7 +322,7 @@ func TestCatalogHealthReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, }, { @@ -369,7 +362,7 @@ func TestCatalogHealthReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -388,10 +381,10 @@ func TestCatalogHealthReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newCatalogHealthyState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -411,7 +404,7 @@ func TestCatalogHealthReconcile(t *testing.T) { }, LastUpdated: now, }, - }), + }, }, }, { @@ -458,30 +451,28 @@ func TestCatalogHealthReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState( - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", + in: &v1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sub", + Namespace: "ns", + }, + Spec: &v1alpha1.SubscriptionSpec{ + CatalogSourceNamespace: "ns", + CatalogSource: "cs-0", + }, + Status: v1alpha1.SubscriptionStatus{ + CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ + catalogHealth("ns", "cs-0", &earlier, true), }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier), - }, - LastUpdated: earlier, + Conditions: []v1alpha1.SubscriptionCondition{ + catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier), }, + LastUpdated: earlier, }, - ), + }, }, want: want{ - out: newCatalogUnhealthyState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -499,7 +490,7 @@ func TestCatalogHealthReconcile(t *testing.T) { }, LastUpdated: now, }, - }), + }, }, }, } @@ -513,12 +504,6 @@ func TestCatalogHealthReconcile(t *testing.T) { out, err := rec.Reconcile(ctx, tt.args.in) require.Equal(t, tt.want.err, err) require.Equal(t, tt.want.out, out) - - // Ensure the client's view of the subscription matches the typestate's - sub := out.(SubscriptionState).Subscription() - clusterSub, err := rec.client.OperatorsV1alpha1().Subscriptions(sub.GetNamespace()).Get(context.TODO(), sub.GetName(), metav1.GetOptions{}) - require.NoError(t, err) - require.Equal(t, sub, clusterSub) }) } } @@ -533,11 +518,11 @@ func TestInstallPlanReconcile(t *testing.T) { config *fakeReconcilerConfig } type args struct { - in kubestate.State + in *v1alpha1.Subscription } type want struct { err error - out kubestate.State + out *v1alpha1.Subscription } tests := []struct { @@ -564,20 +549,20 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", }, - }), + }, }, want: want{ - out: newNoInstallPlanReferencedState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", }, - }), + }, }, }, { @@ -608,7 +593,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newCatalogHealthyState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -623,27 +608,23 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: &noInstallPlanReferencedState{ - InstallPlanState: &installPlanState{ - SubscriptionExistsState: newCatalogHealthyState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, true), - catalogHealth("ns", "cs-1", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier), - }, - LastUpdated: earlier, - }, - }), + out: &v1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sub", + Namespace: "ns", + }, + Status: v1alpha1.SubscriptionStatus{ + CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ + catalogHealth("ns", "cs-0", &earlier, true), + catalogHealth("ns", "cs-1", &earlier, true), + }, + Conditions: []v1alpha1.SubscriptionCondition{ + catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier), + }, + LastUpdated: earlier, }, }, }, @@ -673,7 +654,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -685,10 +666,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanMissingState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -703,7 +684,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: now, }, - }), + }, }, }, { @@ -734,7 +715,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -749,10 +730,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanMissingState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -767,7 +748,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, }, { @@ -798,7 +779,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newCatalogHealthyState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -817,36 +798,28 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: &installPlanMissingState{ - InstallPlanKnownState: &installPlanKnownState{ - InstallPlanReferencedState: &installPlanReferencedState{ - InstallPlanState: &installPlanState{ - SubscriptionExistsState: newCatalogHealthyState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - InstallPlanRef: &corev1.ObjectReference{ - Namespace: "ns", - Name: "ip", - }, - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, true), - catalogHealth("ns", "cs-1", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier), - planMissingCondition(corev1.ConditionTrue, v1alpha1.ReferencedInstallPlanNotFound, "", &now), - }, - LastUpdated: now, - }, - }), - }, + out: &v1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sub", + Namespace: "ns", + }, + Status: v1alpha1.SubscriptionStatus{ + InstallPlanRef: &corev1.ObjectReference{ + Namespace: "ns", + Name: "ip", + }, + CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ + catalogHealth("ns", "cs-0", &earlier, true), + catalogHealth("ns", "cs-1", &earlier, true), + }, + Conditions: []v1alpha1.SubscriptionCondition{ + catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier), + planMissingCondition(corev1.ConditionTrue, v1alpha1.ReferencedInstallPlanNotFound, "", &now), }, + LastUpdated: now, }, }, }, @@ -877,7 +850,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -889,10 +862,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanPendingState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -907,7 +880,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: now, }, - }), + }, }, }, { @@ -941,7 +914,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -953,10 +926,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanPendingState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -971,7 +944,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: now, }, - }), + }, }, }, { @@ -1008,7 +981,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1023,10 +996,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanPendingState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1041,7 +1014,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, }, { @@ -1075,7 +1048,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1087,10 +1060,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanPendingState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1105,7 +1078,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: now, }, - }), + }, }, }, { @@ -1139,7 +1112,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1151,10 +1124,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanPendingState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1169,7 +1142,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: now, }, - }), + }, }, }, { @@ -1198,7 +1171,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1214,10 +1187,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, State: v1alpha1.SubscriptionStateUpgradePending, }, - }), + }, }, want: want{ - out: newInstallPlanMissingState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1226,7 +1199,7 @@ func TestInstallPlanReconcile(t *testing.T) { LastUpdated: now, State: v1alpha1.SubscriptionStateNone, }, - }), + }, }, }, { @@ -1263,7 +1236,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1278,10 +1251,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanPendingState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1296,7 +1269,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, }, { @@ -1333,7 +1306,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1348,10 +1321,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanFailedState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1366,7 +1339,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: now, }, - }), + }, }, }, { @@ -1410,7 +1383,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1425,10 +1398,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanFailedState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1443,7 +1416,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: now, }, - }), + }, }, }, { @@ -1487,7 +1460,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1502,10 +1475,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanFailedState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1520,7 +1493,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, }, { @@ -1557,7 +1530,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1572,10 +1545,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanInstalledState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1585,9 +1558,9 @@ func TestInstallPlanReconcile(t *testing.T) { Namespace: "ns", Name: "ip", }, - LastUpdated: now, + LastUpdated: earlier, }, - }), + }, }, }, { @@ -1621,7 +1594,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, }, args: args{ - in: newSubscriptionExistsState(&v1alpha1.Subscription{ + in: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1633,10 +1606,10 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, want: want{ - out: newInstallPlanInstalledState(&v1alpha1.Subscription{ + out: &v1alpha1.Subscription{ ObjectMeta: metav1.ObjectMeta{ Name: "sub", Namespace: "ns", @@ -1648,7 +1621,7 @@ func TestInstallPlanReconcile(t *testing.T) { }, LastUpdated: earlier, }, - }), + }, }, }, } @@ -1663,12 +1636,6 @@ func TestInstallPlanReconcile(t *testing.T) { out, err := rec.Reconcile(ctx, tt.args.in) require.Equal(t, tt.want.err, err) require.Equal(t, tt.want.out, out) - - // Ensure the client's view of the subscription matches the typestate's - sub := out.(SubscriptionState).Subscription() - clusterSub, err := rec.client.OperatorsV1alpha1().Subscriptions(sub.GetNamespace()).Get(context.TODO(), sub.GetName(), metav1.GetOptions{}) - require.NoError(t, err) - require.Equal(t, sub, clusterSub) }) } } @@ -1709,7 +1676,6 @@ func newFakeCatalogHealthReconciler(ctx context.Context, t *testing.T, config *f rec := &catalogHealthReconciler{ now: config.now, - client: fakeClient, catalogLister: lister.OperatorsV1alpha1().CatalogSourceLister(), registryReconcilerFactory: config.registryReconcilerFactory, globalCatalogNamespace: config.globalCatalogNamespace, @@ -1730,7 +1696,6 @@ func newFakeInstallPlanReconciler(ctx context.Context, t *testing.T, config *fak rec := &installPlanReconciler{ now: config.now, - client: fakeClient, installPlanLister: lister.OperatorsV1alpha1().InstallPlanLister(), } @@ -1740,81 +1705,6 @@ func newFakeInstallPlanReconciler(ctx context.Context, t *testing.T, config *fak return rec } -// Helper functions to shortcut to a particular state. -// They should not be used outside of testing. - -func newSubscriptionExistsState(sub *v1alpha1.Subscription) SubscriptionExistsState { - return &subscriptionExistsState{ - SubscriptionState: NewSubscriptionState(sub), - } -} - -func newCatalogHealthState(sub *v1alpha1.Subscription) CatalogHealthState { - return &catalogHealthState{ - SubscriptionExistsState: newSubscriptionExistsState(sub), - } -} - -func newCatalogHealthKnownState(sub *v1alpha1.Subscription) CatalogHealthKnownState { - return &catalogHealthKnownState{ - CatalogHealthState: newCatalogHealthState(sub), - } -} - -func newCatalogHealthyState(sub *v1alpha1.Subscription) CatalogHealthyState { - return &catalogHealthyState{ - CatalogHealthKnownState: newCatalogHealthKnownState(sub), - } -} - -func newCatalogUnhealthyState(sub *v1alpha1.Subscription) CatalogUnhealthyState { - return &catalogUnhealthyState{ - CatalogHealthKnownState: newCatalogHealthKnownState(sub), - } -} - -func newNoInstallPlanReferencedState(sub *v1alpha1.Subscription) NoInstallPlanReferencedState { - return &noInstallPlanReferencedState{ - InstallPlanState: newInstallPlanState(newSubscriptionExistsState(sub)), - } -} - -func newInstallPlanReferencedState(sub *v1alpha1.Subscription) InstallPlanReferencedState { - return &installPlanReferencedState{ - InstallPlanState: newInstallPlanState(newSubscriptionExistsState(sub)), - } -} - -func newInstallPlanKnownState(sub *v1alpha1.Subscription) InstallPlanKnownState { - return &installPlanKnownState{ - InstallPlanReferencedState: newInstallPlanReferencedState(sub), - } -} - -func newInstallPlanMissingState(sub *v1alpha1.Subscription) InstallPlanMissingState { - return &installPlanMissingState{ - InstallPlanKnownState: newInstallPlanKnownState(sub), - } -} - -func newInstallPlanPendingState(sub *v1alpha1.Subscription) InstallPlanPendingState { - return &installPlanPendingState{ - InstallPlanKnownState: newInstallPlanKnownState(sub), - } -} - -func newInstallPlanFailedState(sub *v1alpha1.Subscription) InstallPlanFailedState { - return &installPlanFailedState{ - InstallPlanKnownState: newInstallPlanKnownState(sub), - } -} - -func newInstallPlanInstalledState(sub *v1alpha1.Subscription) InstallPlanInstalledState { - return &installPlanInstalledState{ - InstallPlanKnownState: newInstallPlanKnownState(sub), - } -} - // Helper functions for generating OLM resources. func catalogSource(namespace, name string) *v1alpha1.CatalogSource { @@ -1850,3 +1740,43 @@ func withInstallPlanStatus(plan *v1alpha1.InstallPlan, status *v1alpha1.InstallP return plan } + +func catalogHealth(namespace, name string, lastUpdated *metav1.Time, healthy bool) v1alpha1.SubscriptionCatalogHealth { + return v1alpha1.SubscriptionCatalogHealth{ + CatalogSourceRef: &corev1.ObjectReference{ + Kind: v1alpha1.CatalogSourceKind, + Namespace: namespace, + Name: name, + UID: types.UID(name), + APIVersion: v1alpha1.CatalogSourceCRDAPIVersion, + }, + LastUpdated: lastUpdated, + Healthy: healthy, + } +} + +func subscriptionCondition(conditionType v1alpha1.SubscriptionConditionType, status corev1.ConditionStatus, reason, message string, time *metav1.Time) v1alpha1.SubscriptionCondition { + return v1alpha1.SubscriptionCondition{ + Type: conditionType, + Status: status, + Reason: reason, + Message: message, + LastTransitionTime: time, + } +} + +func catalogUnhealthyCondition(status corev1.ConditionStatus, reason, message string, time *metav1.Time) v1alpha1.SubscriptionCondition { + return subscriptionCondition(v1alpha1.SubscriptionCatalogSourcesUnhealthy, status, reason, message, time) +} + +func planMissingCondition(status corev1.ConditionStatus, reason, message string, time *metav1.Time) v1alpha1.SubscriptionCondition { + return subscriptionCondition(v1alpha1.SubscriptionInstallPlanMissing, status, reason, message, time) +} + +func planFailedCondition(status corev1.ConditionStatus, reason, message string, time *metav1.Time) v1alpha1.SubscriptionCondition { + return subscriptionCondition(v1alpha1.SubscriptionInstallPlanFailed, status, reason, message, time) +} + +func planPendingCondition(status corev1.ConditionStatus, reason, message string, time *metav1.Time) v1alpha1.SubscriptionCondition { + return subscriptionCondition(v1alpha1.SubscriptionInstallPlanPending, status, reason, message, time) +} diff --git a/pkg/controller/operators/catalog/subscription/state.go b/pkg/controller/operators/catalog/subscription/state.go deleted file mode 100644 index 50e26b67b9..0000000000 --- a/pkg/controller/operators/catalog/subscription/state.go +++ /dev/null @@ -1,553 +0,0 @@ -package subscription - -import ( - "bytes" - "context" - "fmt" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - - "github.com/operator-framework/api/pkg/operators/v1alpha1" - clientv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/typed/operators/v1alpha1" - "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/comparison" - "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubestate" -) - -// SubscriptionState describes subscription states. -type SubscriptionState interface { - kubestate.State - - isSubscriptionState() - setSubscription(*v1alpha1.Subscription) - - Subscription() *v1alpha1.Subscription - Add() SubscriptionExistsState - Update() SubscriptionExistsState - Delete() SubscriptionDeletedState -} - -// SubscriptionExistsState describes subscription states in which the subscription exists on the cluster. -type SubscriptionExistsState interface { - SubscriptionState - - isSubscriptionExistsState() -} - -// SubscriptionAddedState describes subscription states in which the subscription was added to cluster. -type SubscriptionAddedState interface { - SubscriptionExistsState - - isSubscriptionAddedState() -} - -// SubscriptionUpdatedState describes subscription states in which the subscription was updated in the cluster. -type SubscriptionUpdatedState interface { - SubscriptionExistsState - - isSubscriptionUpdatedState() -} - -// SubscriptionDeletedState describes subscription states in which the subscription no longer exists and was deleted from the cluster. -type SubscriptionDeletedState interface { - SubscriptionState - - isSubscriptionDeletedState() -} - -// CatalogHealthState describes subscription states that represent a subscription with respect to catalog health. -type CatalogHealthState interface { - SubscriptionExistsState - - isCatalogHealthState() - - // UpdateHealth transitions the CatalogHealthState to another CatalogHealthState based on the given subscription catalog health. - // The state's underlying subscription may be updated. If the subscription is updated, a boolean value of 'true' will be - // returned and the resulting state will contain the updated version. - UpdateHealth(now *metav1.Time, health ...v1alpha1.SubscriptionCatalogHealth) (CatalogHealthState, bool) -} - -// CatalogHealthKnownState describes subscription states in which all relevant catalog health is known. -type CatalogHealthKnownState interface { - CatalogHealthState - - isCatalogHealthKnownState() -} - -// CatalogHealthyState describes subscription states in which all relevant catalogs are known to be healthy. -type CatalogHealthyState interface { - CatalogHealthKnownState - - isCatalogHealthyState() -} - -// CatalogUnhealthyState describes subscription states in which at least one relevant catalog is known to be unhealthy. -type CatalogUnhealthyState interface { - CatalogHealthKnownState - - isCatalogUnhealthyState() -} - -// InstallPlanState describes Subscription states with respect to an InstallPlan. -type InstallPlanState interface { - SubscriptionExistsState - - isInstallPlanState() - - CheckReference() InstallPlanState -} - -type NoInstallPlanReferencedState interface { - InstallPlanState - - isNoInstallPlanReferencedState() -} - -type InstallPlanReferencedState interface { - InstallPlanState - - isInstallPlanReferencedState() - - InstallPlanNotFound(now *metav1.Time, client clientv1alpha1.SubscriptionInterface) (InstallPlanReferencedState, error) - - CheckInstallPlanStatus(now *metav1.Time, client clientv1alpha1.SubscriptionInterface, status *v1alpha1.InstallPlanStatus) (InstallPlanReferencedState, error) -} - -type InstallPlanKnownState interface { - InstallPlanReferencedState - - isInstallPlanKnownState() -} - -type InstallPlanMissingState interface { - InstallPlanKnownState - - isInstallPlanMissingState() -} - -type InstallPlanPendingState interface { - InstallPlanKnownState - - isInstallPlanPendingState() -} - -type InstallPlanFailedState interface { - InstallPlanKnownState - - isInstallPlanFailedState() -} - -type InstallPlanInstalledState interface { - InstallPlanKnownState - - isInstallPlanInstalledState() -} - -type subscriptionState struct { - kubestate.State - - subscription *v1alpha1.Subscription -} - -func (s *subscriptionState) isSubscriptionState() {} - -func (s *subscriptionState) setSubscription(sub *v1alpha1.Subscription) { - s.subscription = sub -} - -func (s *subscriptionState) Subscription() *v1alpha1.Subscription { - return s.subscription -} - -func (s *subscriptionState) Add() SubscriptionExistsState { - return &subscriptionAddedState{ - SubscriptionExistsState: &subscriptionExistsState{ - SubscriptionState: s, - }, - } -} - -func (s *subscriptionState) Update() SubscriptionExistsState { - return &subscriptionUpdatedState{ - SubscriptionExistsState: &subscriptionExistsState{ - SubscriptionState: s, - }, - } -} - -func (s *subscriptionState) Delete() SubscriptionDeletedState { - return &subscriptionDeletedState{ - SubscriptionState: s, - } -} - -func NewSubscriptionState(sub *v1alpha1.Subscription) SubscriptionState { - return &subscriptionState{ - State: kubestate.NewState(), - subscription: sub, - } -} - -type subscriptionExistsState struct { - SubscriptionState -} - -func (*subscriptionExistsState) isSubscriptionExistsState() {} - -type subscriptionAddedState struct { - SubscriptionExistsState -} - -func (c *subscriptionAddedState) isSubscriptionAddedState() {} - -type subscriptionUpdatedState struct { - SubscriptionExistsState -} - -func (c *subscriptionUpdatedState) isSubscriptionUpdatedState() {} - -type subscriptionDeletedState struct { - SubscriptionState -} - -func (c *subscriptionDeletedState) isSubscriptionDeletedState() {} - -type catalogHealthState struct { - SubscriptionExistsState -} - -func (c *catalogHealthState) isCatalogHealthState() {} - -func (c *catalogHealthState) UpdateHealth(now *metav1.Time, catalogHealth ...v1alpha1.SubscriptionCatalogHealth) (CatalogHealthState, bool) { - in := c.Subscription() - out := in.DeepCopy() - - healthSet := make(map[types.UID]v1alpha1.SubscriptionCatalogHealth, len(catalogHealth)) - healthy := true - missingTargeted := true - - cond := out.Status.GetCondition(v1alpha1.SubscriptionCatalogSourcesUnhealthy) - for _, h := range catalogHealth { - ref := h.CatalogSourceRef - healthSet[ref.UID] = h - healthy = healthy && h.Healthy - - if ref.Namespace == in.Spec.CatalogSourceNamespace && ref.Name == in.Spec.CatalogSource { - missingTargeted = false - if !h.Healthy { - cond.Message = fmt.Sprintf("targeted catalogsource %s/%s unhealthy", ref.Namespace, ref.Name) - } - } - } - - var known CatalogHealthKnownState - switch { - case missingTargeted: - cond.Message = fmt.Sprintf("targeted catalogsource %s/%s missing", in.Spec.CatalogSourceNamespace, in.Spec.CatalogSource) - fallthrough - case !healthy: - cond.Status = corev1.ConditionTrue - cond.Reason = v1alpha1.UnhealthyCatalogSourceFound - known = &catalogUnhealthyState{ - CatalogHealthKnownState: &catalogHealthKnownState{ - CatalogHealthState: c, - }, - } - default: - cond.Status = corev1.ConditionFalse - cond.Reason = v1alpha1.AllCatalogSourcesHealthy - cond.Message = "all available catalogsources are healthy" - known = &catalogHealthyState{ - CatalogHealthKnownState: &catalogHealthKnownState{ - CatalogHealthState: c, - }, - } - } - - // Check for changes in CatalogHealth - update := true - switch numNew, numOld := len(healthSet), len(in.Status.CatalogHealth); { - case numNew > numOld: - cond.Reason = v1alpha1.CatalogSourcesAdded - case numNew < numOld: - cond.Reason = v1alpha1.CatalogSourcesDeleted - case numNew == 0 && numNew == numOld: - cond.Reason = v1alpha1.NoCatalogSourcesFound - cond.Message = "dependency resolution requires at least one catalogsource" - case numNew == numOld: - // Check against existing subscription - for _, oldHealth := range in.Status.CatalogHealth { - uid := oldHealth.CatalogSourceRef.UID - if newHealth, ok := healthSet[uid]; !ok || !newHealth.Equals(oldHealth) { - cond.Reason = v1alpha1.CatalogSourcesUpdated - break - } - } - - fallthrough - default: - update = false - } - - if !update && cond.Equals(in.Status.GetCondition(v1alpha1.SubscriptionCatalogSourcesUnhealthy)) { - // Nothing to do, transition to self - return known, false - } - - cond.LastTransitionTime = now - out.Status.LastUpdated = *now - out.Status.SetCondition(cond) - out.Status.CatalogHealth = catalogHealth - - // Inject updated subscription into the state - known.setSubscription(out) - - return known, true -} - -func NewCatalogHealthState(s SubscriptionExistsState) CatalogHealthState { - return &catalogHealthState{ - SubscriptionExistsState: s, - } -} - -type catalogHealthKnownState struct { - CatalogHealthState -} - -func (c *catalogHealthKnownState) isCatalogHealthKnownState() {} - -func (c *catalogHealthKnownState) CatalogHealth() []v1alpha1.SubscriptionCatalogHealth { - return c.Subscription().Status.CatalogHealth -} - -type catalogHealthyState struct { - CatalogHealthKnownState -} - -func (c *catalogHealthyState) isCatalogHealthyState() {} - -type catalogUnhealthyState struct { - CatalogHealthKnownState -} - -func (c *catalogUnhealthyState) isCatalogUnhealthyState() {} - -type installPlanState struct { - SubscriptionExistsState -} - -func (i *installPlanState) isInstallPlanState() {} - -func (i *installPlanState) CheckReference() InstallPlanState { - if i.Subscription().Status.InstallPlanRef != nil { - return &installPlanReferencedState{ - InstallPlanState: i, - } - } - - return &noInstallPlanReferencedState{ - InstallPlanState: i, - } -} - -func newInstallPlanState(s SubscriptionExistsState) InstallPlanState { - return &installPlanState{ - SubscriptionExistsState: s, - } -} - -type noInstallPlanReferencedState struct { - InstallPlanState -} - -func (n *noInstallPlanReferencedState) isNoInstallPlanReferencedState() {} - -type installPlanReferencedState struct { - InstallPlanState -} - -func (i *installPlanReferencedState) isInstallPlanReferencedState() {} - -var hashEqual = comparison.NewHashEqualitor() - -func (i *installPlanReferencedState) InstallPlanNotFound(now *metav1.Time, client clientv1alpha1.SubscriptionInterface) (InstallPlanReferencedState, error) { - in := i.Subscription() - out := in.DeepCopy() - - // Remove pending and failed conditions - out.Status.RemoveConditions(v1alpha1.SubscriptionInstallPlanPending, v1alpha1.SubscriptionInstallPlanFailed) - - // If the installplan is missing when subscription is in pending upgrade, - // clear the installplan ref so the resolution can happen again - if in.Status.State == v1alpha1.SubscriptionStateUpgradePending { - out.Status.InstallPlanRef = nil - out.Status.Install = nil - out.Status.CurrentCSV = "" - out.Status.State = v1alpha1.SubscriptionStateNone - out.Status.LastUpdated = *now - } else { - // Set missing condition to true - cond := out.Status.GetCondition(v1alpha1.SubscriptionInstallPlanMissing) - cond.Status = corev1.ConditionTrue - cond.Reason = v1alpha1.ReferencedInstallPlanNotFound - cond.LastTransitionTime = now - out.Status.SetCondition(cond) - } - - // Build missing state - missingState := &installPlanMissingState{ - InstallPlanKnownState: &installPlanKnownState{ - InstallPlanReferencedState: i, - }, - } - - // Bail out if the conditions haven't changed (using select fields included in a hash) - if hashEqual(out.Status.Conditions, in.Status.Conditions) { - return missingState, nil - } - - // Update the Subscription - out.Status.LastUpdated = *now - updated, err := client.UpdateStatus(context.TODO(), out, metav1.UpdateOptions{}) - if err != nil { - return i, err - } - - // Stuff updated Subscription into state - missingState.setSubscription(updated) - - return missingState, nil -} - -func (i *installPlanReferencedState) CheckInstallPlanStatus(now *metav1.Time, client clientv1alpha1.SubscriptionInterface, status *v1alpha1.InstallPlanStatus) (InstallPlanReferencedState, error) { - in := i.Subscription() - out := in.DeepCopy() - - // Remove missing, pending, and failed conditions - out.Status.RemoveConditions(v1alpha1.SubscriptionInstallPlanMissing, v1alpha1.SubscriptionInstallPlanPending, v1alpha1.SubscriptionInstallPlanFailed) - - // Build and set the InstallPlan condition, if any - cond := v1alpha1.SubscriptionCondition{ - Status: corev1.ConditionUnknown, - LastTransitionTime: now, - } - - // TODO: Use InstallPlan conditions instead of phases - // Get the status of the InstallPlan and create the appropriate condition and state - var known InstallPlanKnownState - switch phase := status.Phase; phase { - case v1alpha1.InstallPlanPhaseNone: - // Set reason and let the following case fill out the pending condition - cond.Reason = v1alpha1.InstallPlanNotYetReconciled - fallthrough - case v1alpha1.InstallPlanPhasePlanning, v1alpha1.InstallPlanPhaseInstalling, v1alpha1.InstallPlanPhaseRequiresApproval: - if cond.Reason == "" { - cond.Reason = string(phase) - } - cond.Message = extractMessage(status) - cond.Type = v1alpha1.SubscriptionInstallPlanPending - cond.Status = corev1.ConditionTrue - out.Status.SetCondition(cond) - - // Build pending state - known = &installPlanPendingState{ - InstallPlanKnownState: &installPlanKnownState{ - InstallPlanReferencedState: i, - }, - } - case v1alpha1.InstallPlanPhaseFailed: - // Attempt to project reason from failed InstallPlan condition - if installedCond := status.GetCondition(v1alpha1.InstallPlanInstalled); installedCond.Status == corev1.ConditionFalse { - cond.Reason = string(installedCond.Reason) - } else { - cond.Reason = v1alpha1.InstallPlanFailed - } - - cond.Type = v1alpha1.SubscriptionInstallPlanFailed - cond.Message = extractMessage(status) - cond.Status = corev1.ConditionTrue - out.Status.SetCondition(cond) - - // Build failed state - known = &installPlanFailedState{ - InstallPlanKnownState: &installPlanKnownState{ - InstallPlanReferencedState: i, - }, - } - default: - // Build installed state - known = &installPlanInstalledState{ - InstallPlanKnownState: &installPlanKnownState{ - InstallPlanReferencedState: i, - }, - } - } - - // Bail out if the conditions haven't changed (using select fields included in a hash) - if hashEqual(out.Status.Conditions, in.Status.Conditions) { - return known, nil - } - - // Update the Subscription - out.Status.LastUpdated = *now - updated, err := client.UpdateStatus(context.TODO(), out, metav1.UpdateOptions{}) - if err != nil { - return i, err - } - - // Stuff updated Subscription into state - known.setSubscription(updated) - - return known, nil -} - -func extractMessage(status *v1alpha1.InstallPlanStatus) string { - if cond := status.GetCondition(v1alpha1.InstallPlanInstalled); cond.Status != corev1.ConditionUnknown && cond.Message != "" { - return cond.Message - } - - var b bytes.Buffer - for _, lookup := range status.BundleLookups { - if cond := lookup.GetCondition(v1alpha1.BundleLookupPending); cond.Status != corev1.ConditionUnknown && cond.Message != "" { - if b.Len() != 0 { - b.WriteString(" ") - } - b.WriteString(cond.Message) - b.WriteString(".") - } - } - - return b.String() -} - -type installPlanKnownState struct { - InstallPlanReferencedState -} - -func (i *installPlanKnownState) isInstallPlanKnownState() {} - -type installPlanMissingState struct { - InstallPlanKnownState -} - -func (i *installPlanMissingState) isInstallPlanMissingState() {} - -type installPlanPendingState struct { - InstallPlanKnownState -} - -func (i *installPlanPendingState) isInstallPlanPendingState() {} - -type installPlanFailedState struct { - InstallPlanKnownState -} - -func (i *installPlanFailedState) isInstallPlanFailedState() {} - -type installPlanInstalledState struct { - InstallPlanKnownState -} - -func (i *installPlanInstalledState) isInstallPlanInstalledState() {} diff --git a/pkg/controller/operators/catalog/subscription/state_test.go b/pkg/controller/operators/catalog/subscription/state_test.go deleted file mode 100644 index 7242b968c3..0000000000 --- a/pkg/controller/operators/catalog/subscription/state_test.go +++ /dev/null @@ -1,1815 +0,0 @@ -package subscription - -import ( - "context" - "testing" - "time" - - "github.com/operator-framework/api/pkg/operators/v1alpha1" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - utilclock "k8s.io/utils/clock/testing" -) - -func TestUpdateHealth(t *testing.T) { - clockFake := utilclock.NewFakeClock(time.Date(2018, time.January, 26, 20, 40, 0, 0, time.UTC)) - now := metav1.NewTime(clockFake.Now()) - earlier := metav1.NewTime(now.Add(-time.Minute)) - - type fields struct { - existingObjs existingObjs - namespace string - state CatalogHealthState - } - type args struct { - now *metav1.Time - catalogHealth []v1alpha1.SubscriptionCatalogHealth - } - type want struct { - transitioned CatalogHealthState - terminal bool - updated bool - } - - tests := []struct { - description string - fields fields - args args - want want - }{ - { - description: "CatalogHealthState/NoCatalogSources/NoConditions/Unhealthy/ConditionsAdded", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - }, - }, - }, - namespace: "ns", - state: newCatalogHealthState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - }), - }, - args: args{ - now: &now, - }, - want: want{ - updated: true, - transitioned: newCatalogUnhealthyState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionTrue, v1alpha1.NoCatalogSourcesFound, "dependency resolution requires at least one catalogsource", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "CatalogHealthState/CatalogSources/NoConditions/Unhealthy/CatalogsAdded", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - }, - }, - }, - namespace: "ns", - state: newCatalogHealthState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, false), - }, - }, - }), - }, - args: args{ - now: &now, - catalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &now, false), - }, - }, - want: want{ - updated: true, - transitioned: newCatalogUnhealthyState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &now, false), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionTrue, v1alpha1.UnhealthyCatalogSourceFound, "targeted catalogsource ns/cs-0 unhealthy", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "CatalogHealthState/CatalogSources/Conditions/Unhealthy/NoChanges", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, false), - catalogHealth("ns", "cs-1", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionTrue, v1alpha1.UnhealthyCatalogSourceFound, "targeted catalogsource ns/cs-0 unhealthy", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newCatalogHealthState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, false), - catalogHealth("ns", "cs-1", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionTrue, v1alpha1.UnhealthyCatalogSourceFound, "targeted catalogsource ns/cs-0 unhealthy", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - catalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &now, false), - catalogHealth("ns", "cs-1", &now, true), - }, - }, - want: want{ - updated: false, - transitioned: newCatalogUnhealthyState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, false), - catalogHealth("ns", "cs-1", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionTrue, v1alpha1.UnhealthyCatalogSourceFound, "targeted catalogsource ns/cs-0 unhealthy", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - }, - { - description: "CatalogHealthState/CatalogSources/Conditions/Unhealthy/ToHealthy", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, false), - catalogHealth("ns", "cs-1", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionTrue, v1alpha1.UnhealthyCatalogSourceFound, "targeted catalogsource ns/cs-0 unhealthy", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newCatalogHealthState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, false), - catalogHealth("ns", "cs-1", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionTrue, v1alpha1.UnhealthyCatalogSourceFound, "targeted catalogsource ns/cs-0 unhealthy", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - catalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &now, true), - catalogHealth("ns", "cs-1", &now, true), - }, - }, - want: want{ - updated: true, - transitioned: newCatalogHealthyState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &now, true), - catalogHealth("ns", "cs-1", &now, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.CatalogSourcesUpdated, "all available catalogsources are healthy", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "CatalogHealthState/CatalogSources/Conditions/MissingTargeted/Healthy/ToUnhealthy", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, false), - catalogHealth("ns", "cs-1", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newCatalogHealthState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, false), - catalogHealth("ns", "cs-1", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - catalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-1", &now, true), - catalogHealth("global", "cs-g", &now, true), - }, - }, - want: want{ - updated: true, - transitioned: newCatalogUnhealthyState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-1", &now, true), - catalogHealth("global", "cs-g", &now, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionTrue, v1alpha1.CatalogSourcesUpdated, "targeted catalogsource ns/cs-0 missing", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "CatalogHealthState/CatalogSources/Conditions/Healthy/NoChanges", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, true), - catalogHealth("ns", "cs-1", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newCatalogHealthState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, true), - catalogHealth("ns", "cs-1", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - catalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &now, true), - catalogHealth("ns", "cs-1", &now, true), - }, - }, - want: want{ - updated: false, - transitioned: newCatalogHealthyState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Spec: &v1alpha1.SubscriptionSpec{ - CatalogSourceNamespace: "ns", - CatalogSource: "cs-0", - }, - Status: v1alpha1.SubscriptionStatus{ - CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{ - catalogHealth("ns", "cs-0", &earlier, true), - catalogHealth("ns", "cs-1", &earlier, true), - }, - Conditions: []v1alpha1.SubscriptionCondition{ - catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - transitioned, updated := tt.fields.state.UpdateHealth(tt.args.now, tt.args.catalogHealth...) - require.Equal(t, tt.want.transitioned, transitioned) - require.Equal(t, tt.want.updated, updated) - if tt.want.transitioned != nil { - require.Equal(t, tt.want.terminal, transitioned.Terminal()) - } - }) - } -} - -func TestCheckReference(t *testing.T) { - type fields struct { - state InstallPlanState - } - type want struct { - transitioned InstallPlanState - terminal bool - } - - tests := []struct { - description string - fields fields - want want - }{ - { - description: "NoReference/FromInstallPlanState/ToNoInstallPlanReferencedState", - fields: fields{ - state: newInstallPlanState(newSubscriptionExistsState( - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }, - )), - }, - want: want{ - transitioned: newNoInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }), - terminal: false, - }, - }, - { - description: "NoReference/FromNoInstallPlanReferencedState/ToNoInstallPlanReferencedState", - fields: fields{ - state: newNoInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }), - }, - want: want{ - transitioned: newNoInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }), - terminal: false, - }, - }, - { - description: "NoReference/FromInstallPlanReferencedState/ToNoInstallPlanReferencedState", - fields: fields{ - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }), - }, - want: want{ - transitioned: newNoInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }), - terminal: false, - }, - }, - { - description: "Reference/FromInstallPlanState/ToInstallPlanReferencedState", - fields: fields{ - state: newInstallPlanState(newSubscriptionExistsState( - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - InstallPlanRef: &corev1.ObjectReference{ - Namespace: "ns", - Name: "ip", - }, - }, - }, - )), - }, - want: want{ - transitioned: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - InstallPlanRef: &corev1.ObjectReference{ - Namespace: "ns", - Name: "ip", - }, - }, - }), - terminal: false, - }, - }, - { - description: "Reference/FromInstallPlanReferencedState/ToInstallPlanReferencedState", - fields: fields{ - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - InstallPlanRef: &corev1.ObjectReference{ - Namespace: "ns", - Name: "ip", - }, - }, - }), - }, - want: want{ - transitioned: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - InstallPlanRef: &corev1.ObjectReference{ - Namespace: "ns", - Name: "ip", - }, - }, - }), - terminal: false, - }, - }, - { - description: "Reference/FromNoInstallPlanReferencedState/ToInstallPlanReferencedState", - fields: fields{ - state: newNoInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - InstallPlanRef: &corev1.ObjectReference{ - Namespace: "ns", - Name: "ip", - }, - }, - }), - }, - want: want{ - transitioned: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - InstallPlanRef: &corev1.ObjectReference{ - Namespace: "ns", - Name: "ip", - }, - }, - }), - terminal: false, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - transitioned := tt.fields.state.CheckReference() - require.Equal(t, tt.want.transitioned, transitioned) - require.Equal(t, tt.want.terminal, transitioned.Terminal()) - }) - } -} - -func TestInstallPlanNotFound(t *testing.T) { - clockFake := utilclock.NewFakeClock(time.Date(2018, time.January, 26, 20, 40, 0, 0, time.UTC)) - now := metav1.NewTime(clockFake.Now()) - earlier := metav1.NewTime(now.Add(-time.Minute)) - - type fields struct { - existingObjs existingObjs - namespace string - state InstallPlanReferencedState - } - type args struct { - now *metav1.Time - } - type want struct { - transitioned InstallPlanReferencedState - terminal bool - err error - } - tests := []struct { - description string - fields fields - args args - want want - }{ - { - description: "InstallPlanReferencedState/NoConditions/ToInstallPlanMissingState/Update", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - }, - want: want{ - transitioned: newInstallPlanMissingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, v1alpha1.ReferencedInstallPlanNotFound, "", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/Conditions/ToInstallPlanMissingState/NoUpdate", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, v1alpha1.ReferencedInstallPlanNotFound, "", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, v1alpha1.ReferencedInstallPlanNotFound, "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - }, - want: want{ - transitioned: newInstallPlanMissingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, v1alpha1.ReferencedInstallPlanNotFound, "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - }, - { - description: "InstallPlanMissingState/Conditions/ToInstallPlanMissingState/NoUpdate", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, v1alpha1.ReferencedInstallPlanNotFound, "", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanMissingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, v1alpha1.ReferencedInstallPlanNotFound, "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - }, - want: want{ - transitioned: newInstallPlanMissingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, v1alpha1.ReferencedInstallPlanNotFound, "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/Conditions/ToInstallPlanMissingState/Update/RemovesFailedAndPending", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, v1alpha1.ReferencedInstallPlanNotFound, "", &earlier), - planFailedCondition(corev1.ConditionTrue, "", "", &earlier), - planPendingCondition(corev1.ConditionTrue, "", "", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, v1alpha1.ReferencedInstallPlanNotFound, "", &earlier), - planFailedCondition(corev1.ConditionTrue, "", "", &earlier), - planPendingCondition(corev1.ConditionTrue, "", "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - }, - want: want{ - transitioned: newInstallPlanMissingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, v1alpha1.ReferencedInstallPlanNotFound, "", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - fakeClient := tt.fields.existingObjs.fakeClientset(t).OperatorsV1alpha1().Subscriptions(tt.fields.namespace) - transitioned, err := tt.fields.state.InstallPlanNotFound(tt.args.now, fakeClient) - require.Equal(t, tt.want.err, err) - require.Equal(t, tt.want.transitioned, transitioned) - - if tt.want.transitioned != nil { - require.Equal(t, tt.want.terminal, transitioned.Terminal()) - - // Ensure the client's view of the subscription matches the typestate's - sub := transitioned.(SubscriptionState).Subscription() - clusterSub, err := fakeClient.Get(context.TODO(), sub.GetName(), metav1.GetOptions{}) - require.NoError(t, err) - require.Equal(t, sub, clusterSub) - } - }) - } -} - -func TestCheckInstallPlanStatus(t *testing.T) { - clockFake := utilclock.NewFakeClock(time.Date(2018, time.January, 26, 20, 40, 0, 0, time.UTC)) - now := metav1.NewTime(clockFake.Now()) - earlier := metav1.NewTime(now.Add(-time.Minute)) - - type fields struct { - existingObjs existingObjs - namespace string - state InstallPlanReferencedState - } - type args struct { - now *metav1.Time - status *v1alpha1.InstallPlanStatus - } - type want struct { - transitioned InstallPlanReferencedState - terminal bool - err error - } - tests := []struct { - description string - fields fields - args args - want want - }{ - { - description: "InstallPlanReferencedState/NoConditions/InstallPlanNotYetReconciled/ToInstallPlanPendingState/Update", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{}, - }, - want: want{ - transitioned: newInstallPlanPendingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, v1alpha1.InstallPlanNotYetReconciled, "", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/Conditions/InstallPlanNotYetReconciled/ToInstallPlanPendingState/NoUpdate", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, v1alpha1.InstallPlanNotYetReconciled, "", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, v1alpha1.InstallPlanNotYetReconciled, "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{}, - }, - want: want{ - transitioned: newInstallPlanPendingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, v1alpha1.InstallPlanNotYetReconciled, "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - }, - { - description: "InstallPlanPendingState/Conditions/InstallPlanNotYetReconciled/ToInstallPlanPendingState/NoUpdate", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, v1alpha1.InstallPlanNotYetReconciled, "", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanPendingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, v1alpha1.InstallPlanNotYetReconciled, "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{}, - }, - want: want{ - transitioned: newInstallPlanPendingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, v1alpha1.InstallPlanNotYetReconciled, "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/Conditions/InstallPlanNotYetReconciled/ToInstallPlanPendingState/Update/RemovesFailedAndMissing", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, "", "", &earlier), - planPendingCondition(corev1.ConditionTrue, v1alpha1.InstallPlanNotYetReconciled, "", &earlier), - planFailedCondition(corev1.ConditionTrue, "", "", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, "", "", &earlier), - planPendingCondition(corev1.ConditionTrue, v1alpha1.InstallPlanNotYetReconciled, "", &earlier), - planFailedCondition(corev1.ConditionTrue, "", "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{}, - }, - want: want{ - transitioned: newInstallPlanPendingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, v1alpha1.InstallPlanNotYetReconciled, "", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/NoConditions/RequiresApproval/ToInstallPlanPendingState/Update", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{ - Phase: v1alpha1.InstallPlanPhaseRequiresApproval, - }, - }, - want: want{ - transitioned: newInstallPlanPendingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanPhaseRequiresApproval), "", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/Conditions/RequiresApproval/ToInstallPlanPendingState/NoUpdate", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanPhaseRequiresApproval), "", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanPhaseRequiresApproval), "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{ - Phase: v1alpha1.InstallPlanPhaseRequiresApproval, - }, - }, - want: want{ - transitioned: newInstallPlanPendingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanPhaseRequiresApproval), "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/Conditions/RequiresApproval/ToInstallPlanPendingState/Update/RemovesMissingAndFailed", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, "", "", &earlier), - planPendingCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanPhaseRequiresApproval), "", &earlier), - planFailedCondition(corev1.ConditionTrue, "", "", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, "", "", &earlier), - planPendingCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanPhaseRequiresApproval), "", &earlier), - planFailedCondition(corev1.ConditionTrue, "", "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{ - Phase: v1alpha1.InstallPlanPhaseRequiresApproval, - }, - }, - want: want{ - transitioned: newInstallPlanPendingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanPhaseRequiresApproval), "", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/NoConditions/Installing/ToInstallPlanPendingState/Update", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{ - Phase: v1alpha1.InstallPlanPhaseInstalling, - Conditions: []v1alpha1.InstallPlanCondition{ - { - Type: v1alpha1.InstallPlanInstalled, - Message: "no operatorgroup found that is managing this namespace", - Reason: v1alpha1.InstallPlanConditionReason("Installing"), - Status: corev1.ConditionFalse, - }, - }, - }, - }, - want: want{ - transitioned: newInstallPlanPendingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanPhaseInstalling), "no operatorgroup found that is managing this namespace", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/Conditions/Installing/ToInstallPlanPendingState/NoUpdate", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanPhaseInstalling), "", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanPhaseInstalling), "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{ - Phase: v1alpha1.InstallPlanPhaseInstalling, - }, - }, - want: want{ - transitioned: newInstallPlanPendingState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planPendingCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanPhaseInstalling), "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/NoConditions/Failed/ToInstallPlanFailedState/Update/NoReason", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{ - Phase: v1alpha1.InstallPlanPhaseFailed, - }, - }, - want: want{ - transitioned: newInstallPlanFailedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planFailedCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanFailed), "", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/Conditions/Failed/ToInstallPlanFailedState/NoUpdate/NoReason", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planFailedCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanFailed), "", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planFailedCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanFailed), "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{ - Phase: v1alpha1.InstallPlanPhaseFailed, - }, - }, - want: want{ - transitioned: newInstallPlanFailedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planFailedCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanFailed), "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/NoConditions/Failed/ToInstallPlanFailedState/Update/InstallPlanReasonComponentFailed", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{ - Phase: v1alpha1.InstallPlanPhaseFailed, - Conditions: []v1alpha1.InstallPlanCondition{ - { - Type: v1alpha1.InstallPlanInstalled, - Status: corev1.ConditionFalse, - Reason: v1alpha1.InstallPlanReasonComponentFailed, - }, - }, - BundleLookups: []v1alpha1.BundleLookup{ - { - Conditions: []v1alpha1.BundleLookupCondition{ - { - Type: v1alpha1.BundleLookupPending, - Status: corev1.ConditionTrue, - Message: "unpack job not completed: Unpack pod(olm/c5a4) container(pull) is pending. Reason: ImagePullBackOff, Message: Back-off pulling image", - }, - }, - }, - }, - }, - }, - want: want{ - transitioned: newInstallPlanFailedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planFailedCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanReasonComponentFailed), "unpack job not completed: Unpack pod(olm/c5a4) container(pull) is pending. Reason: ImagePullBackOff, Message: Back-off pulling image.", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/NoConditions/Failed/ToInstallPlanFailedState/Update/InstallPlanReasonComponentFailed/MultipleBundleConditions", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{ - Phase: v1alpha1.InstallPlanPhaseFailed, - Conditions: []v1alpha1.InstallPlanCondition{ - { - Type: v1alpha1.InstallPlanInstalled, - Status: corev1.ConditionFalse, - Reason: v1alpha1.InstallPlanReasonComponentFailed, - }, - }, - BundleLookups: []v1alpha1.BundleLookup{ - { - Conditions: []v1alpha1.BundleLookupCondition{ - { - Type: v1alpha1.BundleLookupPending, - Status: corev1.ConditionTrue, - Message: "encountered issue foo", - }, - }, - }, - { - Conditions: []v1alpha1.BundleLookupCondition{ - { - Type: v1alpha1.BundleLookupPending, - Status: corev1.ConditionTrue, - Message: "encountered issue bar", - }, - }, - }, - }, - }, - }, - want: want{ - transitioned: newInstallPlanFailedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planFailedCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanReasonComponentFailed), "encountered issue foo. encountered issue bar.", &now), - }, - LastUpdated: now, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/Conditions/Failed/ToInstallPlanFailedState/NoUpdate/InstallPlanReasonComponentFailed", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planFailedCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanReasonComponentFailed), "", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planFailedCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanReasonComponentFailed), "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{ - Phase: v1alpha1.InstallPlanPhaseFailed, - Conditions: []v1alpha1.InstallPlanCondition{ - { - Type: v1alpha1.InstallPlanInstalled, - Status: corev1.ConditionFalse, - Reason: v1alpha1.InstallPlanReasonComponentFailed, - }, - }, - }, - }, - want: want{ - transitioned: newInstallPlanFailedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planFailedCondition(corev1.ConditionTrue, string(v1alpha1.InstallPlanReasonComponentFailed), "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - }, - { - description: "InstallPlanReferencedState/Conditions/Installed/ToInstallPlanInstalledState/Update/RemovesMissingPendingAndFailed", - fields: fields{ - existingObjs: existingObjs{ - clientObjs: []runtime.Object{ - &v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, "", "", &earlier), - planPendingCondition(corev1.ConditionTrue, "", "", &earlier), - planFailedCondition(corev1.ConditionTrue, "", "", &earlier), - }, - LastUpdated: earlier, - }, - }, - }, - }, - namespace: "ns", - state: newInstallPlanReferencedState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - Conditions: []v1alpha1.SubscriptionCondition{ - planMissingCondition(corev1.ConditionTrue, "", "", &earlier), - planPendingCondition(corev1.ConditionTrue, "", "", &earlier), - planFailedCondition(corev1.ConditionTrue, "", "", &earlier), - }, - LastUpdated: earlier, - }, - }), - }, - args: args{ - now: &now, - status: &v1alpha1.InstallPlanStatus{ - Phase: v1alpha1.InstallPlanPhaseComplete, - }, - }, - want: want{ - transitioned: newInstallPlanInstalledState(&v1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: "sub", - Namespace: "ns", - }, - Status: v1alpha1.SubscriptionStatus{ - LastUpdated: now, - }, - }), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - fakeClient := tt.fields.existingObjs.fakeClientset(t).OperatorsV1alpha1().Subscriptions(tt.fields.namespace) - transitioned, err := tt.fields.state.CheckInstallPlanStatus(tt.args.now, fakeClient, tt.args.status) - require.Equal(t, tt.want.err, err) - require.Equal(t, tt.want.transitioned, transitioned) - - if tt.want.transitioned != nil { - require.Equal(t, tt.want.terminal, transitioned.Terminal()) - - // Ensure the client's view of the subscription matches the typestate's - sub := transitioned.(SubscriptionState).Subscription() - clusterSub, err := fakeClient.Get(context.TODO(), sub.GetName(), metav1.GetOptions{}) - require.NoError(t, err) - require.Equal(t, sub, clusterSub) - } - }) - } -} - -func catalogHealth(namespace, name string, lastUpdated *metav1.Time, healthy bool) v1alpha1.SubscriptionCatalogHealth { - return v1alpha1.SubscriptionCatalogHealth{ - CatalogSourceRef: &corev1.ObjectReference{ - Kind: v1alpha1.CatalogSourceKind, - Namespace: namespace, - Name: name, - UID: types.UID(name), - APIVersion: v1alpha1.CatalogSourceCRDAPIVersion, - }, - LastUpdated: lastUpdated, - Healthy: healthy, - } -} - -func subscriptionCondition(conditionType v1alpha1.SubscriptionConditionType, status corev1.ConditionStatus, reason, message string, time *metav1.Time) v1alpha1.SubscriptionCondition { - return v1alpha1.SubscriptionCondition{ - Type: conditionType, - Status: status, - Reason: reason, - Message: message, - LastTransitionTime: time, - } -} - -func catalogUnhealthyCondition(status corev1.ConditionStatus, reason, message string, time *metav1.Time) v1alpha1.SubscriptionCondition { - return subscriptionCondition(v1alpha1.SubscriptionCatalogSourcesUnhealthy, status, reason, message, time) -} - -func planMissingCondition(status corev1.ConditionStatus, reason, message string, time *metav1.Time) v1alpha1.SubscriptionCondition { - return subscriptionCondition(v1alpha1.SubscriptionInstallPlanMissing, status, reason, message, time) -} - -func planFailedCondition(status corev1.ConditionStatus, reason, message string, time *metav1.Time) v1alpha1.SubscriptionCondition { - return subscriptionCondition(v1alpha1.SubscriptionInstallPlanFailed, status, reason, message, time) -} - -func planPendingCondition(status corev1.ConditionStatus, reason, message string, time *metav1.Time) v1alpha1.SubscriptionCondition { - return subscriptionCondition(v1alpha1.SubscriptionInstallPlanPending, status, reason, message, time) -} diff --git a/pkg/controller/operators/catalog/subscription/syncer.go b/pkg/controller/operators/catalog/subscription/syncer.go index 04e8edeb5a..1bc6abcfdb 100644 --- a/pkg/controller/operators/catalog/subscription/syncer.go +++ b/pkg/controller/operators/catalog/subscription/syncer.go @@ -4,6 +4,11 @@ import ( "context" "fmt" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/client-go/util/workqueue" + "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -12,6 +17,7 @@ import ( "github.com/operator-framework/api/pkg/operators/install" "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" listers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1" resolverCache "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubestate" @@ -29,13 +35,16 @@ func init() { // subscriptionSyncer syncs Subscriptions by invoking its reconciler chain for each Subscription event it receives. type subscriptionSyncer struct { logger *logrus.Logger + client versioned.Interface clock utilclock.Clock - reconcilers kubestate.ReconcilerChain + reconcilers ReconcilerChain subscriptionCache cache.Indexer + subscriptionLister listers.SubscriptionLister installPlanLister listers.InstallPlanLister globalCatalogNamespace string notify kubestate.NotifyFunc sourceProvider resolverCache.SourceProvider + nsResolveQueue workqueue.TypedRateLimitingInterface[any] } // now returns the Syncer's current time. @@ -47,16 +56,24 @@ func (s *subscriptionSyncer) now() *metav1.Time { // Sync reconciles Subscription events by invoking a sequence of reconcilers, passing the result of each // successful reconciliation as an argument to its successor. func (s *subscriptionSyncer) Sync(ctx context.Context, event kubestate.ResourceEvent) error { - res := &v1alpha1.Subscription{} - if err := scheme.Convert(event.Resource(), res, nil); err != nil { - return err - } + sub, ok := event.Resource().(*v1alpha1.Subscription) + if !ok { + tombstone, ok := event.Resource().(cache.DeletedFinalStateUnknown) + if !ok { + utilruntime.HandleError(fmt.Errorf("couldn't get object from tombstone %#v", event.Resource())) + return nil + } - metrics.EmitSubMetric(res) + sub, ok = tombstone.Obj.(*v1alpha1.Subscription) + if !ok { + utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a metav1 object %#v", event.Resource())) + return nil + } + } logger := s.logger.WithFields(logrus.Fields{ - "reconciling": fmt.Sprintf("%T", res), - "selflink": res.GetSelfLink(), + "reconciling": fmt.Sprintf("%T", event), + "selflink": sub.GetSelfLink(), "event": event.Type(), }) logger.Info("syncing") @@ -64,24 +81,33 @@ func (s *subscriptionSyncer) Sync(ctx context.Context, event kubestate.ResourceE // Enter initial state based on subscription and event type // TODO: Consider generalizing initial generic add, update, delete transitions in the kubestate package. // Possibly make a resource event aware bridge between Sync and reconciler. - initial := NewSubscriptionState(res.DeepCopy()) - switch event.Type() { - case kubestate.ResourceAdded: - initial = initial.Add() - case kubestate.ResourceUpdated: - initial = initial.Update() - metrics.UpdateSubsSyncCounterStorage(res) - case kubestate.ResourceDeleted: - initial = initial.Delete() - metrics.DeleteSubsMetric(res) - } - - reconciled, err := s.reconcilers.Reconcile(ctx, initial) + if event.Type() == kubestate.ResourceDeleted { + metrics.DeleteSubsMetric(sub) + return nil + } + + res, err := s.subscriptionLister.Subscriptions(sub.GetNamespace()).Get(sub.GetName()) + if err != nil { + return err + } + + metrics.EmitSubMetric(res) + metrics.UpdateSubsSyncCounterStorage(res) + + reconciled, err := s.reconcilers.Reconcile(ctx, res.DeepCopy()) if err != nil { logger.WithError(err).Warn("an error was encountered during reconciliation") return err } + if !equality.Semantic.DeepEqual(res.Status, reconciled.Status) { + if _, err := s.client.OperatorsV1alpha1().Subscriptions(reconciled.GetNamespace()).UpdateStatus(ctx, reconciled, metav1.UpdateOptions{}); err != nil { + return err + } + } + + s.nsResolveQueue.Add(res.GetNamespace()) + logger.WithFields(logrus.Fields{ "state": fmt.Sprintf("%T", reconciled), }).Debug("reconciliation successful") @@ -211,12 +237,15 @@ func newSyncerWithConfig(ctx context.Context, config *syncerConfig) (kubestate.S } s := &subscriptionSyncer{ - logger: config.logger, - clock: config.clock, - reconcilers: config.reconcilers, - subscriptionCache: config.subscriptionInformer.GetIndexer(), - installPlanLister: config.lister.OperatorsV1alpha1().InstallPlanLister(), - sourceProvider: config.sourceProvider, + logger: config.logger, + client: config.client, + clock: config.clock, + reconcilers: config.reconcilers, + subscriptionCache: config.subscriptionInformer.GetIndexer(), + installPlanLister: config.lister.OperatorsV1alpha1().InstallPlanLister(), + subscriptionLister: config.lister.OperatorsV1alpha1().SubscriptionLister(), + sourceProvider: config.sourceProvider, + nsResolveQueue: config.nsResolveQueue, notify: func(event kubestate.ResourceEvent) { // Notify Subscriptions by enqueuing to the Subscription queue. config.subscriptionQueue.Add(event) @@ -225,15 +254,13 @@ func newSyncerWithConfig(ctx context.Context, config *syncerConfig) (kubestate.S // Build a reconciler chain from the default and configured reconcilers // Default reconcilers should always come first in the chain - defaultReconcilers := kubestate.ReconcilerChain{ + defaultReconcilers := ReconcilerChain{ &installPlanReconciler{ now: s.now, - client: config.client, installPlanLister: config.lister.OperatorsV1alpha1().InstallPlanLister(), }, &catalogHealthReconciler{ now: s.now, - client: config.client, catalogLister: config.lister.OperatorsV1alpha1().CatalogSourceLister(), registryReconcilerFactory: config.registryReconcilerFactory, globalCatalogNamespace: config.globalCatalogNamespace, diff --git a/pkg/controller/operators/catalog/subscription/syncer_test.go b/pkg/controller/operators/catalog/subscription/syncer_test.go index e1afd66313..550789af8b 100644 --- a/pkg/controller/operators/catalog/subscription/syncer_test.go +++ b/pkg/controller/operators/catalog/subscription/syncer_test.go @@ -3,20 +3,29 @@ package subscription import ( "context" "testing" + "time" + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/fake" "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions" + "github.com/stretchr/testify/require" + listerv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1" + "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubestate" ) func TestSync(t *testing.T) { - type fields struct { - syncer kubestate.Syncer - } type args struct { - event kubestate.ResourceEvent + sub *v1alpha1.Subscription + eventType kubestate.ResourceEventType } type want struct { err error @@ -24,22 +33,19 @@ func TestSync(t *testing.T) { tests := []struct { description string - fields fields args args want want }{ { description: "v1alpha1/OK", - fields: fields{ - syncer: &subscriptionSyncer{ - logger: logrus.New(), - }, - }, args: args{ - event: kubestate.NewResourceEvent( - kubestate.ResourceAdded, - &v1alpha1.Subscription{}, - ), + eventType: kubestate.ResourceAdded, + sub: &v1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-sub", + Namespace: "test-namespace", + }, + }, }, want: want{ err: nil, @@ -52,7 +58,33 @@ func TestSync(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - require.Equal(t, tt.want.err, tt.fields.syncer.Sync(ctx, tt.args.event)) + nsResolveQueue := workqueue.NewTypedRateLimitingQueue[any](workqueue.DefaultTypedControllerRateLimiter[any]()) + syncer := subscriptionSyncer{ + logger: logrus.New(), + subscriptionLister: createFakeSubscriptionLister(ctx, []runtime.Object{tt.args.sub}), + nsResolveQueue: nsResolveQueue, + } + + require.Equal(t, tt.want.err, syncer.Sync(ctx, kubestate.NewResourceEvent(tt.args.eventType, tt.args.sub))) }) } } + +func createFakeSubscriptionLister(ctx context.Context, existingObjs []runtime.Object) listerv1alpha1.SubscriptionLister { + // Create client fakes + clientFake := fake.NewReactionForwardingClientsetDecorator(existingObjs) + wakeupInterval := 5 * time.Minute + + // Create informers and register listers + operatorsFactory := externalversions.NewSharedInformerFactoryWithOptions(clientFake, wakeupInterval, externalversions.WithNamespace(metav1.NamespaceAll)) + subInformer := operatorsFactory.Operators().V1alpha1().Subscriptions() + + stopChan := make(chan struct{}) + go func() { + <-ctx.Done() + close(stopChan) + }() + go subInformer.Informer().Run(stopChan) + cache.WaitForCacheSync(stopChan, subInformer.Informer().HasSynced) + return subInformer.Lister() +} diff --git a/pkg/controller/operators/catalog/subscriptions_test.go b/pkg/controller/operators/catalog/subscriptions_test.go index c21dc1a01b..2aa5ece9de 100644 --- a/pkg/controller/operators/catalog/subscriptions_test.go +++ b/pkg/controller/operators/catalog/subscriptions_test.go @@ -46,7 +46,7 @@ func TestSyncSubscriptions(t *testing.T) { existingOLMObjs []runtime.Object } type args struct { - obj interface{} + obj *v1alpha1.Subscription } tests := []struct { name string @@ -56,13 +56,6 @@ func TestSyncSubscriptions(t *testing.T) { wantInstallPlans []v1alpha1.InstallPlan wantSubscriptions []*v1alpha1.Subscription }{ - { - name: "BadObject", - args: args{ - obj: &v1alpha1.ClusterServiceVersion{}, - }, - wantErr: fmt.Errorf("casting Subscription failed"), - }, { name: "NoStatus/NoCurrentCSV/MissingCatalogSourceNamespace", fields: fields{ @@ -1313,11 +1306,11 @@ func TestSyncSubscriptions(t *testing.T) { Name: testNamespace, }, } - if err := o.syncSubscriptions(tt.args.obj); err != nil { - require.Equal(t, tt.wantErr, err) - } else { - require.Equal(t, tt.wantErr, o.syncResolvingNamespace(namespace)) - } + //if _, err := o.syncSubscriptions(ctx, tt.args.obj); err != nil { + // require.Equal(t, tt.wantErr, err) + //} else { + require.Equal(t, tt.wantErr, o.syncResolvingNamespace(namespace)) + //} for _, s := range tt.wantSubscriptions { fetched, err := o.client.OperatorsV1alpha1().Subscriptions(testNamespace).Get(context.TODO(), s.GetName(), metav1.GetOptions{}) diff --git a/pkg/controller/registry/resolver/source_csvs.go b/pkg/controller/registry/resolver/source_csvs.go index bc7fe506cf..8a876272f9 100644 --- a/pkg/controller/registry/resolver/source_csvs.go +++ b/pkg/controller/registry/resolver/source_csvs.go @@ -93,19 +93,19 @@ func (s *csvSource) Snapshot(ctx context.Context) (*cache.Snapshot, error) { continue } - if cachedSubscription, ok := csvSubscriptions[csv]; !ok || cachedSubscription == nil { - // we might be in an incoherent state, so let's check with live clients to make sure - realSubscriptions, err := s.listSubscriptions(ctx) - if err != nil { - return nil, fmt.Errorf("failed to list subscriptions: %w", err) - } - for _, realSubscription := range realSubscriptions.Items { - if realSubscription.Status.InstalledCSV == csv.Name { - // oops, live cluster state is coherent - return nil, fmt.Errorf("lister caches incoherent for CSV %s/%s - found owning Subscription %s/%s", csv.Namespace, csv.Name, realSubscription.Namespace, realSubscription.Name) - } - } - } + //if cachedSubscription, ok := csvSubscriptions[csv]; !ok || cachedSubscription == nil { + // // we might be in an incoherent state, so let's check with live clients to make sure + // realSubscriptions, err := s.listSubscriptions(ctx) + // if err != nil { + // return nil, fmt.Errorf("failed to list subscriptions: %w", err) + // } + // for _, realSubscription := range realSubscriptions.Items { + // if realSubscription.Status.InstalledCSV == csv.Name { + // // oops, live cluster state is coherent + // return nil, fmt.Errorf("lister caches incoherent for CSV %s/%s - found owning Subscription %s/%s", csv.Namespace, csv.Name, realSubscription.Namespace, realSubscription.Name) + // } + // } + //} if failForwardEnabled { replacementChainEndsInFailure, err := isReplacementChainThatEndsInFailure(csv, ReplacementMapping(csvs)) diff --git a/pkg/lib/kubestate/kubestate.go b/pkg/lib/kubestate/kubestate.go index 3f656069de..348f469a7e 100644 --- a/pkg/lib/kubestate/kubestate.go +++ b/pkg/lib/kubestate/kubestate.go @@ -110,29 +110,6 @@ type deletedState struct { func (d deletedState) isDeletedState() {} -type Reconciler interface { - Reconcile(ctx context.Context, in State) (out State, err error) -} - -type ReconcilerFunc func(ctx context.Context, in State) (out State, err error) - -func (r ReconcilerFunc) Reconcile(ctx context.Context, in State) (out State, err error) { - return r(ctx, in) -} - -type ReconcilerChain []Reconciler - -func (r ReconcilerChain) Reconcile(ctx context.Context, in State) (out State, err error) { - out = in - for _, rec := range r { - if out, err = rec.Reconcile(ctx, out); err != nil || out == nil || out.Terminal() { - break - } - } - - return -} - // ResourceEventType tells an operator what kind of event has occurred on a given resource. type ResourceEventType string diff --git a/test/e2e/subscription_e2e_test.go b/test/e2e/subscription_e2e_test.go index d8fa6f023d..44d7e5e8fd 100644 --- a/test/e2e/subscription_e2e_test.go +++ b/test/e2e/subscription_e2e_test.go @@ -3298,7 +3298,7 @@ func fetchSubscription(crc versioned.Interface, namespace, name string, checker var lastCSV string var lastInstallPlanRef *corev1.ObjectReference - err := wait.Poll(pollInterval, pollDuration, func() (bool, error) { + err := wait.PollUntilContextTimeout(context.TODO(), pollInterval, pollDuration, true, func(ctx context.Context) (bool, error) { var err error fetchedSubscription, err = crc.OperatorsV1alpha1().Subscriptions(namespace).Get(context.Background(), name, metav1.GetOptions{}) if err != nil || fetchedSubscription == nil { @@ -3308,14 +3308,15 @@ func fetchSubscription(crc versioned.Interface, namespace, name string, checker thisState, thisCSV, thisInstallPlanRef := fetchedSubscription.Status.State, fetchedSubscription.Status.CurrentCSV, fetchedSubscription.Status.InstallPlanRef if thisState != lastState || thisCSV != lastCSV || !equality.Semantic.DeepEqual(thisInstallPlanRef, lastInstallPlanRef) { lastState, lastCSV, lastInstallPlanRef = thisState, thisCSV, thisInstallPlanRef - log(fmt.Sprintf("subscription %s/%s state: %s (csv %s): installPlanRef: %#v", namespace, name, thisState, thisCSV, thisInstallPlanRef)) - log(fmt.Sprintf("subscription %s/%s state: %s (csv %s): status: %#v", namespace, name, thisState, thisCSV, fetchedSubscription.Status)) + log(fmt.Sprintf("subscription %s/%s state: %s (csv %s): installPlanRef: %s", namespace, name, thisState, thisCSV, toJSON(thisInstallPlanRef))) + log(fmt.Sprintf("subscription %s/%s state: %s (csv %s): status: %s", namespace, name, thisState, thisCSV, toJSON(fetchedSubscription.Status))) } return checker(fetchedSubscription), nil }) if err != nil { - log(fmt.Sprintf("subscription %s/%s never got correct status: %#v", namespace, name, fetchedSubscription.Status)) - log(fmt.Sprintf("subscription %s/%s spec: %#v", namespace, name, fetchedSubscription.Spec)) + log(fmt.Sprintf("subscription %s/%s never got correct status: %s", namespace, name, toJSON(fetchedSubscription.Status))) + log(fmt.Sprintf("subscription %s/%s spec: %s", namespace, name, toJSON(fetchedSubscription.Spec))) + log(fmt.Sprintf("error %v", err)) return nil, err } return fetchedSubscription, nil diff --git a/test/e2e/util.go b/test/e2e/util.go index 1241113bec..bdb37747b9 100644 --- a/test/e2e/util.go +++ b/test/e2e/util.go @@ -3,6 +3,7 @@ package e2e import ( "bytes" "context" + "encoding/json" "fmt" "os" "regexp" @@ -63,6 +64,14 @@ var ( nonAlphaNumericRegexp = regexp.MustCompile(`[^a-zA-Z0-9]`) ) +func toJSON(obj interface{}) string { + b, err := json.Marshal(obj) + if err != nil { + return "" + } + return string(b) +} + // newKubeClient configures a client to talk to the cluster defined by KUBECONFIG func newKubeClient() operatorclient.ClientInterface { return ctx.Ctx().KubeClient()