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

Flex CLI deployment config changes. #22

Merged
merged 4 commits into from
Feb 16, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,6 @@ def __init__(self):

DEFAULT_MAXIMUM_INSTANCE_COUNT = 100

DEPLOYMENT_STORAGE_AUTH_TYPES = ['systemAssignedIdentity', 'userAssignedIdentity', 'storageAccountConnectionString']
DEPLOYMENT_STORAGE_AUTH_TYPES = ['SystemAssignedIdentity', 'UserAssignedIdentity', 'StorageAccountConnectionString']

STORAGE_BLOB_DATA_CONTRIBUTOR_ROLE_ID = 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'
6 changes: 6 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,12 @@ def load_arguments(self, _):
c.argument('deployment_storage_container_name', help="The deployment storage account container name.", is_preview=True)
c.argument('deployment_storage_auth_type', arg_type=get_enum_type(DEPLOYMENT_STORAGE_AUTH_TYPES), help="The deployment storage account authentication type.", is_preview=True)
c.argument('deployment_storage_auth_value', help="The deployment storage account authentication value. This is only applicable for the user-assigned managed identity authentication type.", is_preview=True)

with self.argument_context('functionapp deployment config set') as c:
c.argument('deployment_storage_name', help="The deployment storage account name.", is_preview=True)
c.argument('deployment_storage_container_name', help="The deployment storage account container name.", is_preview=True)
c.argument('deployment_storage_auth_type', arg_type=get_enum_type(DEPLOYMENT_STORAGE_AUTH_TYPES), help="The deployment storage account authentication type.", is_preview=True)
c.argument('deployment_storage_auth_value', help="The deployment storage account authentication value. This is only applicable for the user-assigned managed identity authentication type.", is_preview=True)

with self.argument_context('functionapp cors credentials') as c:
c.argument('enable', help='enable/disable access-control-allow-credentials', arg_type=get_three_state_flag())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ def load_command_table(self, _):
custom_func_name='update_functionapp', getter_type=appservice_custom, setter_type=appservice_custom, command_type=webapp_sdk,
validator=validate_functionapp_on_containerapp_update)

with self.command_group('functionapp deployment config') as g:
g.custom_command('set', 'update_deployment_configs', exception_handler=ex_handler_factory(), validator=validate_is_flex_functionapp)
g.custom_command('show', 'get_deployment_configs', exception_handler=ex_handler_factory(), validator=validate_is_flex_functionapp)

with self.command_group('functionapp config') as g:
g.custom_command('set', 'update_site_configs_functionapp', validator=validate_functionapp_on_containerapp_site_config_set, exception_handler=ex_handler_factory())
g.custom_show_command('show', 'get_site_configs', validator=validate_functionapp_on_containerapp_site_config_show, exception_handler=ex_handler_factory())
Expand Down
132 changes: 129 additions & 3 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1352,13 +1352,15 @@ def list_runtimes(cmd, os_type=None, linux=False):
linux = False
if os_type == LINUX_OS_NAME:
windows = False

runtime_helper = _StackRuntimeHelper(cmd=cmd, linux=linux, windows=windows)
return runtime_helper.get_stack_names_only(delimiter=":")


def list_function_app_runtimes(cmd, os_type=None):
# show both linux and windows stacks by default
from ._constants import (FLEX_RUNTIMES)

linux = True
windows = True
if os_type == WINDOWS_OS_NAME:
Expand All @@ -1370,10 +1372,10 @@ def list_function_app_runtimes(cmd, os_type=None):
linux_stacks = [r.to_dict() for r in runtime_helper.stacks if r.linux]
windows_stacks = [r.to_dict() for r in runtime_helper.stacks if not r.linux]
if linux and not windows:
return linux_stacks
return {LINUX_OS_NAME: linux_stacks, 'flex': FLEX_RUNTIMES}
if windows and not linux:
return windows_stacks
return {WINDOWS_OS_NAME: windows_stacks, LINUX_OS_NAME: linux_stacks}
return {WINDOWS_OS_NAME: windows_stacks, LINUX_OS_NAME: linux_stacks, 'flex': FLEX_RUNTIMES}


