Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

spring-cloud: add support to enable/disable MSI #1523

Merged
merged 6 commits into from
Apr 16, 2020
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
31 changes: 31 additions & 0 deletions src/spring-cloud/azext_spring_cloud/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,37 @@
short-summary: Show logs of an app instance, logs will be streamed when setting '-f/--follow'.
"""

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

helps['spring-cloud app identity assign'] = """
type: command
short-summary: Enable managed service identity on an app.
leonard520 marked this conversation as resolved.
Show resolved Hide resolved
examples:
- name: Enable the system assigned identity.
text: az spring-cloud app identity assign -n MyApp -s MyCluster -g MyResourceGroup
- 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
"""

helps['spring-cloud app identity remove'] = """
type: command
short-summary: Remove managed service 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
"""

helps['spring-cloud app identity show'] = """
type: command
short-summary: Display app's managed identity info.
leonard520 marked this conversation as resolved.
Show resolved Hide resolved
examples:
- name: Display an app's managed identity info.
text: az spring-cloud app identity show -n MyApp -s MyCluster -g MyResourceGroup
"""

helps['spring-cloud app set-deployment'] = """
type: command
short-summary: Set production deployment of an app.
Expand Down
6 changes: 6 additions & 0 deletions src/spring-cloud/azext_spring_cloud/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ def load_arguments(self, _):
with self.argument_context('spring-cloud app create') as c:
c.argument(
'is_public', arg_type=get_three_state_flag(), help='If true, assign public domain', default=False)
c.argument('assign_identity', arg_type=get_three_state_flag(),
help='If true, assign managed service identity.')

with self.argument_context('spring-cloud app update') as c:
c.argument('is_public', arg_type=get_three_state_flag(),
Expand All @@ -55,6 +57,10 @@ def load_arguments(self, _):
c.argument('deployment', options_list=[
'--deployment', '-d'], help='Name of an existing deployment of the app. Default to the production deployment if not specified.', validator=validate_deployment_name)

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

with self.argument_context('spring-cloud app logs') as c:
c.argument('instance', options_list=['--instance', '-i'], help='Name of an existing instance of the deployment.')
c.argument('lines', type=int, help='Number of lines to show. Maximum is 10000', validator=validate_log_lines)
Expand Down
5 changes: 5 additions & 0 deletions src/spring-cloud/azext_spring_cloud/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ def load_command_table(self, _):
g.custom_command('restart', 'app_restart', supports_no_wait=True)
g.custom_command('logs', 'app_tail_log')

with self.command_group('spring-cloud app identity', client_factory=cf_spring_cloud) as g:
g.custom_command('assign', 'app_identity_assign')
g.custom_command('remove', 'app_identity_remove')
g.custom_show_command('show', 'app_identity_show')

with self.command_group('spring-cloud app log', client_factory=cf_spring_cloud,
deprecate_info=g.deprecate(redirect='az spring-cloud app logs', hide=True)) as g:
g.custom_command('tail', 'app_tail_log')
Expand Down
92 changes: 87 additions & 5 deletions src/spring-cloud/azext_spring_cloud/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from knack.log import get_logger
from .azure_storage_file import FileService
from azure.cli.core.util import sdk_no_wait
from azure.cli.core.profiles import ResourceType, get_sdk
from ast import literal_eval
from azure.cli.core.commands import cached_put
from ._utils import _get_rg_location
Expand Down Expand Up @@ -100,7 +101,8 @@ def app_create(cmd, client, resource_group, service, name,
runtime_version=None,
jvm_options=None,
env=None,
enable_persistent_storage=None):
enable_persistent_storage=None,
assign_identity=None):
apps = _get_all_apps(client, resource_group, service)
if name in apps:
raise CLIError("App '{}' already exists.".format(name))
Expand All @@ -119,8 +121,14 @@ def app_create(cmd, client, resource_group, service, name,
resource = client.services.get(resource_group, service)
location = resource.location

app_resource = models.AppResource()
app_resource.properties = properties
app_resource.location = location
if assign_identity is True:
app_resource.identity = models.ManagedIdentityProperties(type="systemassigned")

poller = client.apps.create_or_update(
resource_group, service, name, properties, location)
resource_group, service, name, app_resource)
while poller.done() is False:
sleep(APP_CREATE_OR_UPDATE_SLEEP_INTERVAL)

