Skip to content

Commit

Permalink
[Spring-cloud] Add support for AppMSI (#4598)
Browse files Browse the repository at this point in the history
* use 2022-03-01-preview sdk for app command and app identity command, and re-generate the recording-test yaml files

* Add a temp version number

* Add params definition for identity remove
Add helps for params in identity remove
Add validators for params in identity remove
Add unit test for params validator

* refine help and example for command group
spring-cloud app identity remove

* refine help and example for command group
spring-cloud app identity remove

* refine help and example for command group
spring-cloud app identity remove

* add support to remove user-assigned managed identities.

* mark user-assigned as preview for identity remove

* refine identity remove

* For identity assign
1) Refine help info for command
2) Add parameters
3) Add validator
4) Add UT for validator

* For identity assign: implement

* refactor and add UT and add scenario test

* refine help info for app identity

* hide role and scope from help info

* For app create with managed identity:
1. Add param
2. Add help info
3. Add validator
4. Add UT for validator

* For app create with managed identity:
refactor validator

* For app create with managed identity:
refine validator

* For app create with managed identity:
implement and add partial recording test

* For app create with managed identity:
add recording test for create app with user assigned managed identities

* For app create with managed identity:
add recording test for create app with both assigned managed identities

* For app identity force-set managed identity:
1. add parameters
2. add validator
3. add method in help
4. add command
5. add stub implementation
6. add UT for validator

* For app identity force-set managed identity:
implement

* For app identity force-set managed identity:
add recording test for force-set

* remove not used import

* fix role assignment bugs

* refine payload for force-set identities

* switch app show to use 2022-03-01-preview api version

* update deprecation info for role param

* refactor code

* simplify force-set

* add version number

* refactor "updating" and "deleting"

* fix bug for async operation

* update version info

* Update version number

* Update version to build a extension for testing team

* refine warning: in the future -> in future

* refine help info for force-set params

* refine example description for command force-set

* refactor _format_identity in _app_factory

* add scope and role from deprecated back to supported status in help information. Will add not support and obsolete warning for specific cases.

* add example for role assignment for system-assigned managed identity.

* update version number for testing

* update version number and history.

* fix lint

* refine the CLIError, and categorize into different types

* refine the CLIError, and categorize into different types

* fix lint 2
  • Loading branch information
jiec-msft committed Mar 31, 2022
1 parent 8b3ddae commit 8f72a56
Show file tree
Hide file tree
Showing 37 changed files with 15,940 additions and 5,674 deletions.
4 changes: 4 additions & 0 deletions src/spring-cloud/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
Release History
===============
3.1.0
---
* Add support for user-assigned managed identity on App (Preview).

3.0.1
---
* `az spring-cloud app deploy` has new preview argument "--build-env" to specify build module and jvm version and so on.
Expand Down
34 changes: 30 additions & 4 deletions src/spring-cloud/azext_spring_cloud/_app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# pylint: disable=wrong-import-order
from azure.cli.core.azclierror import FileOperationError, InvalidArgumentValueError
from .vendored_sdks.appplatform.v2022_01_01_preview import models
from .vendored_sdks.appplatform.v2022_03_01_preview import models as models_20220301preview
from azure.cli.core.util import get_file_json


Expand All @@ -23,10 +24,35 @@ def _format_properties(self, **kwargs):
kwargs['temporary_disk'] = self._load_temp_disk(**kwargs)
return models.AppResourceProperties(**kwargs)

def _format_identity(self, assign_identity=None, **_):
if assign_identity is not None:
assign_type = 'systemassigned' if assign_identity else 'None'
return models.ManagedIdentityProperties(type=assign_type)
def _format_identity(self, system_assigned=None, user_assigned=None, **_):
target_identity_type = self._get_identity_assign_type(system_assigned, user_assigned)
user_identity_payload = self._get_user_identity_payload(user_assigned)
identity_props = None
if target_identity_type != models_20220301preview.ManagedIdentityType.NONE:
identity_props = models_20220301preview.ManagedIdentityProperties()
identity_props.type = target_identity_type
identity_props.user_assigned_identities = user_identity_payload
return identity_props

def _get_identity_assign_type(self, system_assigned=None, user_assigned=None):
target_identity_type = models_20220301preview.ManagedIdentityType.NONE
if system_assigned and user_assigned:
target_identity_type = models_20220301preview.ManagedIdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED
elif system_assigned:
target_identity_type = models_20220301preview.ManagedIdentityType.SYSTEM_ASSIGNED
elif user_assigned:
target_identity_type = models_20220301preview.ManagedIdentityType.USER_ASSIGNED
return target_identity_type