def delete_logic_app(cmd, resource_group_name, name, slot=None):
Expand Down Expand Up @@ -1571,6 +1573,106 @@ def _get_linux_multicontainer_encoded_config_from_file(file_name):
return b64encode(config_file_bytes).decode('utf-8')


def get_deployment_configs(cmd, resource_group_name, name):
functionapp = get_raw_functionapp(cmd, resource_group_name, name)
return functionapp.get("properties", {}).get("functionAppConfig", {}).get(
"deployment", {})

def update_deployment_configs(cmd, resource_group_name, name,
deployment_storage_name=None,
deployment_storage_container_name=None, deployment_storage_auth_type=None,
deployment_storage_auth_value=None):

if (deployment_storage_name is not None) != (deployment_storage_container_name is not None):
raise ArgumentUsageError("Please provide both --deployment-storage-name and --deployment-storage-container-name nor neither.")

if deployment_storage_auth_type == 'UserAssignedIdentity' and not deployment_storage_auth_value:
raise ArgumentUsageError('--deployment-storage-auth-value is required when --deployment-storage-auth-type is set to UserAssignedIdentity.')

if deployment_storage_auth_value and deployment_storage_auth_type != 'UserAssignedIdentity':
raise ArgumentUsageError(
'--deployment-storage-auth-value is only a valid input when --deployment-storage-auth-type set to UserAssignedIdentity. '
'Please try again with --deployment-storage-auth-type set to UserAssignedIdentity.'
)

functionapp = get_raw_functionapp(cmd, resource_group_name, name)

if ("functionAppConfig" in functionapp["properties"]):
function_app_config = functionapp["properties"]["functionAppConfig"]
else:
function_app_config = {"deployment": None, "runtime": None, "scaleAndConcurrency": None}

if ("deployment" not in function_app_config or function_app_config["deployment"] is None):
function_app_config["deployment"] = {"storage": None}

deployment_storage = None
functionapp_deployment_storage = None
if ("storage" not in function_app_config["deployment"] or function_app_config["deployment"]["storage"] is None):
function_app_config["deployment"]["storage"] = functionapp_deployment_storage = {"type": "blobContainer", "value": None, "authentication": None}

if (functionapp_deployment_storage["value"] is None):
if (deployment_storage_name == None):
raise ValidationError("Please provide a values for --deployment-storage-name and --deployment-storage-container-name as function app deployment storage value is not set.")

if ("authentication" not in functionapp_deployment_storage or functionapp_deployment_storage["authentication"] is None):
if (deployment_storage_auth_type == None):
raise ValidationError("Please provide a value for --deployment-storage-auth-type as function app deployment storage authentication type is not set.")
functionapp_deployment_storage["authentication"] = {"type": "SystemAssignedIdentity", "userAssignedIdentityResourceId": None, "storageAccountConnectionStringName": None}

# Storage
deployment_config_storage_value = None
if (deployment_storage_name is not None):
deployment_storage = _validate_and_get_deployment_storage(cmd.cli_ctx, resource_group_name, deployment_storage_name)
deployment_storage_container = _get_deployment_storage_container(cmd, resource_group_name, deployment_storage_name, deployment_storage_container_name)
deployment_storage_container_name = deployment_storage_container.name
deployment_config_storage_value = getattr(deployment_storage.primary_endpoints, 'blob') + deployment_storage_container_name
functionapp_deployment_storage["value"] = deployment_config_storage_value

