Skip to content
Closed
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
5 changes: 5 additions & 0 deletions src/spring/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
Release History
===============
1.18.0
---
* Add arguments `--bind-service-registry` in `spring app create`.
* Add arguments `--bind-application-configuration-service` in `spring app create`.

1.17.0
---
* Add arguments `--enable-api-try-out` in `spring api-portal update`
Expand Down
34 changes: 25 additions & 9 deletions src/spring/azext_spring/_app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def _format_properties(self, **kwargs):
kwargs['vnet_addons'] = self._load_vnet_addons(**kwargs)
kwargs['ingress_settings'] = self._load_ingress_settings(**kwargs)
kwargs['secrets'] = self._load_secrets_config(**kwargs)
kwargs['addon_configs'] = self._load_addon_configs(**kwargs)
return models.AppResourceProperties(**kwargs)

def _format_identity(self, system_assigned=None, user_assigned=None, **_):
Expand Down Expand Up @@ -113,13 +114,14 @@ def _load_custom_persistent_disks(self, client, resource_group, service, sku, pe
storage_resource = client.storages.get(resource_group, service, item['storageName'])
storage_id = storage_resource.id

custom_props = item['customPersistentDiskProperties']
custom_persistent_disk_properties = models.AzureFileVolume(
type=item['customPersistentDiskProperties']['type'],
share_name=item['customPersistentDiskProperties']['shareName'] if 'shareName' in item['customPersistentDiskProperties'] else None,
mount_path=item['customPersistentDiskProperties']['mountPath'],
mount_options=item['customPersistentDiskProperties']['mountOptions'] if 'mountOptions' in item['customPersistentDiskProperties'] else None,
read_only=item['customPersistentDiskProperties']['readOnly'] if 'readOnly' in item['customPersistentDiskProperties'] else None,
enable_sub_path=item['customPersistentDiskProperties']['enableSubPath'] if 'enableSubPath' in item['customPersistentDiskProperties'] else None)
type=custom_props['type'],
share_name=custom_props['shareName'] if 'shareName' in custom_props else None,
mount_path=custom_props['mountPath'],
mount_options=custom_props['mountOptions'] if 'mountOptions' in custom_props else None,
read_only=custom_props['readOnly'] if 'readOnly' in custom_props else None,
enable_sub_path=custom_props['enableSubPath'] if 'enableSubPath' in custom_props else None)

