Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions ibm/mas_devops/plugins/action/apply_subscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from mas.devops.olm import applySubscription, OLMException

urllib3.disable_warnings() # Disabling warnings will prevent InsecureRequestWarnings from dynClient
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(name)-20s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')


class ActionModule(ActionBase):
"""
Expand All @@ -22,6 +22,8 @@ class ActionModule(ActionBase):
package_name: ibm-sls
package_channel: 3.x
install_mode: AllNamespaces (default is OwnNamespace)
install_plan_approval: Manual (optional, default is Automatic)
starting_csv: ibm-sls.v3.8.0 (optional)
register: subscription
"""
def run(self, tmp=None, task_vars=None):
Expand All @@ -42,18 +44,41 @@ def run(self, tmp=None, task_vars=None):
# Which install mode?
installMode = self._task.args.get('install_mode', 'OwnNamespace')

# Install plan approval and starting CSV (optional)
installPlanApproval = self._task.args.get('install_plan_approval', None)
startingCSV = self._task.args.get('starting_csv', None)

if namespace is None:
raise AnsibleError(f"Error: namespace argument was not provided")
if not isinstance(packageName, str):
raise AnsibleError(f"Error: packageName argument is not a string")

# Validate that Manual approval requires a startingCSV for automatic first-time approval
if installPlanApproval == "Manual" and startingCSV is None:
raise AnsibleError(
f"Error: When install_plan_approval is set to 'Manual', a starting_csv must be provided "
f"to allow automatic approval of the initial installation. Without starting_csv, the "
f"InstallPlan will remain in 'RequiresApproval' state indefinitely."
)

# Initialize DynamicClient and apply the Subscription
host = self._task.args.get('host', None)
api_key = self._task.args.get('api_key', None)

dynClient = get_api_client(api_key=api_key, host=host)
try:
subscription = applySubscription(dynClient, namespace, packageName, packageChannel, catalogSource, catalogSourceNamespace, config, installMode)
subscription = applySubscription(
dynClient,
namespace,
packageName,
packageChannel,
catalogSource,
catalogSourceNamespace,
config,
installMode,
installPlanApproval=installPlanApproval,
startingCSV=startingCSV
)
except OLMException as e:
raise AnsibleError(f"Error applying subscription: {e}")

Expand Down
17 changes: 13 additions & 4 deletions ibm/mas_devops/plugins/action/verify_subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,21 @@ def run(self, tmp=None, task_vars=None):
notAtLatest = []

for subscription in subs.items:
display.v(f"* {subscription.metadata.namespace}/{subscription.metadata.name} = {subscription.status.state}")
if subscription.status.state != "AtLatestKnown":
state = subscription.status.state
installPlanApproval = getattr(subscription.spec, 'installPlanApproval', 'Automatic')
display.v(f"* {subscription.metadata.namespace}/{subscription.metadata.name} = {state} (approval: {installPlanApproval})")

# Accept both "AtLatestKnown" and "UpgradePending" with Manual approval as valid states
# UpgradePending with Manual approval means the operator is installed at the desired version
# but newer versions are available that require manual approval
isValidState = (state == "AtLatestKnown" or
(state == "UpgradePending" and installPlanApproval == "Manual"))

if not isValidState:
allSubscriptionsAtLatestThisLoop = False
notAtLatest.append(f"{subscription.metadata.namespace}/{subscription.metadata.name} = {subscription.status.state}")
notAtLatest.append(f"{subscription.metadata.namespace}/{subscription.metadata.name} = {state} (approval: {installPlanApproval})")
else:
atLatest.append(f"{subscription.metadata.namespace}/{subscription.metadata.name} = {subscription.status.state}")
atLatest.append(f"{subscription.metadata.namespace}/{subscription.metadata.name} = {state} (approval: {installPlanApproval})")

if allSubscriptionsAtLatestThisLoop:
allSubscriptionsAtLatest = True
Expand Down
4 changes: 4 additions & 0 deletions ibm/mas_devops/roles/grafana/tasks/install/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,15 @@

# 4. Create Grafana Subscription
# -----------------------------------------------------------------------------
# Setting to Manual and starting_csv: grafana-operator.v5.21.2 due to
# grafana bug https://github.com/grafana/grafana-operator/issues/2552
- name: "install : Create Grafana v{{grafana_major_version}} Subscription"
ibm.mas_devops.apply_subscription:
namespace: "{{ grafana_namespace }}"
package_name: grafana-operator
package_channel: "v{{grafana_major_version}}"
install_plan_approval: Manual
starting_csv: grafana-operator.v5.21.2
config:
env:
- name: "WATCH_NAMESPACE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,3 @@
- "Username: {{ atlas_db_username }}"
- "Status: {{ 'Created' if atlas_user_check.status == 404 else 'Already exists' }}"
- "=========================================="