# Authentication
assign_identities = None
if (deployment_storage_auth_type != None):
functionapp_deployment_storage["authentication"]["type"] = deployment_storage_auth_type
if deployment_storage_auth_type == 'StorageAccountConnectionString':
deployment_storage_conn_string = _get_storage_connection_string(cmd.cli_ctx, deployment_storage)
update_app_settings(cmd, resource_group_name, name,
["DEPLOYMENT_STORAGE_CONNECTION_STRING={}".format(deployment_storage_conn_string)])
functionapp_deployment_storage["authentication"]["userAssignedIdentityResourceId"] = None
functionapp_deployment_storage["authentication"]["storageAccountConnectionStringName"] = "DEPLOYMENT_STORAGE_CONNECTION_STRING"
elif deployment_storage_auth_type == 'SystemAssignedIdentity':
assign_identities = ['[system]']
functionapp_deployment_storage["authentication"]["userAssignedIdentityResourceId"] = None
functionapp_deployment_storage["authentication"]["storageAccountConnectionStringName"] = None
elif deployment_storage_auth_type == 'UserAssignedIdentity':
deployment_storage_user_assigned_identity = _get_or_create_user_assigned_identity(cmd, resource_group_name, name, deployment_storage_auth_value, None)
functionapp_deployment_storage["authentication"]["userAssignedIdentityResourceId"] = deployment_storage_user_assigned_identity.id
functionapp_deployment_storage["authentication"]["storageAccountConnectionStringName"] = None
assign_identities = [deployment_storage_user_assigned_identity.id]
else:
raise ValidationError("Invalid value for --deployment-storage-auth-type. Please try again with a valid value.")
functionapp["properties"]["functionAppConfig"] = function_app_config

update_flex_functionapp(cmd, resource_group_name, name, functionapp)

client = web_client_factory(cmd.cli_ctx)
functionapp = client.web_apps.get(resource_group_name, name)
if deployment_storage_auth_type == 'UserAssignedIdentity':
assign_identity(cmd, resource_group_name, name, assign_identities)
if (_has_deployment_storage_role_assignment_on_resource(cmd.cli_ctx, deployment_storage,
Copy link
Author

Choose a reason for hiding this comment

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

I think the create flow may also need this check.

deployment_storage_user_assigned_identity.principal_id) == False):
_assign_deployment_storage_managed_identity_role(cmd.cli_ctx, deployment_storage,
deployment_storage_user_assigned_identity.principal_id)
else:
logger.warning("User assigned identity '%s' already has the role assignment on the storage account '%s'",
deployment_storage_user_assigned_identity.principal_id, deployment_storage_name)
elif deployment_storage_auth_type == 'SystemAssignedIdentity':
assign_identity(cmd, resource_group_name, name, assign_identities, 'Storage Blob Data Contributor',
None, deployment_storage.id)

poller = client.web_apps.begin_create_or_update(resource_group_name, name, functionapp)
functionapp = LongRunningOperation(cmd.cli_ctx)(poller)
return functionapp


# for any modifications to the non-optional parameters, adjust the reflection logic accordingly
# in the method
# pylint: disable=unused-argument
Expand Down Expand Up @@ -4987,6 +5089,13 @@ def _get_or_create_deployment_storage_container(cmd, resource_group_name, functi
return storage_container


def _get_deployment_storage_container(cmd, resource_group_name, deployment_storage_name,
deployment_storage_container_name):
storage_client = get_mgmt_service_client(cmd.cli_ctx, StorageManagementClient)
return storage_client.blob_containers.get(resource_group_name, deployment_storage_name,
deployment_storage_container_name)


def _get_or_create_user_assigned_identity(cmd, resource_group_name, functionapp_name, user_assigned_identity, location):
from azure.mgmt.msi import ManagedServiceIdentityClient
msi_client = get_mgmt_service_client(cmd.cli_ctx, ManagedServiceIdentityClient)
Expand Down Expand Up @@ -5046,6 +5155,23 @@ def _assign_deployment_storage_managed_identity_role(cli_ctx, deployment_storage
role_assignment_name=str(uuid.uuid4()), parameters=parameters)


def _has_deployment_storage_role_assignment_on_resource(cli_ctx, deployment_storage_account, principal_id):
from azure.cli.core.commands.client_factory import get_subscription_id
auth_client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_AUTHORIZATION)

sub_id = get_subscription_id(cli_ctx)
role_definition_id = "/subscriptions/{}/providers/Microsoft.Authorization/roleDefinitions/{}".format(
sub_id, STORAGE_BLOB_DATA_CONTRIBUTOR_ROLE_ID)

list_for_scope = auth_client.role_assignments.list_for_scope(deployment_storage_account.id)
for assignment in list_for_scope:
if assignment.role_definition_id.lower() == role_definition_id.lower() and \
assignment.principal_id.lower() == principal_id.lower():
return True

return False


def _parse_key_value_pairs(key_value_list):
key_value_list = key_value_list or []
result = {}
Expand Down