From 4abafdb3c4a1d818791ce46503393cf07dd8c800 Mon Sep 17 00:00:00 2001 From: Jon Jackson Date: Thu, 25 Jun 2026 13:39:25 -0400 Subject: [PATCH] OCPBUGS-79520: Fix kebab actions on Installed Operators list page subscriptionForCSV had unnecessary guards checking operatorNamespace and csvName were truthy before matching. When operatorNamespaceFor returned undefined and fallback to csv.metadata.namespace produced falsy value, guard prevented matching even when valid subscription existed with same namespace and installedCSV. Removed operatorNamespace && csvName && guards. Kubernetes objects always have name/namespace, so guards unnecessary. Matching now works correctly with or without olm.operatorNamespace annotation. Added comprehensive test coverage for all matching scenarios including edge cases. Co-Authored-By: Claude Sonnet 4.5 --- .../src/status/__tests__/csv-status.spec.ts | 145 ++++++++++++++++++ .../src/status/csv-status.ts | 7 +- 2 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 frontend/packages/operator-lifecycle-manager/src/status/__tests__/csv-status.spec.ts diff --git a/frontend/packages/operator-lifecycle-manager/src/status/__tests__/csv-status.spec.ts b/frontend/packages/operator-lifecycle-manager/src/status/__tests__/csv-status.spec.ts new file mode 100644 index 00000000000..1a4155f42c5 --- /dev/null +++ b/frontend/packages/operator-lifecycle-manager/src/status/__tests__/csv-status.spec.ts @@ -0,0 +1,145 @@ +import type { ClusterServiceVersionKind, SubscriptionKind } from '../../types'; +import { ClusterServiceVersionPhase, CSVConditionReason } from '../../types'; +import { subscriptionForCSV } from '../csv-status'; + +describe('subscriptionForCSV', () => { + const csvNamespace = 'test-namespace'; + const csvName = 'test-operator.v1.0.0'; + + const createCSV = ( + name: string, + namespace: string, + operatorNamespace?: string, + ): ClusterServiceVersionKind => ({ + apiVersion: 'operators.coreos.com/v1alpha1', + kind: 'ClusterServiceVersion', + metadata: { + name, + namespace, + ...(operatorNamespace && { + annotations: { 'olm.operatorNamespace': operatorNamespace }, + }), + }, + spec: { + install: { + strategy: 'Deployment', + spec: { + permissions: [], + deployments: [], + }, + }, + }, + status: { + phase: ClusterServiceVersionPhase.CSVPhaseSucceeded, + reason: CSVConditionReason.CSVReasonInstallSuccessful, + }, + }); + + const createSubscription = ( + name: string, + namespace: string, + installedCSV: string, + ): SubscriptionKind => ({ + apiVersion: 'operators.coreos.com/v1alpha1', + kind: 'Subscription', + metadata: { + name, + namespace, + }, + spec: { + source: 'test-catalog', + name: 'test-package', + }, + status: { + installedCSV, + }, + }); + + it('should match subscription when operator namespace annotation present', () => { + const csv = createCSV(csvName, csvNamespace, csvNamespace); + const subscription = createSubscription('test-sub', csvNamespace, csvName); + const subscriptions = [subscription]; + + const result = subscriptionForCSV(subscriptions, csv); + + expect(result).toBe(subscription); + }); + + it('should match subscription when operator namespace annotation missing', () => { + const csv = createCSV(csvName, csvNamespace); + const subscription = createSubscription('test-sub', csvNamespace, csvName); + const subscriptions = [subscription]; + + const result = subscriptionForCSV(subscriptions, csv); + + expect(result).toBe(subscription); + }); + + it('should not match subscription with different namespace', () => { + const csv = createCSV(csvName, csvNamespace); + const subscription = createSubscription('test-sub', 'other-namespace', csvName); + const subscriptions = [subscription]; + + const result = subscriptionForCSV(subscriptions, csv); + + expect(result).toBeUndefined(); + }); + + it('should not match subscription with different installedCSV', () => { + const csv = createCSV(csvName, csvNamespace); + const subscription = createSubscription('test-sub', csvNamespace, 'other-operator.v1.0.0'); + const subscriptions = [subscription]; + + const result = subscriptionForCSV(subscriptions, csv); + + expect(result).toBeUndefined(); + }); + + it('should match subscription when operator namespace annotation differs from CSV namespace', () => { + const operatorNs = 'operator-namespace'; + const csv = createCSV(csvName, csvNamespace, operatorNs); + const subscription = createSubscription('test-sub', operatorNs, csvName); + const subscriptions = [subscription]; + + const result = subscriptionForCSV(subscriptions, csv); + + expect(result).toBe(subscription); + }); + + it('should return undefined when subscriptions array is empty', () => { + const csv = createCSV(csvName, csvNamespace); + const subscriptions: SubscriptionKind[] = []; + + const result = subscriptionForCSV(subscriptions, csv); + + expect(result).toBeUndefined(); + }); + + it('should return undefined when subscription status missing installedCSV', () => { + const csv = createCSV(csvName, csvNamespace); + const subscription = { + metadata: { + name: 'test-sub', + namespace: csvNamespace, + }, + spec: {}, + status: {}, + } as SubscriptionKind; + const subscriptions = [subscription]; + + const result = subscriptionForCSV(subscriptions, csv); + + expect(result).toBeUndefined(); + }); + + it('should match subscription when CSV has empty namespace but subscription exists', () => { + // Edge case: CSV with empty namespace string + const csv = createCSV(csvName, ''); + const subscription = createSubscription('test-sub', '', csvName); + const subscriptions = [subscription]; + + const result = subscriptionForCSV(subscriptions, csv); + + expect(result).toBe(subscription); + }); +}); diff --git a/frontend/packages/operator-lifecycle-manager/src/status/csv-status.ts b/frontend/packages/operator-lifecycle-manager/src/status/csv-status.ts index a427d2df193..4fdf17a0b96 100644 --- a/frontend/packages/operator-lifecycle-manager/src/status/csv-status.ts +++ b/frontend/packages/operator-lifecycle-manager/src/status/csv-status.ts @@ -26,12 +26,7 @@ export const subscriptionForCSV = ( return (subscriptions ?? []).find((subscription) => { const subscriptionNamespace = subscription.metadata?.namespace || ''; const installedCSV = subscription.status?.installedCSV || ''; - return ( - operatorNamespace && - csvName && - subscriptionNamespace === operatorNamespace && - installedCSV === csvName - ); + return subscriptionNamespace === operatorNamespace && installedCSV === csvName; }); };