Skip to content

Commit 14ce50e

Browse files
authored
Add registry replication count value (Azure#26932)
* schema changes an initial support class changes * context switch changes * swagger changes * update tests and add tests for replicated ids * nits * fix lint issues * linting pass * pylint second pass * fix merge * get e2e test passing again after merge * more testing * remove pdb * linting * comment nits * rerun test recordings after main merge * remove description * schema deserialization fix * fix lint * add replication count check at conversion
1 parent 1f6b376 commit 14ce50e

16 files changed

+1618
-402
lines changed

sdk/ml/azure-ai-ml/azure/ai/ml/_schema/registry/system_created_storage_account.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
@experimental
1616
class SystemCreatedStorageAccountSchema(metaclass=PatchedSchemaMeta):
1717
arm_resource_id = fields.Str(dump_only=True)
18-
storage_account_hns = fields.Bool()
18+
storage_account_hns = fields.Bool(load_default=False)
1919
storage_account_type = StringTransformedEnum(
20+
load_default=StorageAccountType.STANDARD_LRS,
2021
allowed_values=[accountType.value for accountType in StorageAccountType], casing_transform=lambda x: x.lower()
2122
)
23+
replication_count = fields.Int(load_default=1, validate=lambda count : count > 0)
24+
replicated_ids = fields.List(fields.Str(), dump_only=True)
2225

2326
@post_load
2427
def make(self, data, **kwargs):

sdk/ml/azure-ai-ml/azure/ai/ml/entities/_registry/registry.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from azure.ai.ml.entities._util import load_from_dict
2222
from azure.ai.ml._utils._experimental import experimental
2323

24-
from .registry_support_classes import RegistryRegionDetails, SystemCreatedStorageAccount
24+
from .registry_support_classes import RegistryRegionDetails
2525

2626
CONTAINER_REGISTRY = "container_registry"
2727
REPLICATION_LOCATIONS = "replication_locations"
@@ -34,7 +34,6 @@ def __init__(
3434
name: str,
3535
location: str,
3636
identity: IdentityConfiguration = None,
37-
description: str = None,
3837
tags: Dict[str, str] = None,
3938
public_network_access: str = None,
4039
discovery_url: str = None,
@@ -52,8 +51,6 @@ def __init__(
5251
:type location: str
5352
:param identity: registry's System Managed Identity
5453
:type identity: ManagedServiceIdentity
55-
:param description: Description of the registry.
56-
:type description: str
5754
:param tags: Tags of the registry.
5855
:type tags: dict
5956
:param public_network_access: Whether to allow public endpoint connectivity.
@@ -72,7 +69,7 @@ def __init__(
7269
:type kwargs: dict
7370
"""
7471

75-
super().__init__(name=name, description=description, tags=tags, **kwargs)
72+
super().__init__(name=name, tags=tags, **kwargs)
7673

7774
# self.display_name = name # Do we need a top-level visible name value?
7875
self.location = location
@@ -118,15 +115,6 @@ def _to_dict(self) -> Dict:
118115
if self.replication_locations[0].acr_config and len(self.replication_locations[0].acr_config) > 0:
119116
self.container_registry = self.replication_locations[0].acr_config[0]
120117

121-
# Change single-list managed storage accounts to not be lists.
122-
# Although storage accounts are storage in a list to match the
123-
# underlying API, users should only enter in one managed storage
124-
# in YAML.
125-
for region_detail in self.replication_locations:
126-
if region_detail.storage_config and isinstance(
127-
region_detail.storage_config[0], SystemCreatedStorageAccount
128-
):
129-
region_detail.storage_config = region_detail.storage_config[0]
130118
return schema.dump(self)
131119

132120
@classmethod
@@ -166,7 +154,6 @@ def _from_rest_object(cls, rest_obj: RestRegistry) -> "Registry":
166154
identity = IdentityConfiguration._from_rest_object(rest_obj.identity)
167155
return Registry(
168156
name=rest_obj.name,
169-
description=real_registry.description,
170157
identity=identity,
171158
id=rest_obj.id,
172159
tags=rest_obj.tags,
@@ -200,10 +187,6 @@ def _convert_yaml_dict_to_entity_input(
200187
if global_acr_exists:
201188
if not hasattr(region_detail, "acr_details") or len(region_detail.acr_details) == 0:
202189
region_detail.acr_config = [acr_input]
203-
# Convert single, non-list managed storage into a 1-element list.
204-
if hasattr(region_detail, "storage_config") and isinstance(region_detail.storage_config, \
205-
SystemCreatedStorageAccount):
206-
region_detail.storage_config = [region_detail.storage_config]
207190

208191

209192
def _to_rest_object(self) -> RestRegistry:
@@ -227,7 +210,6 @@ def _to_rest_object(self) -> RestRegistry:
227210
location=self.location,
228211
identity=identity,
229212
tags=self.tags,
230-
description=self.description,
231213
properties=RegistryProperties(
232214
public_network_access=self.public_network_access,
233215
discovery_url=self.discovery_url,

sdk/ml/azure-ai-ml/azure/ai/ml/entities/_registry/registry_support_classes.py

Lines changed: 106 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@
55

66
from typing import List, Union
77

8+
from functools import reduce
9+
from copy import deepcopy
810
from azure.ai.ml._restclient.v2022_10_01_preview.models import (
911
AcrDetails as RestAcrDetails,
1012
ArmResourceId as RestArmResourceId,
1113
RegistryRegionArmDetails as RestRegistryRegionArmDetails,
1214
StorageAccountDetails as RestStorageAccountDetails,
1315
SystemCreatedAcrAccount as RestSystemCreatedAcrAccount,
1416
SystemCreatedStorageAccount as RestSystemCreatedStorageAccount,
15-
UserCreatedAcrAccount as RestUserCreatedAcrAccount,
16-
UserCreatedStorageAccount as RestUserCreatedStorageAccount)
17+
UserCreatedAcrAccount as RestUserCreatedAcrAccount)
1718
from azure.ai.ml._utils._experimental import experimental
1819
from azure.ai.ml.constants._registry import StorageAccountType
19-
20+
from azure.ai.ml.exceptions import ErrorCategory, ErrorTarget, ValidationErrorType, ValidationException
21+
from azure.ai.ml._exception_helper import log_and_raise_error
22+
from .util import _make_rest_user_storage_from_id
2023

2124
# This exists despite not being used by the schema validator because this entire
2225
# class is an output only value from the API.
@@ -93,6 +96,8 @@ def __init__(
9396
storage_account_hns: bool,
9497
storage_account_type: StorageAccountType,
9598
arm_resource_id: str = None,
99+
replicated_ids: List[str] = None,
100+
replication_count = 1,
96101
):
97102
"""
98103
:param arm_resource_id: Resource ID of the storage account.
@@ -104,58 +109,19 @@ def __init__(
104109
"Standard_GRS, "Standard_RAGRS", "Standard_ZRS", "Standard_GZRS",
105110
"Standard_RAGZRS", "Premium_LRS", "Premium_ZRS"
106111
:type storage_account_type: StorageAccountType
112+
:param replication_count: The number of replicas of this storage account
113+
that should be created. Defaults to 1. Values less than 1 are invalid.
114+
:type replication_count: int
115+
:param replicated_ids: If this storage was replicated, then this is a
116+
list of all storage IDs with these settings for this registry.
117+
Defaults to none for un-replicated storage accounts.
118+
:type replicated_ids: List[str]
107119
"""
108120
self.arm_resource_id = arm_resource_id
109121
self.storage_account_hns = storage_account_hns
110122
self.storage_account_type = storage_account_type
111-
112-
# storage should technically be a union between str and SystemCreatedStorageAccount,
113-
# but python doesn't accept self class references apparently.
114-
# Class method instead of normal function to accept possible
115-
# string input.
116-
117-
@classmethod
118-
def _to_rest_object(cls, storage) -> RestStorageAccountDetails:
119-
if hasattr(storage, "storage_account_type") and storage.storage_account_type is not None:
120-
121-
# We DO NOT want to set the arm_resource_id. The backend provides very
122-
# unhelpful errors if you provide an empty/null/invalid resource ID,
123-
# and ignores the value otherwise. It's better to avoid setting it in
124-
# the conversion in this direction at all.
125-
# We don't bother processing storage_account_type because the
126-
# rest version is case insensitive.
127-
account = RestSystemCreatedStorageAccount(
128-
storage_account_hns_enabled=storage.storage_account_hns,
129-
storage_account_type=storage.storage_account_type,
130-
)
131-
return RestStorageAccountDetails(system_created_storage_account=account)
132-
else:
133-
return RestStorageAccountDetails(
134-
user_created_storage_account=RestUserCreatedStorageAccount(
135-
arm_resource_id=RestArmResourceId(resource_id=storage)
136-
)
137-
)
138-
139-
@classmethod
140-
def _from_rest_object(cls, rest_obj: RestStorageAccountDetails) -> "Union[str, SystemCreatedStorageAccount]":
141-
if not rest_obj:
142-
return None
143-
# TODO should we even bother check if both values are set and throw an error? This shouldn't be possible.
144-
if rest_obj.system_created_storage_account:
145-
resource_id = None
146-
if rest_obj.system_created_storage_account.arm_resource_id:
147-
resource_id = rest_obj.system_created_storage_account.arm_resource_id.resource_id
148-
return SystemCreatedStorageAccount(
149-
storage_account_hns=rest_obj.system_created_storage_account.storage_account_hns_enabled,
150-
storage_account_type=StorageAccountType(
151-
rest_obj.system_created_storage_account.storage_account_type.lower()
152-
), # TODO validate storage account type?
153-
arm_resource_id=resource_id,
154-
)
155-
elif rest_obj.user_created_storage_account:
156-
return rest_obj.user_created_storage_account.arm_resource_id.resource_id
157-
else:
158-
return None # TODO should this throw an error instead?
123+
self.replication_count = replication_count
124+
self.replicated_ids = replicated_ids
159125

160126

161127
# Per-region information for registries.
@@ -166,7 +132,7 @@ def __init__(
166132
*,
167133
acr_config: List[Union[str, SystemCreatedAcrAccount]] = None,
168134
location: str = None,
169-
storage_config: List[Union[str, SystemCreatedStorageAccount]] = None,
135+
storage_config: Union[List[str], SystemCreatedStorageAccount] = None,
170136
):
171137
"""
172138
Details for each region a registry is in.
@@ -197,10 +163,12 @@ def _from_rest_object(cls, rest_obj: RestRegistryRegionArmDetails) -> "RegistryR
197163
acr) for acr in rest_obj.acr_details]
198164
storages = []
199165
if rest_obj.storage_account_details:
200-
storages = [SystemCreatedStorageAccount._from_rest_object(
201-
storages) for storages in rest_obj.storage_account_details]
166+
storages = cls._storage_config_from_rest_object(rest_obj.storage_account_details)
167+
202168
return RegistryRegionDetails(
203-
acr_config=converted_acr_details, location=rest_obj.location, storage_config=storages
169+
acr_config=converted_acr_details,
170+
location=rest_obj.location,
171+
storage_config=storages
204172
)
205173

206174
def _to_rest_object(self) -> RestRegistryRegionArmDetails:
@@ -210,10 +178,91 @@ def _to_rest_object(self) -> RestRegistryRegionArmDetails:
210178
acr) for acr in self.acr_config]
211179
storages = []
212180
if self.storage_config:
213-
storages = [SystemCreatedStorageAccount._to_rest_object(
214-
storage) for storage in self.storage_config]
181+
storages = self._storage_config_to_rest_object()
215182
return RestRegistryRegionArmDetails(
216183
acr_details=converted_acr_details,
217184
location=self.location,
218185
storage_account_details=storages,
219186
)
187+
188+
189+
def _storage_config_to_rest_object(self) -> List[RestStorageAccountDetails]:
190+
storage = self.storage_config
191+
# storage_config can either be a single system-created storage account,
192+
# or list of user-inputted id's.
193+
if (storage is not None
194+
and hasattr(storage, "storage_account_type")
195+
and storage.storage_account_type is not None):
196+
197+
# We DO NOT want to set the arm_resource_id. The backend provides very
198+
# unhelpful errors if you provide an empty/null/invalid resource ID,
199+
# and ignores the value otherwise. It's better to avoid setting it in
200+
# the conversion in this direction at all.
201+
# We don't bother processing storage_account_type because the
202+
# rest version is case insensitive.
203+
account = RestStorageAccountDetails(system_created_storage_account=RestSystemCreatedStorageAccount(
204+
storage_account_hns_enabled=storage.storage_account_hns,
205+
storage_account_type=storage.storage_account_type,
206+
))
207+
# duplicate this value based on the replication_count
208+
count = storage.replication_count
209+
if count < 1:
210+
raise ValueError(f"Replication count cannot be less than 1. Value was: {count}.")
211+
return [deepcopy(account) for _ in range(0, count)]
212+
elif storage is not None and len(storage) > 0:
213+
return [_make_rest_user_storage_from_id(user_id=user_id) for user_id in storage]
214+
else:
215+
return []
216+
217+
@classmethod
218+
def _storage_config_from_rest_object(
219+
cls,
220+
rest_configs: List[RestStorageAccountDetails]
221+
) -> Union[List[str], SystemCreatedStorageAccount]:
222+
if not rest_configs:
223+
return None
224+
num_configs = len(rest_configs)
225+
if num_configs == 0:
226+
return None
227+
system_created_count = reduce(lambda x, y: int(x) + int(y),
228+
[hasattr(config, "system_created_storage_account") and config.system_created_storage_account is not None
229+
for config in rest_configs])
230+
# configs should be mono-typed. Either they're all system created
231+
# or all user created.
232+
if system_created_count == num_configs:
233+
# System created case - assume all elements are duplicates
234+
# of a single storage configuration.
235+
# Convert back into a single local representation by
236+
# combining id's into a list, and using the first element's
237+
# account type and hns.
238+
first_config = rest_configs[0].system_created_storage_account
239+
resource_id = None
240+
if first_config.arm_resource_id:
241+
resource_id = first_config.arm_resource_id.resource_id
242+
# account for ids of duplicated if they exist
243+
replicated_ids = None
244+
if num_configs > 1:
245+
replicated_ids = [config.system_created_storage_account.arm_resource_id.resource_id
246+
for config in rest_configs]
247+
return SystemCreatedStorageAccount(
248+
storage_account_hns=first_config.storage_account_hns_enabled,
249+
storage_account_type=StorageAccountType(
250+
first_config.storage_account_type.lower()
251+
), # TODO validate storage account type? GI
252+
arm_resource_id=resource_id,
253+
replication_count=num_configs,
254+
replicated_ids=replicated_ids
255+
)
256+
elif system_created_count == 0:
257+
return [config.user_created_storage_account.arm_resource_id.resource_id for config in rest_configs]
258+
else:
259+
msg = f'''tried reading in a registry whose storage accounts were not
260+
mono-managed or user-created. {system_created_count} out of {num_configs} were managed.'''
261+
err = ValidationException(
262+
message=msg,
263+
target=ErrorTarget.REGISTRY,
264+
no_personal_data_message=msg,
265+
error_category=ErrorCategory.USER_ERROR,
266+
error_type=ValidationErrorType.INVALID_VALUE,
267+
)
268+
log_and_raise_error(err)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# ---------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# ---------------------------------------------------------
4+
# pylint:disable=protected-access,no-else-return
5+
6+
from azure.ai.ml._restclient.v2022_10_01_preview.models import (
7+
ArmResourceId as RestArmResourceId,
8+
StorageAccountDetails as RestStorageAccountDetails,
9+
UserCreatedStorageAccount as RestUserCreatedStorageAccount)
10+
11+
12+
def _make_rest_user_storage_from_id(*, user_id: str) -> RestStorageAccountDetails:
13+
return RestStorageAccountDetails(user_created_storage_account=RestUserCreatedStorageAccount(
14+
arm_resource_id=RestArmResourceId(resource_id=user_id)
15+
)
16+
)

0 commit comments

Comments
 (0)