def _get_user_identity_payload(self, user_assigned=None):
if not user_assigned:
return None
user_identity_payload = {}
for user_identity_resource_id in user_assigned:
user_identity_payload[user_identity_resource_id] = models_20220301preview.UserAssignedManagedIdentity()
if len(user_identity_payload) == 0:
user_identity_payload = None
return user_identity_payload

def _load_temp_disk(self, enable_temporary_disk=None, **_):
if enable_temporary_disk is not None:
Expand Down
137 changes: 137 additions & 0 deletions src/spring-cloud/azext_spring_cloud/_app_managed_identity_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------


from azure.cli.core.azclierror import InvalidArgumentValueError
from azure.mgmt.core.tools import is_valid_resource_id
from knack.log import get_logger


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."
ENABLE_LOWER = "enable"
DISABLE_LOWER = "disable"


def validate_app_identity_remove_or_warning(namespace):
if namespace.system_assigned is None and namespace.user_assigned is None:
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.")
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))


def _normalized_user_identitiy_resource_id_list(user_identity_resource_id_list):
result = []
if not user_identity_resource_id_list:
return result
for id in user_identity_resource_id_list:
result.append(id.strip().lower())
return result


def _is_valid_user_assigned_managed_identity_resource_id(resource_id):
if not is_valid_resource_id(resource_id.lower()):
return False
if "/providers/Microsoft.ManagedIdentity/userAssignedIdentities/".lower() not in resource_id.lower():
return False
return True


def validate_app_identity_assign_or_warning(namespace):
_warn_if_no_identity_type_params(namespace)
_validate_role_and_scope_should_use_together(namespace)
_validate_role_and_scope_should_not_use_with_user_identity(namespace)
_validate_user_identity_resource_id(namespace)
_normalize_user_identity_resource_id(namespace)


def _warn_if_no_identity_type_params(namespace):
if namespace.system_assigned is None and namespace.user_assigned is None:
logger.warning(OBSOLETE_APP_IDENTITY_ASSIGN)


def _validate_role_and_scope_should_use_together(namespace):
if _has_role_or_scope(namespace) and not _has_role_and_scope(namespace):
raise InvalidArgumentValueError("Parameter \"role\" and \"scope\" should be used together.")


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.")


def _has_role_and_scope(namespace):
return namespace.role and namespace.scope


def _has_role_or_scope(namespace):
return namespace.role or namespace.scope


def _only_has_user_assigned(namespace):
return (namespace.user_assigned) and (not namespace.system_assigned)


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))


def _normalize_user_identity_resource_id(namespace):
if namespace.user_assigned:
namespace.user_assigned = _normalized_user_identitiy_resource_id_list(namespace.user_assigned)


def validate_create_app_with_user_identity_or_warning(namespace):
_validate_user_identity_resource_id(namespace)
_normalize_user_identity_resource_id(namespace)


def validate_create_app_with_system_identity_or_warning(namespace):
"""
Note: assign_identity is deprecated, use system_assigned instead.
"""
if namespace.system_assigned is not None and namespace.assign_identity is not None:
raise InvalidArgumentValueError('Parameter "system-assigned" should not use together with "assign-identity".')
if namespace.assign_identity is not None:
namespace.system_assigned = namespace.assign_identity


def validate_app_force_set_system_identity_or_warning(namespace):
if namespace.system_assigned is None:
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))


def validate_app_force_set_user_identity_or_warning(namespace):
if namespace.user_assigned is None or len(namespace.user_assigned) == 0:
raise InvalidArgumentValueError('Parameter "user-assigned" expected at least one argument.')
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))
elif single_element == DISABLE_LOWER:
namespace.user_assigned = [DISABLE_LOWER]
else:
_normalize_user_identity_resource_id(namespace)
else:
_validate_user_identity_resource_id(namespace)
_normalize_user_identity_resource_id(namespace)
7 changes: 7 additions & 0 deletions src/spring-cloud/azext_spring_cloud/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from .vendored_sdks.appplatform.v2022_01_01_preview import (
AppPlatformManagementClient as AppPlatformManagementClient_20220101preview
)
from .vendored_sdks.appplatform.v2022_03_01_preview import (
AppPlatformManagementClient as AppPlatformManagementClient_20220301preview
)
from .vendored_sdks.appplatform.v2021_06_01_preview import (
AppPlatformManagementClient as AppPlatformManagementClient_20210601preview
)
Expand All @@ -19,6 +22,10 @@
)


