diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_params.py b/src/azure-cli/azure/cli/command_modules/appservice/_params.py index 95bbe0ec77b..3405c73af0c 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -37,6 +37,7 @@ ASE_KINDS = ['ASEv2', 'ASEv3'] ASE_OS_PREFERENCE_TYPES = ['Windows', 'Linux'] PUBLIC_NETWORK_ACCESS_MODES = ['Enabled', 'Disabled'] +DAPR_LOG_LEVELS = ['debug', 'error', 'info', 'warn'] # pylint: disable=too-many-statements, too-many-lines @@ -397,8 +398,15 @@ def load_arguments(self, _): help='the container registry server username') c.argument('registry_password', options_list=['--registry-password', '-p', c.deprecate(target='--docker-registry-server-password', redirect='--registry-password')], help='the container registry server password') - c.argument('min_replicas', type=int, help="The minimum number of replicas when create funtion app on container app", is_preview=True) - c.argument('max_replicas', type=int, help="The maximum number of replicas when create funtion app on container app", is_preview=True) + c.argument('min_replicas', type=int, help="The minimum number of replicas when create function app on container app", is_preview=True) + c.argument('max_replicas', type=int, help="The maximum number of replicas when create function app on container app", is_preview=True) + c.argument('enable_dapr', help="Enable/Disable Dapr for a function app on an Azure Container App environment", arg_type=get_three_state_flag(return_label=True)) + c.argument('dapr_app_id', help="The Dapr application identifier.") + c.argument('dapr_app_port', type=int, help="The port Dapr uses to communicate to the application.") + c.argument('dapr_http_max_request_size', type=int, options_list=['--dapr-http-max-request-size', '--dhmrs'], help="Max size of request body http and grpc servers in MB to handle uploading of large files.") + c.argument('dapr_http_read_buffer_size', type=int, options_list=['--dapr-http-read-buffer-size', '--dhrbs'], help="Max size of http header read buffer in KB to handle when sending multi-KB headers.") + c.argument('dapr_log_level', help="The log level for the Dapr sidecar", arg_type=get_enum_type(DAPR_LOG_LEVELS)) + c.argument('dapr_enable_api_logging', options_list=['--dapr-enable-api-logging', '--dal'], help="Enable/Disable API logging for the Dapr sidecar.", arg_type=get_three_state_flag(return_label=True)) with self.argument_context('webapp config connection-string list') as c: c.argument('name', arg_type=webapp_name_arg_type, id_part=None) @@ -737,8 +745,15 @@ def load_arguments(self, _): c.argument('registry_username', options_list=['--registry-username', '-d', c.deprecate(target='--docker-registry-server-user', redirect='--registry-username')], help='The container registry server username.') c.argument('registry_password', options_list=['--registry-password', '-w', c.deprecate(target='--docker-registry-server-password', redirect='--registry-password')], help='The container registry server password. Required for private registries.') - c.argument('min_replicas', type=int, help="The minimum number of replicas when create funtion app on container app", is_preview=True) - c.argument('max_replicas', type=int, help="The maximum number of replicas when create funtion app on container app", is_preview=True) + c.argument('min_replicas', type=int, help="The minimum number of replicas when create function app on container app", is_preview=True) + c.argument('max_replicas', type=int, help="The maximum number of replicas when create function app on container app", is_preview=True) + c.argument('enable_dapr', help="Enable/Disable Dapr for a function app on an Azure Container App environment", arg_type=get_three_state_flag(return_label=True)) + c.argument('dapr_app_id', help="The Dapr application identifier.") + c.argument('dapr_app_port', type=int, help="The port Dapr uses to communicate to the application.") + c.argument('dapr_http_max_request_size', type=int, options_list=['--dapr-http-max-request-size', '--dhmrs'], help="Max size of request body http and grpc servers in MB to handle uploading of large files.") + c.argument('dapr_http_read_buffer_size', type=int, options_list=['--dapr-http-read-buffer-size', '--dhrbs'], help="Max size of http header read buffer in KB to handle when sending multi-KB headers.") + c.argument('dapr_log_level', help="The log level for the Dapr sidecar", arg_type=get_enum_type(DAPR_LOG_LEVELS)) + c.argument('dapr_enable_api_logging', options_list=['--dapr-enable-api-logging', '--dal'], help="Enable/Disable API logging for the Dapr sidecar.", arg_type=get_three_state_flag(return_label=True)) c.argument('workspace', help="Name of an existing log analytics workspace to be used for the application insights component") with self.argument_context('functionapp cors credentials') as c: diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index c8208f2a642..3bdd0dde780 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -1780,7 +1780,14 @@ def update_container_settings(cmd, resource_group_name, name, docker_registry_se def update_container_settings_functionapp(cmd, resource_group_name, name, registry_server=None, image=None, registry_username=None, - registry_password=None, slot=None, min_replicas=None, max_replicas=None): + registry_password=None, slot=None, min_replicas=None, max_replicas=None, + enable_dapr=None, dapr_app_id=None, dapr_app_port=None, + dapr_http_max_request_size=None, dapr_http_read_buffer_size=None, + dapr_log_level=None, dapr_enable_api_logging=None): + if is_centauri_functionapp(cmd, resource_group_name, name): + update_dapr_config(cmd, resource_group_name, name, enable_dapr, dapr_app_id, dapr_app_port, + dapr_http_max_request_size, dapr_http_read_buffer_size, dapr_log_level, + dapr_enable_api_logging) return update_container_settings(cmd, resource_group_name, name, registry_server, image, registry_username, None, registry_password, multicontainer_config_type=None, @@ -3756,6 +3763,39 @@ def should_enable_distributed_tracing(consumption_plan_location, matched_runtime and image is None +def update_functionapp_polling(cmd, resource_group_name, name, functionapp): + try: + _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'update', None, functionapp) + except Exception as ex: # pylint: disable=broad-except + poll_url = ex.response.headers['Location'] if 'Location' in ex.response.headers else None + if ex.response.status_code == 202 and poll_url: + r = send_raw_request(cmd.cli_ctx, method='get', url=poll_url) + poll_timeout = time.time() + 60 * 2 # 2 minute timeout + + while r.status_code != 200 and time.time() < poll_timeout: + time.sleep(5) + r = send_raw_request(cmd.cli_ctx, method='get', url=poll_url) + else: + raise CLIError(ex) + + +def update_dapr_config(cmd, resource_group_name, name, enabled=None, app_id=None, app_port=None, + http_max_request_size=None, http_read_buffer_size=None, log_level=None, + enable_api_logging=None): + site = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get') + import inspect + frame = inspect.currentframe() + bool_flags = ['enabled', 'enable_api_logging'] + int_flags = ['app_port', 'http_max_request_size', 'http_read_buffer_size'] + args, _, _, values = inspect.getargvalues(frame) # pylint: disable=deprecated-method + for arg in args[3:]: + if arg in int_flags and values[arg] is not None: + values[arg] = validate_and_convert_to_int(arg, values[arg]) + if values.get(arg, None): + setattr(site.dapr_config, arg, values[arg] if arg not in bool_flags else values[arg] == 'true') + update_functionapp_polling(cmd, resource_group_name, name, site) + + def create_functionapp(cmd, resource_group_name, name, storage_account, plan=None, os_type=None, functions_version=None, runtime=None, runtime_version=None, consumption_plan_location=None, app_insights=None, app_insights_key=None, @@ -3764,7 +3804,9 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non registry_server=None, registry_password=None, registry_username=None, image=None, tags=None, assign_identities=None, role='Contributor', scope=None, vnet=None, subnet=None, https_only=False, - environment=None, min_replicas=None, max_replicas=None, workspace=None): + environment=None, min_replicas=None, max_replicas=None, workspace=None, + enable_dapr=False, dapr_app_id=None, dapr_app_port=None, dapr_http_max_request_size=None, + dapr_http_read_buffer_size=None, dapr_log_level=None, dapr_enable_api_logging=False): # pylint: disable=too-many-statements, too-many-branches if functions_version is None: logger.warning("No functions version specified so defaulting to 3. In the future, specifying a version will " @@ -3779,13 +3821,23 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non raise RequiredArgumentMissingError("usage error: parameters --min-replicas and --max-replicas must be " "used with parameter --environment, please provide the name " "of the container app environment using --environment.") + if any([enable_dapr, dapr_app_id, dapr_app_port, dapr_http_max_request_size, dapr_http_read_buffer_size, + dapr_log_level, dapr_enable_api_logging]) and environment is None: + raise RequiredArgumentMissingError("usage error: parameters --enable-dapr, --dapr-app-id, " + "--dapr-app-port, --dapr-http-max-request-size, " + "--dapr-http-read-buffer-size, --dapr-log-level and " + "dapr-enable-api-logging must be used with parameter --environment," + "please provide the name of the container app environment using " + "--environment.") from azure.mgmt.web.models import Site - SiteConfig, NameValuePair = cmd.get_models('SiteConfig', 'NameValuePair') + SiteConfig, NameValuePair, DaprConfig = cmd.get_models('SiteConfig', 'NameValuePair', 'DaprConfig') disable_app_insights = (disable_app_insights == "true") site_config = SiteConfig(app_settings=[]) client = web_client_factory(cmd.cli_ctx) + dapr_config = DaprConfig() + if vnet or subnet: if plan: if is_valid_resource_id(plan): @@ -3815,7 +3867,7 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non subnet_resource_id = None vnet_route_all_enabled = None - functionapp_def = Site(location=None, site_config=site_config, tags=tags, + functionapp_def = Site(location=None, site_config=site_config, dapr_config=dapr_config, tags=tags, virtual_network_subnet_id=subnet_resource_id, https_only=https_only, vnet_route_all_enabled=vnet_route_all_enabled) @@ -3975,6 +4027,17 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non if max_replicas is not None: site_config.function_app_scale_limit = max_replicas + if enable_dapr: + logger.warning("Please note while using Dapr Extension for Azure Functions, app port is " + "mandatory when using Dapr triggers and should be empty when using only Dapr bindings.") + dapr_config.enabled = True + dapr_config.app_id = dapr_app_id + dapr_config.app_port = dapr_app_port + dapr_config.http_max_request_size = dapr_http_max_request_size + dapr_config.http_read_buffer_size = dapr_http_read_buffer_size + dapr_config.log_level = dapr_log_level + dapr_config.enable_api_logging = dapr_enable_api_logging + managed_environment = get_managed_environment(cmd, resource_group_name, environment) location = managed_environment.location functionapp_def.location = location diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_functionapp_commands.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_functionapp_commands.py index 75db1a5a144..30725023d33 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_functionapp_commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_functionapp_commands.py @@ -508,6 +508,59 @@ def test_functionapp_consumption_linux_powershell(self, resource_group, storage_ self.cmd('functionapp config show -g {} -n {}'.format(resource_group, functionapp_name), checks=[ JMESPathCheck('linuxFxVersion', 'PowerShell|7.2')]) + +class FunctionappDapr(LiveScenarioTest): + @AllowLargeResponse(8192) + @ResourceGroupPreparer(location="northeurope") + @StorageAccountPreparer() + def test_functionapp_dapr_config_e2e(self, resource_group, storage_account): + functionapp_name = self.create_random_name( + 'functionappdapr', 24) + managed_environment_name = self.create_random_name( + 'managedenvironment', 40 + ) + + self.cmd('containerapp env create --name {} --resource-group {} --location {} --logs-destination none'.format( + managed_environment_name, + resource_group, + "northeurope" + )) + + self.cmd('functionapp create -g {} -n {} -s {} --environment {} --dapr-app-id daprappid --dapr-app-port 800 --dhmrs 4 --dhrbs 50 --dapr-log-level debug --enable-dapr true --functions-version 4'.format( + resource_group, + functionapp_name, + storage_account, + managed_environment_name + )).assert_with_checks([ + JMESPathCheck('daprConfig.enabled', True), + JMESPathCheck('daprConfig.appId', 'daprappid'), + JMESPathCheck('daprConfig.appPort', 800), + JMESPathCheck('daprConfig.httpReadBufferSize', 50), + JMESPathCheck('daprConfig.httpMaxRequestSize', 4), + JMESPathCheck('daprConfig.logLevel', 'debug'), + JMESPathCheck('daprConfig.enableApiLogging', False) + ]) + + time.sleep(1200) + + self.cmd('functionapp config container set -g {} -n {} --dapr-app-id daprappid1 --dapr-app-port 80 --dal --dhmrs 6 --dhrbs 60 --dapr-log-level warn --enable-dapr false'.format( + resource_group, + functionapp_name + )) + + time.sleep(1200) + + self.cmd('functionapp show -g {} -n {}'.format(resource_group, functionapp_name)).assert_with_checks([ + JMESPathCheck('daprConfig.enabled', False), + JMESPathCheck('daprConfig.appId', 'daprappid1'), + JMESPathCheck('daprConfig.appPort', 80), + JMESPathCheck('daprConfig.httpReadBufferSize', 60), + JMESPathCheck('daprConfig.httpMaxRequestSize', 6), + JMESPathCheck('daprConfig.logLevel', 'warn'), + JMESPathCheck('daprConfig.enableApiLogging', True) + ]) + + class FunctionAppManagedEnvironment(LiveScenarioTest): @ResourceGroupPreparer(location=WINDOWS_ASP_LOCATION_FUNCTIONAPP) @StorageAccountPreparer()