custom_persistent_disks.append(
models.CustomPersistentDiskResource(
Expand All @@ -135,7 +137,8 @@ def _load_vnet_addons(self, public_for_vnet=None, **_):
else:
return None

def _load_ingress_settings(self, ingress_read_timeout=None, ingress_send_timeout=None, session_affinity=None, session_max_age=None, backend_protocol=None, client_auth_certs=None, **_):
def _load_ingress_settings(self, ingress_read_timeout=None, ingress_send_timeout=None, session_affinity=None,
session_max_age=None, backend_protocol=None, client_auth_certs=None, **_):
if (ingress_read_timeout is not None) or (ingress_send_timeout is not None) or \
(session_affinity is not None) or (session_max_age is not None) or (backend_protocol is not None) or \
(client_auth_certs is not None):
Expand Down Expand Up @@ -164,7 +167,7 @@ def _load_secrets_config(self, secrets=None, **_):
raise InvalidArgumentValueError("Secrets must be in format \"<key>=<value> <key>=<value> ...\".")
if key_val[0] in secret_pairs:
raise InvalidArgumentValueError(
"Duplicate secret \"{secret}\" found, secret names must be unique.".format(secret=key_val[0]))
f"Duplicate secret \"{key_val[0]}\" found, secret names must be unique.")
secret_pairs[key_val[0]] = key_val[1]

secret_var_def = []
Expand All @@ -175,6 +178,18 @@ def _load_secrets_config(self, secrets=None, **_):

return secret_var_def

def _load_addon_configs(self, bind_service_registry=None, bind_application_configuration_service=None, **_):
if not bind_service_registry and not bind_application_configuration_service:
return None

addon_configs = {}

if bind_service_registry:
addon_configs['serviceRegistry'] = {'resourceId': bind_service_registry}
if bind_application_configuration_service:
addon_configs['applicationConfigurationService'] = {'resourceId': bind_application_configuration_service}
return addon_configs


class BasicTierApp(DefaultApp):
def _get_persistent_disk_size(self, enable_persistent_storage, **_):
Expand All @@ -184,7 +199,8 @@ def _get_persistent_disk_size(self, enable_persistent_storage, **_):
class EnterpriseTierApp(DefaultApp):
def _get_persistent_disk_size(self, enable_persistent_storage, **_):
if enable_persistent_storage:
raise InvalidArgumentValueError('Enterprise tier Spring instance does not support --enable-persistent-storage')
raise InvalidArgumentValueError('Enterprise tier Spring instance does not support '
'--enable-persistent-storage')


def app_selector(sku, **_):
Expand Down
30 changes: 21 additions & 9 deletions src/spring/azext_spring/_app_managed_identity_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@
logger = get_logger(__name__)


OBSOLETE_APP_IDENTITY_REMOVE = "Remove managed identities without \"system-assigned\" or \"user-assigned\" parameters is obsolete, will only remove system-assigned managed identity, and will not be supported in a future release."
WARNING_NO_USER_IDENTITY_RESOURCE_ID = "No resource ID of user-assigned managed identity is given for parameter \"user-assigned\", will remove ALL user-assigned managed identities."
OBSOLETE_APP_IDENTITY_ASSIGN = "Assign managed identities without \"system-assigned\" or \"user-assigned\" parameters is obsolete, will only enable system-assigned managed identity, and will not be supported in a future release."
OBSOLETE_APP_IDENTITY_REMOVE = ("Remove managed identities without \"system-assigned\" or \"user-assigned\" "
"parameters is obsolete, will only remove system-assigned managed identity, "
"and will not be supported in a future release.")
WARNING_NO_USER_IDENTITY_RESOURCE_ID = ("No resource ID of user-assigned managed identity is given for parameter "
"\"user-assigned\", will remove ALL user-assigned managed identities.")
OBSOLETE_APP_IDENTITY_ASSIGN = ("Assign managed identities without \"system-assigned\" or \"user-assigned\" "
"parameters is obsolete, will only enable system-assigned managed identity, "
"and will not be supported in a future release.")
ENABLE_LOWER = "enable"
DISABLE_LOWER = "disable"

Expand All @@ -24,14 +29,16 @@ def validate_app_identity_remove_or_warning(namespace):
logger.warning(OBSOLETE_APP_IDENTITY_REMOVE)
if namespace.user_assigned is not None:
if not isinstance(namespace.user_assigned, list):
raise InvalidArgumentValueError("Parameter value for \"user-assigned\" should be empty or a list of space-separated managed identity resource ID.")
raise InvalidArgumentValueError("Parameter value for \"user-assigned\" should be empty "
"or a list of space-separated managed identity resource ID.")
if len(namespace.user_assigned) == 0:
logger.warning(WARNING_NO_USER_IDENTITY_RESOURCE_ID)
namespace.user_assigned = _normalized_user_identitiy_resource_id_list(namespace.user_assigned)
for resource_id in namespace.user_assigned:
is_valid = _is_valid_user_assigned_managed_identity_resource_id(resource_id)
if not is_valid:
raise InvalidArgumentValueError("Invalid user-assigned managed identity resource ID \"{}\".".format(resource_id))
error_msg_template = "Invalid user-assigned managed identity resource ID \"{}\"."
raise InvalidArgumentValueError(error_msg_template.format(resource_id))


def _normalized_user_identitiy_resource_id_list(user_identity_resource_id_list):
Expand Down Expand Up @@ -71,7 +78,8 @@ def _validate_role_and_scope_should_use_together(namespace):

def _validate_role_and_scope_should_not_use_with_user_identity(namespace):
if _has_role_and_scope(namespace) and _only_has_user_assigned(namespace):
raise InvalidArgumentValueError("Invalid to use parameter \"role\" and \"scope\" with \"user-assigned\" parameter.")
raise InvalidArgumentValueError("Invalid to use parameter \"role\" and \"scope\" "
"with \"user-assigned\" parameter.")


def _has_role_and_scope(namespace):
Expand All @@ -90,7 +98,8 @@ def _validate_user_identity_resource_id(namespace):
if namespace.user_assigned:
for resource_id in namespace.user_assigned:
if not _is_valid_user_assigned_managed_identity_resource_id(resource_id):
raise InvalidArgumentValueError("Invalid user-assigned managed identity resource ID \"{}\".".format(resource_id))
error_msg_template = "Invalid user-assigned managed identity resource ID \"{}\"."
raise InvalidArgumentValueError(error_msg_template.format(resource_id))


def _normalize_user_identity_resource_id(namespace):
Expand Down Expand Up @@ -118,7 +127,8 @@ def validate_app_force_set_system_identity_or_warning(namespace):
raise InvalidArgumentValueError('Parameter "system-assigned" expected at least one argument.')
namespace.system_assigned = namespace.system_assigned.strip().lower()
if namespace.system_assigned.strip().lower() not in (ENABLE_LOWER, DISABLE_LOWER):
raise InvalidArgumentValueError('Allowed values for "system-assigned" are: {}, {}.'.format(ENABLE_LOWER, DISABLE_LOWER))
error_msg_template = 'Allowed values for "system-assigned" are: {}, {}.'
raise InvalidArgumentValueError(error_msg_template.format(ENABLE_LOWER, DISABLE_LOWER))


def validate_app_force_set_user_identity_or_warning(namespace):
Expand All @@ -127,7 +137,9 @@ def validate_app_force_set_user_identity_or_warning(namespace):
if len(namespace.user_assigned) == 1:
single_element = namespace.user_assigned[0].strip().lower()
if single_element != DISABLE_LOWER and not _is_valid_user_assigned_managed_identity_resource_id(single_element):
raise InvalidArgumentValueError('Allowed values for "user-assigned" are: {}, space-separated user-assigned managed identity resource IDs.'.format(DISABLE_LOWER))
error_msg_template = ('Allowed values for "user-assigned" are: {}, '
'space-separated user-assigned managed identity resource IDs.')
raise InvalidArgumentValueError(error_msg_template.format(DISABLE_LOWER))
elif single_element == DISABLE_LOWER:
namespace.user_assigned = [DISABLE_LOWER]
else:
Expand Down
12 changes: 6 additions & 6 deletions src/spring/azext_spring/_app_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,14 @@ def ensure_not_active_deployment(cmd, namespace):
client = cf_spring(cmd.cli_ctx)
deployment = _ensure_deployment_exist(client, namespace.resource_group, namespace.service, namespace.name, namespace.deployment)
if deployment.properties.active:
raise InvalidArgumentValueError('Deployment {} is already the production deployment'.format(deployment.name))
raise InvalidArgumentValueError(f'Deployment {deployment.name} is already the production deployment')


def _ensure_deployment_exist(client, resource_group, service, app, deployment):
try:
return client.deployments.get(resource_group, service, app, deployment)
except CloudError:
raise InvalidArgumentValueError('Deployment {} not found under app {}'.format(deployment, app))
raise InvalidArgumentValueError(f'Deployment {deployment} not found under app {app}')


def _ensure_active_deployment_exist_and_get(client, resource_group, service, name):
Expand All @@ -97,7 +97,7 @@ def _get_active_deployment(client, resource_group, service, name):
deployments = client.deployments.list(resource_group, service, name)
return next(iter(x for x in deployments if x.properties.active), None)
except ResourceNotFoundError:
raise InvalidArgumentValueError('App {} not found'.format(name))
raise InvalidArgumentValueError(f'App {name} not found')


def validate_deloy_path(cmd, namespace):
Expand Down Expand Up @@ -159,6 +159,6 @@ def _validate_container_registry(cmd, namespace):
" See more details in https://learn.microsoft.com/en-us/azure/spring-apps/how-to-deploy-with-custom-container-image?tabs=azure-cli")
except ResourceNotFoundError:
if namespace.source_path or namespace.artifact_path:
raise InvalidArgumentValueError(
"The instance without build service can only use '--container-image' to deploy."
" See more details in https://learn.microsoft.com/en-us/azure/spring-apps/how-to-deploy-with-custom-container-image?tabs=azure-cli")
raise InvalidArgumentValueError(
"The instance without build service can only use '--container-image' to deploy."
" See more details in https://learn.microsoft.com/en-us/azure/spring-apps/how-to-deploy-with-custom-container-image?tabs=azure-cli")
41 changes: 26 additions & 15 deletions src/spring/azext_spring/_build_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,24 +51,30 @@ def create_build_service(cmd, client, resource_group, service, disable_build_ser
LongRunningOperation(cmd.cli_ctx)(poller)

subscription = get_subscription_id(cmd.cli_ctx)
service_resource_id = '/subscriptions/{}/resourceGroups/{}/providers/Microsoft.AppPlatform/Spring/{}'.format(subscription, resource_group, service)
service_resource_id_template = '/subscriptions/{}/resourceGroups/{}/providers/Microsoft.AppPlatform/Spring/{}'
resource_id = service_resource_id_template.format(subscription, resource_group, service)
build_service_properties = models.BuildServiceProperties(
container_registry='{}/containerregistries/{}'.format(service_resource_id, DEFAULT_CONTAINER_REGISTRY_NAME))
container_registry='{}/containerregistries/{}'.format(resource_id, DEFAULT_CONTAINER_REGISTRY_NAME))
build_service_resource = models.BuildService(
properties=build_service_properties)
return client.build_service.begin_create_or_update(resource_group, service, DEFAULT_BUILD_SERVICE_NAME, build_service_resource)
return client.build_service.begin_create_or_update(resource_group, service,
DEFAULT_BUILD_SERVICE_NAME, build_service_resource)
else:
build_service_properties = models.BuildServiceProperties(
container_registry=None)
build_service_resource = models.BuildService(
properties=build_service_properties)
return client.build_service.begin_create_or_update(resource_group, service, DEFAULT_BUILD_SERVICE_NAME, build_service_resource)
return client.build_service.begin_create_or_update(resource_group, service,
DEFAULT_BUILD_SERVICE_NAME, build_service_resource)