def cf_spring_cloud_20220301preview(cli_ctx, *_):
return get_mgmt_service_client(cli_ctx, AppPlatformManagementClient_20220301preview)


def cf_spring_cloud_20220101preview(cli_ctx, *_):
return get_mgmt_service_client(cli_ctx, AppPlatformManagementClient_20220101preview)

Expand Down
11 changes: 11 additions & 0 deletions src/spring-cloud/azext_spring_cloud/_clierror.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core.azclierror import UserFault


class ConflictRequestError(UserFault):
""" Conflict request: 409 error """
pass
32 changes: 25 additions & 7 deletions src/spring-cloud/azext_spring_cloud/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,25 +256,31 @@

helps['spring-cloud app identity'] = """
type: group
short-summary: Manage an app's managed service identity.
short-summary: Manage an app's managed identities.
"""

helps['spring-cloud app identity assign'] = """
type: command
short-summary: Enable managed service identity on an app.
short-summary: Enable system-assigned managed identity or assign user-assigned managed identities to an app.
examples:
- name: Enable the system assigned identity.
text: az spring-cloud app identity assign -n MyApp -s MyCluster -g MyResourceGroup
text: az spring-cloud app identity assign -n MyApp -s MyCluster -g MyResourceGroup --system-assigned
- name: Enable the system assigned identity on an app with the 'Reader' role.
text: az spring-cloud app identity assign -n MyApp -s MyCluster -g MyResourceGroup --role Reader --scope /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/xxxxx/providers/Microsoft.KeyVault/vaults/xxxxx
text: az spring-cloud app identity assign -n MyApp -s MyCluster -g MyResourceGroup --system-assigned --role Reader --scope /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/xxxxx/providers/Microsoft.KeyVault/vaults/xxxxx
- name: Assign two user-assigned managed identities to an app.
text: az spring-cloud app identity assign -n MyApp -s MyCluster -g MyResourceGroup --user-assigned IdentityResourceId1 IdentityResourceId2
"""

helps['spring-cloud app identity remove'] = """
type: command
short-summary: Remove managed service identity from an app.
short-summary: Remove managed identity from an app.
examples:
- name: Remove the system assigned identity from an app.
text: az spring-cloud app identity remove -n MyApp -s MyCluster -g MyResourceGroup
- name: Remove the system-assigned managed identity from an app.
text: az spring-cloud app identity remove -n MyApp -s MyCluster -g MyResourceGroup --system-assigned
- name: Remove the system-assigned and user-assigned managed identities from an app.
text: az spring-cloud app identity remove -n MyApp -s MyCluster -g MyResourceGroup --system-assigned --user-assigned IdentityResourceId1 IdentityResourceId2
- name: Remove ALL user-assigned managed identities from an app.
text: az spring-cloud app identity remove -n MyApp -s MyCluster -g MyResourceGroup --user-assigned
"""

helps['spring-cloud app identity show'] = """
Expand All @@ -285,6 +291,18 @@
text: az spring-cloud app identity show -n MyApp -s MyCluster -g MyResourceGroup
"""

helps['spring-cloud app identity force-set'] = """
type: command
short-summary: Force set managed identities on an app.
examples:
- name: Force remove all managed identities on an app.
text: az spring-cloud app identity force-set -n MyApp -s MyCluster -g MyResourceGroup --system-assigned disable --user-assigned disable
- name: Force remove all user-assigned managed identities on an app, and enable or keep system-assigned managed identity.
text: az spring-cloud app identity force-set -n MyApp -s MyCluster -g MyResourceGroup --system-assigned enable --user-assigned disable
- name: Force remove system-assigned managed identity on an app, and assign or keep user-assigned managed identities.
text: az spring-cloud app identity force-set -n MyApp -s MyCluster -g MyResourceGroup --system-assigned disable --user-assigned IdentityResourceId1 IdentityResourceId2
"""