Expand All @@ -147,7 +155,10 @@ def app_create(cmd, client, resource_group, service, name,
properties = models.AppResourceProperties(
active_deployment_name=DEFAULT_DEPLOYMENT_NAME, public=is_public)

app_poller = client.apps.update(resource_group, service, name, properties, location)
app_resource.properties = properties
app_resource.location = location

app_poller = client.apps.update(resource_group, service, name, app_resource)
logger.warning(
"[4/4] Updating app '{}' (this operation can take a while to complete)".format(name))
while not poller.done() or not app_poller.done():
Expand Down Expand Up @@ -178,9 +189,13 @@ def app_update(cmd, client, resource_group, service, name,
resource = client.services.get(resource_group, service)
location = resource.location

app_resource = models.AppResource()
app_resource.properties = properties
app_resource.location = location

logger.warning("[1/2] updating app '{}'".format(name))
poller = client.apps.update(
resource_group, service, name, properties, location)
resource_group, service, name, app_resource)
while poller.done() is False:
sleep(APP_CREATE_OR_UPDATE_SLEEP_INTERVAL)

Expand Down Expand Up @@ -425,6 +440,69 @@ def app_tail_log(cmd, client, resource_group, service, name, instance=None, foll
raise exceptions[0]


def app_identity_assign(cmd, client, resource_group, service, name, role=None, scope=None):
app_resource = models.AppResource()
identity = models.ManagedIdentityProperties(type="systemassigned")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have plan to support other type? if yes better to expose type as a param with default value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet. But we can refactor code if we will support in future.

properties = models.AppResourceProperties()
resource = client.services.get(resource_group, service)
location = resource.location

app_resource.identity = identity
app_resource.properties = properties
app_resource.location = location
client.apps.update(resource_group, service, name, app_resource)
app = client.apps.get(resource_group, service, name)
if role:
principal_id = app.identity.principal_id

from azure.cli.core.commands import arm as _arm
identity_role_id = _arm.resolve_role_id(cmd.cli_ctx, role, scope)
from azure.cli.core.commands.client_factory import get_mgmt_service_client
assignments_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_AUTHORIZATION).role_assignments
RoleAssignmentCreateParameters = get_sdk(cmd.cli_ctx, ResourceType.MGMT_AUTHORIZATION,
'RoleAssignmentCreateParameters', mod='models',
operation_group='role_assignments')
parameters = RoleAssignmentCreateParameters(role_definition_id=identity_role_id, principal_id=principal_id)
logger.info("Creating an assignment with a role '%s' on the scope of '%s'", identity_role_id, scope)
retry_times = 36
assignment_name = _arm._gen_guid()
for l in range(0, retry_times):
try:
assignments_client.create(scope=scope, role_assignment_name=assignment_name,
parameters=parameters)
break
except CloudError as ex:
if 'role assignment already exists' in ex.message:
logger.info('Role assignment already exists')
break
elif l < retry_times and ' does not exist in the directory ' in ex.message:
sleep(APP_CREATE_OR_UPDATE_SLEEP_INTERVAL)
logger.warning('Retrying role assignment creation: %s/%s', l + 1,
retry_times)
continue
else:
raise
return app


def app_identity_remove(cmd, client, resource_group, service, name):
app_resource = models.AppResource()
identity = models.ManagedIdentityProperties(type="none")
properties = models.AppResourceProperties()
resource = client.services.get(resource_group, service)
location = resource.location

app_resource.identity = identity
app_resource.properties = properties
app_resource.location = location
return client.apps.update(resource_group, service, name, app_resource)


def app_identity_show(cmd, client, resource_group, service, name):
app = client.apps.get(resource_group, service, name)
return app.identity


def app_set_deployment(cmd, client, resource_group, service, name, deployment):
deployments = _get_all_deployments(client, resource_group, service, name)
active_deployment = client.apps.get(
Expand All @@ -441,7 +519,11 @@ def app_set_deployment(cmd, client, resource_group, service, name, deployment):
resource = client.services.get(resource_group, service)
location = resource.location

return client.apps.update(resource_group, service, name, properties, location)
app_resource = models.AppResource()
app_resource.properties = properties
app_resource.location = location

return client.apps.update(resource_group, service, name, app_resource)


def deployment_create(cmd, client, resource_group, service, app, name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from ._models_py3 import GitPatternRepository
from ._models_py3 import LogFileUrlResponse
from ._models_py3 import LogSpecification
from ._models_py3 import ManagedIdentityProperties
from ._models_py3 import MetricDimension
from ._models_py3 import MetricSpecification
from ._models_py3 import NameAvailability
Expand Down Expand Up @@ -62,6 +63,7 @@
from ._models import GitPatternRepository
from ._models import LogFileUrlResponse
from ._models import LogSpecification
from ._models import ManagedIdentityProperties
from ._models import MetricDimension
from ._models import MetricSpecification
from ._models import NameAvailability
Expand Down Expand Up @@ -90,6 +92,7 @@
ProvisioningState,
ConfigServerState,
TraceProxyState,
ManagedIdentityType,
TestKeyType,
AppResourceProvisioningState,
UserSourceType,
Expand All @@ -115,6 +118,7 @@
'GitPatternRepository',
'LogFileUrlResponse',
'LogSpecification',
'ManagedIdentityProperties',
'MetricDimension',
'MetricSpecification',
'NameAvailability',
Expand Down Expand Up @@ -142,6 +146,7 @@
'ProvisioningState',
'ConfigServerState',
'TraceProxyState',
'ManagedIdentityType',
'TestKeyType',
'AppResourceProvisioningState',
'UserSourceType',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ class TraceProxyState(str, Enum):
updating = "Updating"


class ManagedIdentityType(str, Enum):

none = "None"
system_assigned = "SystemAssigned"
user_assigned = "UserAssigned"
Copy link

@xgugeng xgugeng Apr 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep none and system_assigned only, since the other two is not supported yet. Never mind, our backend handles the other two well

system_assigned_user_assigned = "SystemAssigned,UserAssigned"


class TestKeyType(str, Enum):

primary = "Primary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class AppResource(ProxyResource):
:vartype type: str
:param properties: Properties of the App resource
:type properties: ~azure.mgmt.appplatform.models.AppResourceProperties
:param identity: The Managed Identity type of the app resource
:type identity: ~azure.mgmt.appplatform.models.ManagedIdentityProperties
:param location: The GEO location of the application, always the same with
its parent resource
:type location: str
Expand All @@ -107,12 +109,14 @@ class AppResource(ProxyResource):
'name': {'key': 'name', 'type': 'str'},
'type': {'key': 'type', 'type': 'str'},
'properties': {'key': 'properties', 'type': 'AppResourceProperties'},
'identity': {'key': 'identity', 'type': 'ManagedIdentityProperties'},
'location': {'key': 'location', 'type': 'str'},
}

def __init__(self, **kwargs):
super(AppResource, self).__init__(**kwargs)
self.properties = kwargs.get('properties', None)
self.identity = kwargs.get('identity', None)
self.location = kwargs.get('location', None)


Expand Down Expand Up @@ -778,6 +782,31 @@ def __init__(self, **kwargs):
self.blob_duration = kwargs.get('blob_duration', None)


class ManagedIdentityProperties(Model):
"""Managed identity properties retrieved from ARM request headers.

:param type: Possible values include: 'None', 'SystemAssigned',
'UserAssigned', 'SystemAssigned,UserAssigned'
:type type: str or ~azure.mgmt.appplatform.models.ManagedIdentityType
:param principal_id:
:type principal_id: str
:param tenant_id:
:type tenant_id: str
"""

_attribute_map = {
'type': {'key': 'type', 'type': 'str'},
'principal_id': {'key': 'principalId', 'type': 'str'},
'tenant_id': {'key': 'tenantId', 'type': 'str'},
}

def __init__(self, **kwargs):
super(ManagedIdentityProperties, self).__init__(**kwargs)
self.type = kwargs.get('type', None)
self.principal_id = kwargs.get('principal_id', None)
self.tenant_id = kwargs.get('tenant_id', None)


class MetricDimension(Model):
"""Specifications of the Dimension of metrics.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class AppResource(ProxyResource):
:vartype type: str
:param properties: Properties of the App resource
:type properties: ~azure.mgmt.appplatform.models.AppResourceProperties
:param identity: The Managed Identity type of the app resource
:type identity: ~azure.mgmt.appplatform.models.ManagedIdentityProperties
:param location: The GEO location of the application, always the same with
its parent resource
:type location: str
Expand All @@ -107,12 +109,14 @@ class AppResource(ProxyResource):
'name': {'key': 'name', 'type': 'str'},
'type': {'key': 'type', 'type': 'str'},
'properties': {'key': 'properties', 'type': 'AppResourceProperties'},
'identity': {'key': 'identity', 'type': 'ManagedIdentityProperties'},
'location': {'key': 'location', 'type': 'str'},
}

def __init__(self, *, properties=None, location: str=None, **kwargs) -> None:
def __init__(self, *, properties=None, identity=None, location: str=None, **kwargs) -> None:
super(AppResource, self).__init__(**kwargs)
self.properties = properties
self.identity = identity
self.location = location


Expand Down Expand Up @@ -778,6 +782,31 @@ def __init__(self, *, name: str=None, display_name: str=None, blob_duration: str
self.blob_duration = blob_duration


class ManagedIdentityProperties(Model):
"""Managed identity properties retrieved from ARM request headers.

:param type: Possible values include: 'None', 'SystemAssigned',
'UserAssigned', 'SystemAssigned,UserAssigned'
:type type: str or ~azure.mgmt.appplatform.models.ManagedIdentityType
:param principal_id:
:type principal_id: str
:param tenant_id:
:type tenant_id: str
"""

_attribute_map = {
'type': {'key': 'type', 'type': 'str'},
'principal_id': {'key': 'principalId', 'type': 'str'},
'tenant_id': {'key': 'tenantId', 'type': 'str'},
}

def __init__(self, *, type=None, principal_id: str=None, tenant_id: str=None, **kwargs) -> None:
super(ManagedIdentityProperties, self).__init__(**kwargs)
self.type = type
self.principal_id = principal_id
self.tenant_id = tenant_id


class MetricDimension(Model):
"""Specifications of the Dimension of metrics.

Expand Down
Loading