@@ -125,6 +125,10 @@ def applySubscription(dynClient: DynamicClient, namespace: str, packageName: str
125125 Automatically detects default channel and catalog source from PackageManifest if not provided.
126126 Ensures an OperatorGroup exists before creating the subscription.
127127
128+ When installPlanApproval is set to "Manual" and a startingCSV is specified, this function will
129+ automatically approve the InstallPlan for the first-time installation to move to that startingCSV.
130+ Subsequent upgrades will still require manual approval.
131+
128132 Parameters:
129133 dynClient (DynamicClient): OpenShift Dynamic Client
130134 namespace (str): The namespace to create the subscription in
@@ -135,7 +139,8 @@ def applySubscription(dynClient: DynamicClient, namespace: str, packageName: str
135139 config (dict, optional): Additional subscription configuration. Defaults to None.
136140 installMode (str, optional): Install mode for the OperatorGroup. Defaults to "OwnNamespace".
137141 installPlanApproval (str, optional): Install plan approval mode ("Automatic" or "Manual"). Defaults to None.
138- startingCSV (str, optional): The specific CSV version to install. Defaults to None.
142+ startingCSV (str, optional): The specific CSV version to install. When combined with Manual approval,
143+ the first InstallPlan to this CSV will be automatically approved. Defaults to None.
139144
140145 Returns:
141146 Subscription: The created or updated subscription resource
@@ -204,6 +209,7 @@ def applySubscription(dynClient: DynamicClient, namespace: str, packageName: str
204209 logger .debug (f"Waiting for { packageName } .{ namespace } InstallPlans" )
205210 installPlanAPI = dynClient .resources .get (api_version = "operators.coreos.com/v1alpha1" , kind = "InstallPlan" )
206211
212+ # Use label selector to get InstallPlans (standard approach)
207213 installPlanResources = installPlanAPI .get (label_selector = labelSelector , namespace = namespace )
208214 while len (installPlanResources .items ) == 0 :
209215 installPlanResources = installPlanAPI .get (label_selector = labelSelector , namespace = namespace )
@@ -212,27 +218,114 @@ def applySubscription(dynClient: DynamicClient, namespace: str, packageName: str
212218 if len (installPlanResources .items ) == 0 :
213219 raise OLMException (f"Found 0 InstallPlans for { packageName } " )
214220 elif len (installPlanResources .items ) > 1 :
215- logger .warning (f"More than 1 InstallPlan found for { packageName } " )
221+ logger .warning (f"More than 1 InstallPlan found for { packageName } using label selector" )
222+
223+ # Select the InstallPlan to use
224+ installPlanResource = None
225+
226+ # Special handling for Manual approval with startingCSV
227+ if installPlanApproval == "Manual" and startingCSV is not None :
228+ logger .debug (f"Manual approval with startingCSV { startingCSV } - checking if label selector returned correct InstallPlan" )
229+
230+ # Check if any of the InstallPlans from label selector match the startingCSV
231+ for plan in installPlanResources .items :
232+ csvNames = getattr (plan .spec , "clusterServiceVersionNames" , [])
233+ logger .debug (f"InstallPlan { plan .metadata .name } (from label selector) contains CSVs: { csvNames } " )
234+ if csvNames and startingCSV in csvNames :
235+ installPlanResource = plan
236+ logger .info (f"Found InstallPlan { plan .metadata .name } matching startingCSV { startingCSV } via label selector" )
237+ break
238+
239+ # If no match found via label selector, search all InstallPlans owned by this subscription
240+ if installPlanResource is None :
241+ logger .warning (f"Label selector did not return InstallPlan matching startingCSV { startingCSV } " )
242+ logger .debug (f"Searching all InstallPlans in { namespace } owned by subscription { name } " )
243+
244+ allInstallPlans = installPlanAPI .get (namespace = namespace )
245+ for plan in allInstallPlans .items :
246+ # Check if this InstallPlan is owned by our subscription
247+ owner_refs = getattr (plan .metadata , 'ownerReferences' , [])
248+ is_owned_by_subscription = any (
249+ ref .kind == "Subscription" and ref .name == name
250+ for ref in owner_refs
251+ )
252+
253+ if is_owned_by_subscription :
254+ csvNames = getattr (plan .spec , "clusterServiceVersionNames" , [])
255+ logger .debug (f"InstallPlan { plan .metadata .name } (owned by subscription) contains CSVs: { csvNames } " )
256+ if csvNames and startingCSV in csvNames :
257+ installPlanResource = plan
258+ logger .info (f"Found InstallPlan { plan .metadata .name } matching startingCSV { startingCSV } via subscription ownership" )
259+ break
260+
261+ if installPlanResource is None :
262+ logger .warning (f"No InstallPlan found matching startingCSV { startingCSV } , using first from label selector" )
263+ installPlanResource = installPlanResources .items [0 ]
264+ else :
265+ # Standard case: use first InstallPlan from label selector
266+ installPlanResource = installPlanResources .items [0 ]
216267
217- installPlanName = installPlanResources .items [0 ].metadata .name
268+ installPlanName = installPlanResource .metadata .name
269+ installPlanPhase = installPlanResource .status .phase
218270
219- # Wait for InstallPlan to complete
220- logger .debug (f"Waiting for InstallPlan { installPlanName } " )
221- installPlanPhase = installPlanResources .items [0 ].status .phase
222- while installPlanPhase != "Complete" :
223- installPlanResource = installPlanAPI .get (name = installPlanName , namespace = namespace )
224- installPlanPhase = installPlanResource .status .phase
225- sleep (30 )
271+ # If the InstallPlan for our startingCSV is already Complete, we're done
272+ if installPlanPhase == "Complete" :
273+ logger .info (f"InstallPlan { installPlanName } for { startingCSV } is already Complete" )
274+ else :
275+ # Wait for InstallPlan to complete
276+ logger .debug (f"Waiting for InstallPlan { installPlanName } " )
277+
278+ # Track if we've already approved this install plan
279+ approved_manual_install = False
280+
281+ while installPlanPhase != "Complete" :
282+ installPlanResource = installPlanAPI .get (name = installPlanName , namespace = namespace )
283+ installPlanPhase = installPlanResource .status .phase
284+
285+ # If InstallPlan requires approval and this is the first installation to startingCSV
286+ if installPlanPhase == "RequiresApproval" and not approved_manual_install :
287+ # Check if this is the first installation by verifying the CSV matches startingCSV
288+ if startingCSV is not None :
289+ csvName = getattr (installPlanResource .spec , "clusterServiceVersionNames" , [])
290+ if csvName and startingCSV in csvName :
291+ logger .info (f"Approving InstallPlan { installPlanName } for first-time installation to { startingCSV } " )
292+ # Patch the InstallPlan to approve it
293+ installPlanResource .spec .approved = True
294+ installPlanAPI .patch (
295+ body = installPlanResource ,
296+ name = installPlanName ,
297+ namespace = namespace ,
298+ content_type = "application/merge-patch+json"
299+ )
300+ approved_manual_install = True
301+ logger .info (f"InstallPlan { installPlanName } approved successfully" )
302+ else :
303+ logger .debug (f"InstallPlan CSV { csvName } does not match startingCSV { startingCSV } , waiting for manual approval" )
304+ else :
305+ logger .debug (f"No startingCSV specified, InstallPlan { installPlanName } requires manual approval" )
306+
307+ sleep (30 )
226308
227309 # Wait for Subscription to complete
228310 logger .debug (f"Waiting for Subscription { name } in { namespace } " )
229311 while True :
230312 subscriptionResource = subscriptionsAPI .get (name = name , namespace = namespace )
231313 state = getattr (subscriptionResource .status , "state" , None )
232314
315+ # When manual approval is used with startingCSV, the state will be "UpgradePending"
316+ # after the initial installation completes (indicating newer versions are available
317+ # but require manual approval). For automatic approval, the state will be "AtLatestKnown".
233318 if state == "AtLatestKnown" :
234319 logger .debug (f"Subscription { name } in { namespace } reached state: { state } " )
235320 return subscriptionResource
321+ elif state == "UpgradePending" and installPlanApproval == "Manual" and startingCSV is not None :
322+ # Verify the installed CSV matches the startingCSV
323+ installedCSV = getattr (subscriptionResource .status , "installedCSV" , None )
324+ if installedCSV == startingCSV :
325+ logger .debug (f"Subscription { name } in { namespace } reached state: { state } with installedCSV: { installedCSV } " )
326+ return subscriptionResource
327+ else :
328+ logger .debug (f"Subscription { name } in { namespace } state is { state } but installedCSV ({ installedCSV } ) does not match startingCSV ({ startingCSV } ), retrying..." )
236329
237330 logger .debug (f"Subscription { name } in { namespace } not ready yet (state = { state } ), retrying..." )
238331 sleep (30 )
0 commit comments