helps['spring-cloud app set-deployment'] = """
type: command
short-summary: Set production deployment of an app.
Expand Down
52 changes: 48 additions & 4 deletions src/spring-cloud/azext_spring_cloud/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
from ._app_validator import (fulfill_deployment_param, active_deployment_exist,
ensure_not_active_deployment, validate_deloy_path, validate_deloyment_create_path,
validate_cpu, validate_memory, fulfill_deployment_param_or_warning, active_deployment_exist_or_warning)
from ._app_managed_identity_validator import (validate_create_app_with_user_identity_or_warning,
validate_create_app_with_system_identity_or_warning,
validate_app_force_set_system_identity_or_warning,
validate_app_force_set_user_identity_or_warning)
from ._utils import ApiType


Expand Down Expand Up @@ -186,8 +190,21 @@ def load_arguments(self, _):
c.argument('assign_endpoint', arg_type=get_three_state_flag(),
help='If true, assign endpoint URL for direct access.', default=False,
options_list=['--assign-endpoint', c.deprecate(target='--is-public', redirect='--assign-endpoint', hide=True)])
c.argument('assign_identity', arg_type=get_three_state_flag(),
help='If true, assign managed service identity.')
c.argument('assign_identity',
arg_type=get_three_state_flag(),
validator=validate_create_app_with_system_identity_or_warning,
deprecate_info=c.deprecate(target='--assign-identity',
redirect='--system-assigned',
hide=True),
help='Enable system-assigned managed identity.')
c.argument('system_assigned',
arg_type=get_three_state_flag(),
help='Enable system-assigned managed identity.')
c.argument('user_assigned',
is_preview=True,
nargs='+',
validator=validate_create_app_with_user_identity_or_warning,
help="Space-separated user-assigned managed identity resource IDs to assgin to an app.")
c.argument('cpu', arg_type=cpu_type, default="1")
c.argument('memory', arg_type=memort_type, default="1Gi")
c.argument('instance_count', type=int,
Expand Down Expand Up @@ -236,8 +253,35 @@ def load_arguments(self, _):
c.argument('name', name_type, help='Name of app.', validator=active_deployment_exist_or_warning)

with self.argument_context('spring-cloud app identity assign') as c:
c.argument('scope', help="The scope the managed identity has access to")
c.argument('role', help="Role name or id the managed identity will be assigned")
c.argument('scope',
help="The scope the managed identity has access to")
c.argument('role',
help="Role name or id the managed identity will be assigned")
c.argument('system_assigned',
arg_type=get_three_state_flag(),
help="Enable system-assigned managed identity on an app.")
c.argument('user_assigned',
is_preview=True,
nargs='+',
help="Space-separated user-assigned managed identity resource IDs to assgin to an app.")

with self.argument_context('spring-cloud app identity remove') as c:
c.argument('system_assigned',
arg_type=get_three_state_flag(),
help="Remove system-assigned managed identity.")
c.argument('user_assigned',
is_preview=True,
nargs='*',
help="Space-separated user-assigned managed identity resource IDs to remove. If no ID is provided, remove ALL user-assigned managed identities.")

with self.argument_context('spring-cloud app identity force-set') as c:
c.argument('system_assigned',
validator=validate_app_force_set_system_identity_or_warning,
help="Allowed values: [\"enable\", \"disable\"]. Use \"enable\" to enable or keep system-assigned managed identity. Use \"disable\" to remove system-assigned managed identity.")
c.argument('user_assigned',
nargs='+',
validator=validate_app_force_set_user_identity_or_warning,
help="Allowed values: [\"disable\", space-separated user-assigned managed identity resource IDs]. Use \"disable\" to remove all user-assigned managed identities, use resource IDs to assign or keep user-assigned managed identities.")

def prepare_logs_argument(c):
'''`app log tail` is deprecated. `app logs` is the new choice. They share the same command processor.'''
Expand Down
5 changes: 4 additions & 1 deletion src/spring-cloud/azext_spring_cloud/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def app_create(cmd, client, resource_group, service, name,
jvm_options=None,
# app.create
assign_identity=None,
system_assigned=None,
user_assigned=None,
# app.update
enable_persistent_storage=None,
persistent_storage=None,
Expand All @@ -71,7 +73,8 @@ def app_create(cmd, client, resource_group, service, name,
}

create_app_kwargs = {
'assign_identity': assign_identity,
'system_assigned': system_assigned,
'user_assigned': user_assigned,
'enable_temporary_disk': True,
'enable_persistent_storage': enable_persistent_storage,
'persistent_storage': persistent_storage,
Expand Down
Loading

0 comments on commit 8f72a56

Please sign in to comment.