Skip to content

Commit b793f0e

Browse files
committed
fixes for idempotentence
1 parent 36f87aa commit b793f0e

3 files changed

Lines changed: 542 additions & 10 deletions

File tree

src/mas/devops/olm.py

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

test/src/test_olm.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ def test_crud_with_starting_csv():
124124

125125

126126
def test_crud_with_manual_approval_and_starting_csv():
127+
"""
128+
Test that when installPlanApproval is Manual and startingCSV is specified,
129+
the first InstallPlan is automatically approved to reach the startingCSV.
130+
This allows the initial installation to proceed without manual intervention.
131+
132+
Note: With Manual approval and startingCSV, the subscription state will be
133+
"UpgradePending" after installation (indicating newer versions are available
134+
but require manual approval), not "AtLatestKnown".
135+
"""
127136
namespace = "cli-fvt-5"
128137
subscription = olm.applySubscription(
129138
dynClient,
@@ -138,6 +147,15 @@ def test_crud_with_manual_approval_and_starting_csv():
138147
assert subscription.spec.installPlanApproval == "Manual"
139148
assert subscription.spec.startingCSV == "ibm-sls.v3.8.0"
140149

150+
# Verify that the subscription reached UpgradePending state
151+
# This confirms the InstallPlan was automatically approved and installed
152+
# UpgradePending indicates newer versions are available but require manual approval
153+
assert subscription.status.state == "UpgradePending"
154+
155+
# Verify the installed CSV matches the startingCSV
156+
installedCSV = subscription.status.installedCSV
157+
assert installedCSV == "ibm-sls.v3.8.0"
158+
141159
# When we install the ibm-sls subscription OLM will automatically create the ibm-truststore-mgr
142160
# subscription, but when we delete the subscription, OLM will not automatically remove the latter
143161
olm.deleteSubscription(dynClient, namespace, "ibm-sls")

0 commit comments

Comments
 (0)