Original file line number Diff line number Diff line change
Expand Up @@ -625,4 +625,3 @@
- "Connection String (SRV) ..... {{ atlas_standard_srv }}"
- "Database User ............... {{ atlas_db_username | default('(see atlas_database_users for multi-user credentials)') }}"
- "Cluster State ............... {{ atlas_cluster_status.json.stateName }}"

Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ def test_all_subscriptions_at_latest_first_attempt(self, action_module, mock_dis
assert result['message'] == "All Subscriptions are at the latest known operator version"
assert len(result['atLatest']) == 2
assert len(result['notAtLatest']) == 0
assert 'openshift-operators/ibm-mas-operator = AtLatestKnown' in result['atLatest']
assert 'openshift-operators/ibm-sls-operator = AtLatestKnown' in result['atLatest']
# Check that the subscriptions are in atLatest with approval information
assert any('openshift-operators/ibm-mas-operator = AtLatestKnown' in item for item in result['atLatest'])
assert any('openshift-operators/ibm-sls-operator = AtLatestKnown' in item for item in result['atLatest'])

def test_subscriptions_at_latest_after_retry(self, action_module, mock_display):
"""Test when Subscriptions reach latest after retry."""
Expand Down Expand Up @@ -303,5 +304,89 @@ def test_subscription_with_different_states(self, action_module, mock_display):
with patch('verify_subscriptions.time.sleep'):
with pytest.raises(AnsibleError, match="One or more subscriptions did not update"):
action_module.run()
def test_upgrade_pending_with_manual_approval_is_valid(self, action_module, mock_display):
"""Test that UpgradePending state with Manual approval is considered valid."""
# Arrange
action_module._task.args = {
'host': 'https://api.example.com',
'api_key': 'test-key',
'retries': 1,
'delay': 1
}

# Create subscription with UpgradePending and Manual approval
mock_sub1 = Mock()
mock_sub1.metadata.namespace = 'openshift-operators'
mock_sub1.metadata.name = 'ibm-mas-operator'
mock_sub1.status.state = 'UpgradePending'
mock_sub1.spec.installPlanApproval = 'Manual'

# Create subscription with AtLatestKnown
mock_sub2 = Mock()
mock_sub2.metadata.namespace = 'openshift-operators'
mock_sub2.metadata.name = 'ibm-sls-operator'
mock_sub2.status.state = 'AtLatestKnown'
mock_sub2.spec.installPlanApproval = 'Automatic'

mock_subs = Mock()
mock_subs.items = [mock_sub1, mock_sub2]

mock_sub_resource = Mock()
mock_sub_resource.get.return_value = mock_subs

mock_resources = Mock()
mock_resources.get.return_value = mock_sub_resource

mock_client = Mock()
mock_client.resources = mock_resources

# Act
with patch('verify_subscriptions.get_api_client', return_value=mock_client):
result = action_module.run()

# Assert
assert result['failed'] is False
assert result['changed'] is False
assert result['message'] == "All Subscriptions are at the latest known operator version"
assert len(result['atLatest']) == 2
assert len(result['notAtLatest']) == 0
assert 'openshift-operators/ibm-mas-operator = UpgradePending (approval: Manual)' in result['atLatest']
assert 'openshift-operators/ibm-sls-operator = AtLatestKnown (approval: Automatic)' in result['atLatest']

def test_upgrade_pending_with_automatic_approval_is_invalid(self, action_module, mock_display):
"""Test that UpgradePending state with Automatic approval is NOT considered valid."""
# Arrange
action_module._task.args = {
'host': 'https://api.example.com',
'api_key': 'test-key',
'retries': 1,
'delay': 1
}

# Create subscription with UpgradePending and Automatic approval (should fail)
mock_sub = Mock()
mock_sub.metadata.namespace = 'openshift-operators'
mock_sub.metadata.name = 'ibm-mas-operator'
mock_sub.status.state = 'UpgradePending'
mock_sub.spec.installPlanApproval = 'Automatic'

mock_subs = Mock()
mock_subs.items = [mock_sub]

mock_sub_resource = Mock()
mock_sub_resource.get.return_value = mock_subs

mock_resources = Mock()
mock_resources.get.return_value = mock_sub_resource

mock_client = Mock()
mock_client.resources = mock_resources

# Act & Assert
with patch('verify_subscriptions.get_api_client', return_value=mock_client):
with patch('verify_subscriptions.time.sleep'):
with pytest.raises(AnsibleError, match="One or more subscriptions did not update"):
action_module.run()


# Made with Bob
Loading