Skip to content

Commit 78c2e36

Browse files
authored
[Quantum] Fix workspace create crash for providers without managed_application (#10415)
* [Quantum] Fix workspace create crash for providers without managed_application Bug 55823: az quantum workspace create crashed with AttributeError when a provider has no legacy marketplace ManagedApplication association. _get_publisher_and_offer_from_provider_id now treats managed_application=None the same as the existing N/A sentinel (no terms to accept) instead of crashing, reusing the already-correct downstream handling in _provider_terms_need_acceptance/_valid_publisher_and_offer. Adds unit tests for the fixed helper plus an integration-level test on _add_quantum_providers reproducing the exact crash scenario. * [Quantum] Handle missing managed application in offerings table
1 parent d106f8d commit 78c2e36

6 files changed

Lines changed: 143 additions & 7 deletions

File tree

‎src/quantum/HISTORY.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
Release History
44
===============
55

6+
1.0.0b30
7+
++++++++++++++
8+
* Fixed crashes in ``az quantum offerings list -o table`` and ``az quantum workspace create`` when a V2-only provider lacks a legacy marketplace ManagedApplication association.
9+
610
1.0.0b29
711
++++++++++++++
812
* Clarified which suite-offer and workspace quota commands apply to V2 providers and workspaces.

‎src/quantum/azext_quantum/commands.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from collections import OrderedDict
1111
from azure.cli.core.commands import CliCommandType
1212
from ._validators import validate_workspace_info, validate_workspace_user, validate_target_info, validate_workspace_and_target_info, validate_provider_and_sku_info
13+
from .operations.offerings import OFFER_NOT_AVAILABLE, PUBLISHER_NOT_AVAILABLE
1314

1415
logger = logging.getLogger(__name__)
1516

@@ -94,11 +95,14 @@ def transform_file_list(results):
9495

9596
def transform_offerings(offerings):
9697
def one(offering):
98+
properties = offering['properties']
99+
managed_application = properties.get('managedApplication') or {}
100+
97101
return OrderedDict([
98102
('Provider ID', offering['id']),
99-
('SKU', ', '.join([s['id'] for s in offering['properties']['skus']])),
100-
('Publisher ID', offering['properties']['managedApplication']['publisherId']),
101-
('Offer ID', offering['properties']['managedApplication']['offerId'])
103+
('SKU', ', '.join([s['id'] for s in properties['skus']])),
104+
('Publisher ID', managed_application.get('publisherId') or PUBLISHER_NOT_AVAILABLE),
105+
('Offer ID', managed_application.get('offerId') or OFFER_NOT_AVAILABLE)
102106
])
103107

104108
return [one(offering) for offering in offerings]

‎src/quantum/azext_quantum/operations/offerings.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,15 @@ def _get_publisher_and_offer_from_provider_id(providers, provider_id):
3333
offer_id = None
3434
for p in providers:
3535
if p.id.lower() == provider_id.lower():
36-
offer_id = p.properties.managed_application.offer_id
37-
publisher_id = p.properties.managed_application.publisher_id
36+
managed_application = p.properties.managed_application
37+
if managed_application is None:
38+
# V2-only providers have no legacy marketplace ManagedApplication association;
39+
# treat the same as the existing "N/A" sentinel instead of raising.
40+
publisher_id = PUBLISHER_NOT_AVAILABLE
41+
offer_id = OFFER_NOT_AVAILABLE
42+
else:
43+
offer_id = managed_application.offer_id
44+
publisher_id = managed_application.publisher_id
3845
break
3946
return (publisher_id, offer_id)
4047

‎src/quantum/azext_quantum/tests/latest/test_quantum_offerings.py‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,118 @@
33
# Licensed under the MIT License. See License.txt in the project root for license information.
44
# --------------------------------------------------------------------------------------------
55

6+
import unittest
7+
from types import SimpleNamespace
8+
69
import pytest
710

811
from azure.cli.testsdk import ScenarioTest
912
from .utils import issue_cmd_with_param_missing
13+
from ...commands import transform_offerings
14+
from ...operations.offerings import (
15+
_get_publisher_and_offer_from_provider_id,
16+
PUBLISHER_NOT_AVAILABLE,
17+
OFFER_NOT_AVAILABLE,
18+
)
1019

1120

1221
class QuantumOfferingsScenarioTest(ScenarioTest):
1322

1423
def test_offerings_errors(self):
1524
issue_cmd_with_param_missing(self, "az quantum offerings accept-terms", "az quantum offerings accept-terms -p MyProviderId -k MySKU -l MyLocation\nOnce terms have been reviewed, accept the invoking this command.")
1625
issue_cmd_with_param_missing(self, "az quantum offerings show-terms", "az quantum offerings show-terms -p MyProviderId -k MySKU -l MyLocation\nUse a Provider Id and SKU from `az quantum offerings list` to review the terms.")
26+
27+
28+
class QuantumOfferingsUnitTest(unittest.TestCase):
29+
30+
def _make_provider(self, provider_id, managed_application):
31+
return SimpleNamespace(
32+
id=provider_id,
33+
properties=SimpleNamespace(managed_application=managed_application),
34+
)
35+
36+
def test_get_publisher_and_offer_from_provider_id_returns_values(self):
37+
providers = [
38+
self._make_provider(
39+
"rigetti",
40+
SimpleNamespace(publisher_id="rigetti-computing", offer_id="rigetti-azure-basic"),
41+
),
42+
]
43+
44+
self.assertEqual(
45+
_get_publisher_and_offer_from_provider_id(providers, "rigetti"),
46+
("rigetti-computing", "rigetti-azure-basic"),
47+
)
48+
49+
def test_get_publisher_and_offer_from_provider_id_matches_case_insensitively(self):
50+
providers = [
51+
self._make_provider(
52+
"Rigetti",
53+
SimpleNamespace(publisher_id="rigetti-computing", offer_id="rigetti-azure-basic"),
54+
),
55+
]
56+
57+
self.assertEqual(
58+
_get_publisher_and_offer_from_provider_id(providers, "RIGETTI"),
59+
("rigetti-computing", "rigetti-azure-basic"),
60+
)
61+
62+
def test_get_publisher_and_offer_from_provider_id_returns_none_when_not_found(self):
63+
providers = [
64+
self._make_provider(
65+
"rigetti",
66+
SimpleNamespace(publisher_id="rigetti-computing", offer_id="rigetti-azure-basic"),
67+
),
68+
]
69+
70+
self.assertEqual(
71+
_get_publisher_and_offer_from_provider_id(providers, "contoso-v2-provider"),
72+
(None, None),
73+
)
74+
75+
def test_get_publisher_and_offer_from_provider_id_handles_missing_managed_application(self):
76+
# V2-only providers (e.g. contoso-v2-provider) have no legacy marketplace ManagedApplication
77+
# association at all; this must not raise and should be treated like the existing
78+
# "N/A" sentinel (no terms to accept) instead of crashing.
79+
providers = [self._make_provider("contoso-v2-provider", None)]
80+
81+
self.assertEqual(
82+
_get_publisher_and_offer_from_provider_id(providers, "contoso-v2-provider"),
83+
(PUBLISHER_NOT_AVAILABLE, OFFER_NOT_AVAILABLE),
84+
)
85+
86+
def test_transform_offerings_handles_missing_managed_application(self):
87+
offerings = [
88+
{
89+
'id': 'rigetti',
90+
'properties': {
91+
'skus': [{'id': 'azure-basic-qvm-only-unlimited'}],
92+
'managedApplication': {
93+
'publisherId': 'rigetticoinc1644276861431',
94+
'offerId': 'rigetti-aq',
95+
},
96+
},
97+
},
98+
{
99+
'id': 'atom-dev',
100+
'properties': {
101+
'skus': [{'id': 'default'}],
102+
'managedApplication': None,
103+
},
104+
},
105+
]
106+
107+
table = transform_offerings(offerings)
108+
109+
self.assertEqual(len(table), 2)
110+
self.assertEqual(
111+
list(table[0].keys()),
112+
['Provider ID', 'SKU', 'Publisher ID', 'Offer ID'],
113+
)
114+
self.assertEqual(table[0]['Provider ID'], 'rigetti')
115+
self.assertEqual(table[0]['Publisher ID'], 'rigetticoinc1644276861431')
116+
self.assertEqual(table[0]['Offer ID'], 'rigetti-aq')
117+
self.assertEqual(table[1]['Provider ID'], 'atom-dev')
118+
self.assertEqual(table[1]['SKU'], 'default')
119+
self.assertEqual(table[1]['Publisher ID'], PUBLISHER_NOT_AVAILABLE)
120+
self.assertEqual(table[1]['Offer ID'], OFFER_NOT_AVAILABLE)

‎src/quantum/azext_quantum/tests/latest/test_quantum_workspace.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from ..._version_check_helper import check_version
2222
from ..._params import QuotaAction
2323
from ..._validators import validate_email, validate_workspace_user
24-
from ...operations.workspace import _apply_target_quotas, _require_v2_workspace, _validate_target_quota_bounds, _validate_storage_account, _autoadd_providers, create, _resolve_user_id, _list_user_workspace_role_assignments, _scope_distance, _select_user_workspace_role_assignment, add_user, remove_user, list_users, update, QUANTUM_WORKSPACE_DATA_CONTRIBUTOR_ROLE_ID, QUANTUM_WORKSPACE_OWNER_ROLE_ID, SUPPORTED_STORAGE_SKU_TIERS, SUPPORTED_STORAGE_KINDS, DEPLOYMENT_NAME_PREFIX
24+
from ...operations.workspace import _apply_target_quotas, _require_v2_workspace, _validate_target_quota_bounds, _validate_storage_account, _autoadd_providers, _add_quantum_providers, create, _resolve_user_id, _list_user_workspace_role_assignments, _scope_distance, _select_user_workspace_role_assignment, add_user, remove_user, list_users, update, QUANTUM_WORKSPACE_DATA_CONTRIBUTOR_ROLE_ID, QUANTUM_WORKSPACE_OWNER_ROLE_ID, SUPPORTED_STORAGE_SKU_TIERS, SUPPORTED_STORAGE_KINDS, DEPLOYMENT_NAME_PREFIX
2525
from ...operations.workspace import _merge_workspace_quotas
2626
from ...commands import transform_workspace_quotas
2727
from ...vendored_sdks.azure_mgmt_quantum.models import Provider, TargetQuotaAllocations
@@ -280,6 +280,23 @@ def test_workspace_user_v2(self):
280280

281281
class QuantumWorkspacesUnitTest(unittest.TestCase):
282282

283+
@patch('azext_quantum.operations.workspace.cf_offerings')
284+
def test_add_quantum_providers_handles_provider_without_managed_application(self, cf_offerings_mock):
285+
# Regression test: V2-only providers (e.g. contoso-v2-provider) can have no legacy marketplace
286+
# ManagedApplication association at all; workspace create must not crash for them.
287+
provider = SimpleNamespace(
288+
id='contoso-v2-provider',
289+
properties=SimpleNamespace(managed_application=None, skus=[]),
290+
)
291+
cf_offerings_mock.return_value.list.return_value = [provider]
292+
workspace = SimpleNamespace(location='westus', properties=SimpleNamespace(providers=[]))
293+
294+
_add_quantum_providers(Mock(), workspace, 'contoso-v2-provider/default', auto_accept=True, skip_autoadd=True)
295+
296+
self.assertEqual(len(workspace.properties.providers), 1)
297+
self.assertEqual(workspace.properties.providers[0].provider_id, 'contoso-v2-provider')
298+
self.assertEqual(workspace.properties.providers[0].provider_sku, 'default')
299+
283300
def test_run_cleanup_commands_attempts_all_commands(self):
284301
test_case = SimpleNamespace(cmd=Mock(side_effect=[RuntimeError('first cleanup failed'), None]))
285302
cleanup_commands = [('first resource', 'first command'), ('second resource', 'second command')]

‎src/quantum/setup.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# This version should match the latest entry in HISTORY.rst
1818
# Also, when updating this, please review the version used by the extension to
1919
# submit requests, which can be found at './azext_quantum/__init__.py'
20-
VERSION = '1.0.0b29'
20+
VERSION = '1.0.0b30'
2121

2222
# The full list of classifiers is available at
2323
# https://pypi.python.org/pypi?%3Aaction=list_classifiers

0 commit comments

Comments
 (0)