def create_or_update_builder(cmd, client, resource_group, service, name, builder_json=None, builder_file=None, no_wait=False):
logger.warning('Editing builder will regenerate images for all app deployments using this builder. These new images will ' +
'be used after app restart either manually by yourself or automatically by Azure Spring Apps in regular maintenance tasks. ' +
'Use CLI command --"az spring build-service builder show-deployments" to view the app deployment list of the builder.')
def create_or_update_builder(cmd, client, resource_group, service, name,
builder_json=None, builder_file=None, no_wait=False):
logger.warning('Editing builder will regenerate images for all app deployments using this builder. '
'These new images will be used after app restart either manually by yourself or '
'automatically by Azure Spring Apps in regular maintenance tasks. Use CLI '
'command --"az spring build-service builder show-deployments" to view the app '
'deployment list of the builder.')
builder = _update_builder(builder_file, builder_json)
builder_resource = models.BuilderResource(
properties=builder
Expand All @@ -86,10 +92,12 @@ def builder_show_deployments(cmd, client, resource_group, service, name):


def builder_delete(cmd, client, resource_group, service, name, no_wait=False):
return sdk_no_wait(no_wait, client.build_service_builder.begin_delete, resource_group, service, DEFAULT_BUILD_SERVICE_NAME, name)
return sdk_no_wait(no_wait, client.build_service_builder.begin_delete,
resource_group, service, DEFAULT_BUILD_SERVICE_NAME, name)


def create_or_update_container_registry(cmd, client, resource_group, service, name=None, server=None, username=None, password=None):
def create_or_update_container_registry(cmd, client, resource_group, service, name=None,
server=None, username=None, password=None):
container_registry_properties = models.ContainerRegistryProperties(
credentials=models.ContainerRegistryBasicCredentials(
server=server,
Expand Down Expand Up @@ -141,7 +149,8 @@ def build_list(cmd, client, resource_group, service):


def build_delete(cmd, client, resource_group, service, name, no_wait=False):
return sdk_no_wait(no_wait, client.build_service.begin_delete_build, resource_group, service, DEFAULT_BUILD_SERVICE_NAME, name)
return sdk_no_wait(no_wait, client.build_service.begin_delete_build,
resource_group, service, DEFAULT_BUILD_SERVICE_NAME, name)


def build_result_show(cmd, client, resource_group, service, build_name=None, name=None):
Expand All @@ -154,12 +163,14 @@ def build_result_list(cmd, client, resource_group, service, build_name=None):

def update_build_service(cmd, client, resource_group, service, registry_name=None, no_wait=False):
subscription = get_subscription_id(cmd.cli_ctx)
service_resource_id = '/subscriptions/{}/resourceGroups/{}/providers/Microsoft.AppPlatform/Spring/{}'.format(subscription, resource_group, service)
build_service_properties = models.BuildServiceProperties(
container_registry='{}/containerregistries/{}'.format(service_resource_id, registry_name) if registry_name else None)
service_resource_id_template = '/subscriptions/{}/resourceGroups/{}/providers/Microsoft.AppPlatform/Spring/{}'
service_resource_id = service_resource_id_template.format(subscription, resource_group, service)
registry = '{}/containerregistries/{}'.format(service_resource_id, registry_name) if registry_name else None
build_service_properties = models.BuildServiceProperties(container_registry=registry)
build_service_resource = models.BuildService(
properties=build_service_properties)
return sdk_no_wait(no_wait, client.build_service.begin_create_or_update, resource_group, service, DEFAULT_BUILD_SERVICE_NAME, build_service_resource)
return sdk_no_wait(no_wait, client.build_service.begin_create_or_update,
resource_group, service, DEFAULT_BUILD_SERVICE_NAME, build_service_resource)


def build_service_show(cmd, client, resource_group, service):
Expand Down
Loading