From 74daa6cb9df663bf07287c4c53d016b8ff51ff96 Mon Sep 17 00:00:00 2001 From: Chandni Patel Date: Wed, 13 May 2026 17:34:29 -0500 Subject: [PATCH 1/4] update commands --- .../cli/command_modules/appservice/_params.py | 18 + .../cli/command_modules/appservice/custom.py | 158 ++- .../latest/recordings/test_webapp_ssl.yaml | 1061 +++++++++++------ .../test_webapp_ssl_specify_hostname.yaml | 375 ++++-- .../latest/test_webapp_commands_thru_mock.py | 4 +- 5 files changed, 1110 insertions(+), 506 deletions(-) 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 1d97904d8b0..c24c2adb57d 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -706,6 +706,24 @@ def load_arguments(self, _): c.argument('strategy_type', options_list=['--type'], arg_type=get_enum_type(UPDATE_STRATEGY_TYPES), help="The update strategy type. Allowed values: Recreate, RollingUpdate.") + # Add optional 'name' parameter for functionapp SSL commands to support Flex Consumption apps + with self.argument_context('functionapp config ssl list') as c: + c.argument('name', options_list=['--name', '-n'], help='Name of the function app. Required for Flex Consumption apps to list site-scoped certificates.') + + with self.argument_context('functionapp config ssl show') as c: + c.argument('name', options_list=['--name', '-n'], help='Name of the function app. Required for Flex Consumption apps to show site-scoped certificates.') + + with self.argument_context('functionapp config ssl delete') as c: + c.argument('name', options_list=['--name', '-n'], help='Name of the function app. Required for Flex Consumption apps to delete site-scoped certificates.') + + # Add load_to_code parameter for functionapp SSL commands that create/update certificates (Flex Consumption only) + with self.argument_context('functionapp config ssl upload') as c: + c.argument('load_to_code', arg_type=get_three_state_flag(), help='For Flex Consumption apps only. Make the certificate accessible to app code. When set to true, the certificate can be loaded via the WEBSITE_LOAD_CERTIFICATES app setting.') + + with self.argument_context('functionapp config ssl import') as c: + c.argument('load_to_code', arg_type=get_three_state_flag(), help='For Flex Consumption apps only. Make the certificate accessible to app code. When set to true, the certificate can be loaded via the WEBSITE_LOAD_CERTIFICATES app setting.') + c.argument('enable_using_msi', arg_type=get_three_state_flag(), help='For Flex Consumption apps only. Enable Key Vault access using Managed Service Identity. When set to true, the app will use its managed identity to access Key Vault instead of service principal.') + with self.argument_context('webapp config connection-string list') as c: c.argument('name', arg_type=webapp_name_arg_type, id_part=None) 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 4f788cb9119..2d28f16ca7c 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- import ast +import base64 import threading import time import re @@ -6736,7 +6737,7 @@ def _get_log(url, headers, log_file=None): def upload_ssl_cert(cmd, resource_group_name, name, certificate_password, certificate_file, slot=None, - certificate_name=None): + certificate_name=None, load_to_code=None): Certificate = cmd.get_models('Certificate') client = web_client_factory(cmd.cli_ctx) webapp = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) @@ -6757,6 +6758,32 @@ def upload_ssl_cert(cmd, resource_group_name, webapp.location, resource_group_name) cert = Certificate(password=certificate_password, pfx_blob=cert_contents, location=webapp.location, server_farm_id=get_site_server_farm_id(webapp)) + + # Check if this is a Flex Consumption function app + is_flex = is_flex_functionapp(cmd.cli_ctx, resource_group_name, name) + + # For Flex Consumption apps, use the site-scoped certificates endpoint (slots not supported) + if is_flex: + # Build certificate envelope as dict to include loadCertificateToWebsitesSettings + # (not in SDK model yet) + cert_envelope = { + "location": webapp.location, + "properties": { + "password": certificate_password, + "pfxBlob": base64.b64encode(cert_contents).decode('utf-8'), + "serverFarmId": get_site_server_farm_id(webapp) + } + } + if load_to_code is not None: + cert_envelope["properties"]["loadCertificateToWebsitesSettings"] = { + "loadToWebsite": load_to_code + } + return client.site_certificates.create_or_update( + resource_group_name=resource_group_name, + name=name, + certificate_name=cert_name, + certificate_envelope=cert_envelope + ) return client.certificates.create_or_update(resource_group_name, cert_name, cert) @@ -6774,18 +6801,43 @@ def _get_cert(certificate_password, certificate_file): return thumbprint -def list_ssl_certs(cmd, resource_group_name): +def list_ssl_certs(cmd, resource_group_name, name=None): client = web_client_factory(cmd.cli_ctx) + + # Check if this is a Flex Consumption function app + if name and is_flex_functionapp(cmd.cli_ctx, resource_group_name, name): + return client.site_certificates.list(resource_group_name=resource_group_name, name=name) return client.certificates.list_by_resource_group(resource_group_name) -def show_ssl_cert(cmd, resource_group_name, certificate_name): +def show_ssl_cert(cmd, resource_group_name, certificate_name, name=None): client = web_client_factory(cmd.cli_ctx) + + # Check if this is a Flex Consumption function app + if name and is_flex_functionapp(cmd.cli_ctx, resource_group_name, name): + return client.site_certificates.get( + resource_group_name=resource_group_name, + name=name, + certificate_name=certificate_name + ) return client.certificates.get(resource_group_name, certificate_name) -def delete_ssl_cert(cmd, resource_group_name, certificate_thumbprint): +def delete_ssl_cert(cmd, resource_group_name, certificate_thumbprint, name=None): client = web_client_factory(cmd.cli_ctx) + + # Check if this is a Flex Consumption function app + if name and is_flex_functionapp(cmd.cli_ctx, resource_group_name, name): + site_certs = client.site_certificates.list(resource_group_name=resource_group_name, name=name) + for site_cert in site_certs: + if site_cert.thumbprint == certificate_thumbprint: + return client.site_certificates.delete( + resource_group_name=resource_group_name, + name=name, + certificate_name=site_cert.name + ) + raise ResourceNotFoundError("Certificate for thumbprint '{}' not found".format(certificate_thumbprint)) + webapp_certs = client.certificates.list_by_resource_group(resource_group_name) for webapp_cert in webapp_certs: if webapp_cert.thumbprint == certificate_thumbprint: @@ -6793,7 +6845,8 @@ def delete_ssl_cert(cmd, resource_group_name, certificate_thumbprint): raise ResourceNotFoundError("Certificate for thumbprint '{}' not found".format(certificate_thumbprint)) -def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_name, name=None, certificate_name=None): +def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_name, name=None, certificate_name=None, + load_to_code=None, enable_using_msi=None): Certificate = cmd.get_models('Certificate') client = web_client_factory(cmd.cli_ctx) @@ -6841,17 +6894,20 @@ def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_n from azure.cli.core.commands.client_factory import get_subscription_id subscription_id = get_subscription_id(cmd.cli_ctx) if cloud_type.lower() == PUBLIC_CLOUD.lower(): + # Check if app_service_certificate_orders operation group is available in the SDK if kv_subscription.lower() != subscription_id.lower(): diff_subscription_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_APPSERVICE, subscription_id=kv_subscription) - ascs = diff_subscription_client.app_service_certificate_orders.list(api_version=VERSION_2022_09_01) + cert_orders_client = diff_subscription_client else: - ascs = client.app_service_certificate_orders.list(api_version=VERSION_2022_09_01) + cert_orders_client = client - kv_secret_name = None - for asc in ascs: - if asc.name == key_vault_certificate_name: - kv_secret_name = asc.certificates[key_vault_certificate_name].key_vault_secret_name + if hasattr(cert_orders_client, 'app_service_certificate_orders'): + ascs = cert_orders_client.app_service_certificate_orders.list(api_version=VERSION_2022_09_01) + for asc in ascs: + if asc.name == key_vault_certificate_name: + kv_secret_name = asc.certificates[key_vault_certificate_name].key_vault_secret_name + break # if kv_secret_name is not populated, it is not an appservice certificate, proceed for KV certificates if not kv_secret_name: @@ -6872,6 +6928,31 @@ def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_n kv_cert_def = Certificate(location=location, key_vault_id=kv_id, password='', key_vault_secret_name=kv_secret_name) + # Check if this is a Flex Consumption function app + if name and is_flex_functionapp(cmd.cli_ctx, resource_group_name, name): + # Build certificate envelope as dict to include loadCertificateToWebsitesSettings + # (not in SDK model yet) + cert_envelope = { + "location": location, + "properties": { + "keyVaultId": kv_id, + "password": "", + "keyVaultSecretName": kv_secret_name + } + } + if load_to_code is not None: + cert_envelope["properties"]["loadCertificateToWebsitesSettings"] = { + "loadToWebsite": load_to_code + } + if enable_using_msi is not None: + cert_envelope["properties"]["enableKeyVaultAccessUsingMSI"] = enable_using_msi + return client.site_certificates.create_or_update( + resource_group_name=resource_group_name, + name=name, + certificate_name=cert_name, + certificate_envelope=cert_envelope + ) + return client.certificates.create_or_update(name=cert_name, resource_group_name=resource_group_name, certificate_envelope=kv_cert_def) @@ -6903,9 +6984,23 @@ def create_managed_ssl_cert(cmd, resource_group_name, name, hostname, slot=None, easy_cert_def = Certificate(location=location, canonical_name=hostname, server_farm_id=server_farm_id, password='') + # Check if this is a Flex Consumption function app + is_flex = is_flex_functionapp(cmd.cli_ctx, resource_group_name, name) + + # Default certificate_name to hostname if not provided + if not certificate_name: + certificate_name = hostname + # TODO: Update manual polling to use LongRunningOperation once backend API & new SDK supports polling try: - certificate_name = hostname if not certificate_name else certificate_name + # For Flex Consumption apps, use the site-scoped certificates endpoint (slots not supported) + if is_flex: + return client.site_certificates.create_or_update( + resource_group_name=resource_group_name, + name=name, + certificate_name=certificate_name, + certificate_envelope=easy_cert_def + ) return client.certificates.create_or_update(name=certificate_name, resource_group_name=resource_group_name, certificate_envelope=easy_cert_def) except Exception as ex: @@ -6973,24 +7068,37 @@ def _update_ssl_binding(cmd, resource_group_name, name, certificate_thumbprint, if not webapp: raise ResourceNotFoundError("'{}' app doesn't exist".format(name)) - cert_resource_group_name = parse_resource_id(get_site_server_farm_id(webapp))['resource_group'] - webapp_certs = client.certificates.list_by_resource_group(cert_resource_group_name) + # Check if this is a Flex Consumption function app + is_flex = is_flex_functionapp(cmd.cli_ctx, resource_group_name, name) found_cert = None - # search for a cert that matches in the app service plan's RG - for webapp_cert in webapp_certs: - if webapp_cert.thumbprint == certificate_thumbprint: - found_cert = webapp_cert - # search for a cert that matches in the webapp's RG - if not found_cert: - webapp_certs = client.certificates.list_by_resource_group(resource_group_name) + + # For Flex Consumption apps, search in site-scoped certificates + if is_flex: + site_certs = client.site_certificates.list(resource_group_name=resource_group_name, name=name) + for site_cert in site_certs: + if site_cert.thumbprint == certificate_thumbprint: + found_cert = site_cert + break + # If not a Flex app, search in regular certificates + else: + cert_resource_group_name = parse_resource_id(get_site_server_farm_id(webapp))['resource_group'] + webapp_certs = client.certificates.list_by_resource_group(cert_resource_group_name) + # search for a cert that matches in the app service plan's RG for webapp_cert in webapp_certs: if webapp_cert.thumbprint == certificate_thumbprint: found_cert = webapp_cert - # search for a cert that matches in the subscription, filtering on the serverfarm - if not found_cert: - sub_certs = client.certificates.list(filter=f"ServerFarmId eq '{get_site_server_farm_id(webapp)}'") - found_cert = next(iter([c for c in sub_certs if c.thumbprint == certificate_thumbprint]), None) + # search for a cert that matches in the webapp's RG + if not found_cert: + webapp_certs = client.certificates.list_by_resource_group(resource_group_name) + for webapp_cert in webapp_certs: + if webapp_cert.thumbprint == certificate_thumbprint: + found_cert = webapp_cert + # search for a cert that matches in the subscription, filtering on the serverfarm + if not found_cert: + sub_certs = client.certificates.list(filter=f"ServerFarmId eq '{get_site_server_farm_id(webapp)}'") + found_cert = next(iter([c for c in sub_certs if c.thumbprint == certificate_thumbprint]), None) + if found_cert: if not hostname: if len(found_cert.host_names) == 1 and not found_cert.host_names[0].startswith('*'): diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl.yaml b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl.yaml index 91779eef7d0..101979b265a 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl.yaml +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl.yaml @@ -18,7 +18,7 @@ interactions: uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001?api-version=2024-11-01 response: body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001","name":"clitest.rg000001","type":"Microsoft.Resources/resourceGroups","location":"westeurope","tags":{"product":"azurecli","cause":"automation","test":"test_webapp_ssl","date":"2026-05-08T15:53:32Z","module":"appservice"},"properties":{"provisioningState":"Succeeded"}}' + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001","name":"clitest.rg000001","type":"Microsoft.Resources/resourceGroups","location":"westeurope","tags":{"product":"azurecli","cause":"automation","test":"test_webapp_ssl","date":"2026-05-14T15:47:07Z","module":"appservice"},"properties":{"provisioningState":"Succeeded"}}' headers: cache-control: - no-cache @@ -27,7 +27,7 @@ interactions: content-type: - application/json; charset=utf-8 date: - - Fri, 08 May 2026 15:53:34 GMT + - Thu, 14 May 2026 15:47:11 GMT expires: - '-1' pragma: @@ -41,7 +41,7 @@ interactions: x-ms-ratelimit-remaining-subscription-global-reads: - '16499' x-msedge-ref: - - 'Ref A: 3713748C0AE4411291004FD0ADB28A8A Ref B: SN4AA2022304027 Ref C: 2026-05-08T15:53:34Z' + - 'Ref A: 63A6B265122E4BA2988CCF137E10FA7F Ref B: SN4AA2022303029 Ref C: 2026-05-14T15:47:12Z' status: code: 200 message: OK @@ -69,19 +69,19 @@ interactions: uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002?api-version=2025-03-01 response: body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002","name":"ssl-test-plan000002","type":"Microsoft.Web/serverfarms","kind":"app","location":"westeurope","tags":{"plan":"plan1"},"properties":{"serverFarmId":80971,"name":"ssl-test-plan000002","sku":{"name":"S1","tier":"Standard","size":"S1","family":"S","capacity":1},"workerSize":"Small","workerSizeId":0,"workerTierName":null,"numberOfWorkers":1,"currentWorkerSize":"Small","currentWorkerSizeId":0,"currentNumberOfWorkers":1,"status":"Ready","webSpace":"clitest.rg000001-WestEuropewebspace","subscription":"e160ca65-836f-4be6-8ef5-e6234b90de87","adminSiteName":null,"hostingEnvironment":null,"hostingEnvironmentProfile":null,"maximumNumberOfWorkers":0,"planName":"VirtualDedicatedPlan","adminRuntimeSiteName":null,"computeMode":"Dedicated","siteMode":null,"geoRegion":"West - Europe","perSiteScaling":false,"elasticScaleEnabled":false,"maximumElasticWorkerCount":1,"numberOfSites":0,"hostingEnvironmentId":null,"isSpot":false,"spotExpirationTime":null,"freeOfferExpirationTime":null,"tags":{"plan":"plan1"},"kind":"app","resourceGroup":"clitest.rg000001","reserved":false,"isXenon":false,"hyperV":false,"mdmId":"waws-prod-am2-605_80971","targetWorkerCount":0,"targetWorkerSizeId":0,"targetWorkerSku":null,"provisioningState":"Succeeded","webSiteId":null,"existingServerFarmIds":null,"kubeEnvironmentProfile":null,"zoneRedundant":false,"maximumNumberOfZones":3,"currentNumberOfZonesUtilized":1,"migrateToVMSS":null,"vnetConnectionsUsed":null,"vnetConnectionsMax":null,"createdTime":"2026-05-08T15:53:43.1566667","asyncScalingEnabled":false,"isCustomMode":false,"powerState":"Running","eligibleLogCategories":""},"sku":{"name":"S1","tier":"Standard","size":"S1","family":"S","capacity":1}}' + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002","name":"ssl-test-plan000002","type":"Microsoft.Web/serverfarms","kind":"app","location":"westeurope","tags":{"plan":"plan1"},"properties":{"serverFarmId":124189,"name":"ssl-test-plan000002","sku":{"name":"S1","tier":"Standard","size":"S1","family":"S","capacity":1},"workerSize":"Small","workerSizeId":0,"workerTierName":null,"numberOfWorkers":1,"currentWorkerSize":"Small","currentWorkerSizeId":0,"currentNumberOfWorkers":1,"status":"Ready","webSpace":"clitest.rg000001-WestEuropewebspace","subscription":"e160ca65-836f-4be6-8ef5-e6234b90de87","adminSiteName":null,"hostingEnvironment":null,"hostingEnvironmentProfile":null,"maximumNumberOfWorkers":0,"planName":"VirtualDedicatedPlan","adminRuntimeSiteName":null,"computeMode":"Dedicated","siteMode":null,"geoRegion":"West + Europe","perSiteScaling":false,"elasticScaleEnabled":false,"maximumElasticWorkerCount":1,"numberOfSites":0,"hostingEnvironmentId":null,"isSpot":false,"spotExpirationTime":null,"freeOfferExpirationTime":null,"tags":{"plan":"plan1"},"kind":"app","resourceGroup":"clitest.rg000001","reserved":false,"isXenon":false,"hyperV":false,"mdmId":"waws-prod-am2-347_124189","targetWorkerCount":0,"targetWorkerSizeId":0,"targetWorkerSku":null,"provisioningState":"Succeeded","webSiteId":null,"existingServerFarmIds":null,"kubeEnvironmentProfile":null,"zoneRedundant":false,"maximumNumberOfZones":3,"currentNumberOfZonesUtilized":1,"migrateToVMSS":null,"vnetConnectionsUsed":null,"vnetConnectionsMax":null,"createdTime":"2026-05-14T15:47:18.8733333","asyncScalingEnabled":false,"isCustomMode":false,"powerState":"Running","eligibleLogCategories":""},"sku":{"name":"S1","tier":"Standard","size":"S1","family":"S","capacity":1}}' headers: cache-control: - no-cache content-length: - - '1832' + - '1834' content-type: - application/json date: - - Fri, 08 May 2026 15:53:45 GMT + - Thu, 14 May 2026 15:47:20 GMT etag: - - 1DCDF02D8F2ED40 + - 1DCE3B8F26609D5 expires: - '-1' pragma: @@ -95,13 +95,13 @@ interactions: x-content-type-options: - nosniff x-ms-operation-identifier: - - tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47,objectId=f338340e-ef13-48e1-aa76-6cde16f7c55f/southcentralus/89f3ba91-95a1-42d3-9609-e2a455f8b7c3 + - tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47,objectId=f338340e-ef13-48e1-aa76-6cde16f7c55f/westeurope/fe6db366-df1d-4cf8-aa4a-e50099b2cf74 x-ms-ratelimit-remaining-subscription-global-writes: - '12000' x-ms-ratelimit-remaining-subscription-writes: - '800' x-msedge-ref: - - 'Ref A: 187D38AAF9464D8C88343308D69C05AB Ref B: SN4AA2022302023 Ref C: 2026-05-08T15:53:35Z' + - 'Ref A: 34E93F60C6FB438DBACE70E507421E42 Ref B: SN4AA2022305039 Ref C: 2026-05-14T15:47:12Z' x-powered-by: - ASP.NET status: @@ -127,17 +127,17 @@ interactions: response: body: string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002","name":"ssl-test-plan000002","type":"Microsoft.Web/serverfarms","kind":"app","location":"West - Europe","tags":{"plan":"plan1"},"properties":{"serverFarmId":80971,"name":"ssl-test-plan000002","workerSize":"Small","workerSizeId":0,"workerTierName":null,"numberOfWorkers":1,"currentWorkerSize":"Small","currentWorkerSizeId":0,"currentNumberOfWorkers":1,"status":"Ready","webSpace":"clitest.rg000001-WestEuropewebspace","subscription":"e160ca65-836f-4be6-8ef5-e6234b90de87","adminSiteName":null,"hostingEnvironment":null,"hostingEnvironmentProfile":null,"maximumNumberOfWorkers":10,"planName":"VirtualDedicatedPlan","adminRuntimeSiteName":null,"computeMode":"Dedicated","siteMode":null,"geoRegion":"West - Europe","perSiteScaling":false,"elasticScaleEnabled":false,"maximumElasticWorkerCount":1,"numberOfSites":0,"hostingEnvironmentId":null,"isSpot":false,"spotExpirationTime":null,"freeOfferExpirationTime":null,"tags":{"plan":"plan1"},"kind":"app","resourceGroup":"clitest.rg000001","reserved":false,"isXenon":false,"hyperV":false,"mdmId":"waws-prod-am2-605_80971","targetWorkerCount":0,"targetWorkerSizeId":0,"targetWorkerSku":null,"provisioningState":"Succeeded","webSiteId":null,"existingServerFarmIds":null,"kubeEnvironmentProfile":null,"zoneRedundant":false,"maximumNumberOfZones":3,"currentNumberOfZonesUtilized":1,"migrateToVMSS":null,"vnetConnectionsUsed":0,"vnetConnectionsMax":2,"createdTime":"2026-05-08T15:53:43.1566667","asyncScalingEnabled":false,"isCustomMode":false,"powerState":"Running","eligibleLogCategories":""},"sku":{"name":"S1","tier":"Standard","size":"S1","family":"S","capacity":1}}' + Europe","tags":{"plan":"plan1"},"properties":{"serverFarmId":124189,"name":"ssl-test-plan000002","workerSize":"Small","workerSizeId":0,"workerTierName":null,"numberOfWorkers":1,"currentWorkerSize":"Small","currentWorkerSizeId":0,"currentNumberOfWorkers":1,"status":"Ready","webSpace":"clitest.rg000001-WestEuropewebspace","subscription":"e160ca65-836f-4be6-8ef5-e6234b90de87","adminSiteName":null,"hostingEnvironment":null,"hostingEnvironmentProfile":null,"maximumNumberOfWorkers":10,"planName":"VirtualDedicatedPlan","adminRuntimeSiteName":null,"computeMode":"Dedicated","siteMode":null,"geoRegion":"West + Europe","perSiteScaling":false,"elasticScaleEnabled":false,"maximumElasticWorkerCount":1,"numberOfSites":0,"hostingEnvironmentId":null,"isSpot":false,"spotExpirationTime":null,"freeOfferExpirationTime":null,"tags":{"plan":"plan1"},"kind":"app","resourceGroup":"clitest.rg000001","reserved":false,"isXenon":false,"hyperV":false,"mdmId":"waws-prod-am2-347_124189","targetWorkerCount":0,"targetWorkerSizeId":0,"targetWorkerSku":null,"provisioningState":"Succeeded","webSiteId":null,"existingServerFarmIds":null,"kubeEnvironmentProfile":null,"zoneRedundant":false,"maximumNumberOfZones":3,"currentNumberOfZonesUtilized":1,"migrateToVMSS":null,"vnetConnectionsUsed":0,"vnetConnectionsMax":2,"createdTime":"2026-05-14T15:47:18.8733333","asyncScalingEnabled":false,"isCustomMode":false,"powerState":"Running","eligibleLogCategories":""},"sku":{"name":"S1","tier":"Standard","size":"S1","family":"S","capacity":1}}' headers: cache-control: - no-cache content-length: - - '1752' + - '1754' content-type: - application/json date: - - Fri, 08 May 2026 15:53:47 GMT + - Thu, 14 May 2026 15:47:22 GMT expires: - '-1' pragma: @@ -153,7 +153,7 @@ interactions: x-ms-ratelimit-remaining-subscription-global-reads: - '16499' x-msedge-ref: - - 'Ref A: FDD4521CB2DD4567B89BE8CD9620205D Ref B: SN4AA2022304021 Ref C: 2026-05-08T15:53:47Z' + - 'Ref A: E2BA40D1A0B345E28EF3B1255AF2E45B Ref B: SN4AA2022302019 Ref C: 2026-05-14T15:47:21Z' x-powered-by: - ASP.NET status: @@ -179,17 +179,17 @@ interactions: response: body: string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002","name":"ssl-test-plan000002","type":"Microsoft.Web/serverfarms","kind":"app","location":"West - Europe","tags":{"plan":"plan1"},"properties":{"serverFarmId":80971,"name":"ssl-test-plan000002","workerSize":"Small","workerSizeId":0,"workerTierName":null,"numberOfWorkers":1,"currentWorkerSize":"Small","currentWorkerSizeId":0,"currentNumberOfWorkers":1,"status":"Ready","webSpace":"clitest.rg000001-WestEuropewebspace","subscription":"e160ca65-836f-4be6-8ef5-e6234b90de87","adminSiteName":null,"hostingEnvironment":null,"hostingEnvironmentProfile":null,"maximumNumberOfWorkers":10,"planName":"VirtualDedicatedPlan","adminRuntimeSiteName":null,"computeMode":"Dedicated","siteMode":null,"geoRegion":"West - Europe","perSiteScaling":false,"elasticScaleEnabled":false,"maximumElasticWorkerCount":1,"numberOfSites":0,"hostingEnvironmentId":null,"isSpot":false,"spotExpirationTime":null,"freeOfferExpirationTime":null,"tags":{"plan":"plan1"},"kind":"app","resourceGroup":"clitest.rg000001","reserved":false,"isXenon":false,"hyperV":false,"mdmId":"waws-prod-am2-605_80971","targetWorkerCount":0,"targetWorkerSizeId":0,"targetWorkerSku":null,"provisioningState":"Succeeded","webSiteId":null,"existingServerFarmIds":null,"kubeEnvironmentProfile":null,"zoneRedundant":false,"maximumNumberOfZones":3,"currentNumberOfZonesUtilized":1,"migrateToVMSS":null,"vnetConnectionsUsed":0,"vnetConnectionsMax":2,"createdTime":"2026-05-08T15:53:43.1566667","asyncScalingEnabled":false,"isCustomMode":false,"powerState":"Running","eligibleLogCategories":""},"sku":{"name":"S1","tier":"Standard","size":"S1","family":"S","capacity":1}}' + Europe","tags":{"plan":"plan1"},"properties":{"serverFarmId":124189,"name":"ssl-test-plan000002","workerSize":"Small","workerSizeId":0,"workerTierName":null,"numberOfWorkers":1,"currentWorkerSize":"Small","currentWorkerSizeId":0,"currentNumberOfWorkers":1,"status":"Ready","webSpace":"clitest.rg000001-WestEuropewebspace","subscription":"e160ca65-836f-4be6-8ef5-e6234b90de87","adminSiteName":null,"hostingEnvironment":null,"hostingEnvironmentProfile":null,"maximumNumberOfWorkers":10,"planName":"VirtualDedicatedPlan","adminRuntimeSiteName":null,"computeMode":"Dedicated","siteMode":null,"geoRegion":"West + Europe","perSiteScaling":false,"elasticScaleEnabled":false,"maximumElasticWorkerCount":1,"numberOfSites":0,"hostingEnvironmentId":null,"isSpot":false,"spotExpirationTime":null,"freeOfferExpirationTime":null,"tags":{"plan":"plan1"},"kind":"app","resourceGroup":"clitest.rg000001","reserved":false,"isXenon":false,"hyperV":false,"mdmId":"waws-prod-am2-347_124189","targetWorkerCount":0,"targetWorkerSizeId":0,"targetWorkerSku":null,"provisioningState":"Succeeded","webSiteId":null,"existingServerFarmIds":null,"kubeEnvironmentProfile":null,"zoneRedundant":false,"maximumNumberOfZones":3,"currentNumberOfZonesUtilized":1,"migrateToVMSS":null,"vnetConnectionsUsed":0,"vnetConnectionsMax":2,"createdTime":"2026-05-14T15:47:18.8733333","asyncScalingEnabled":false,"isCustomMode":false,"powerState":"Running","eligibleLogCategories":""},"sku":{"name":"S1","tier":"Standard","size":"S1","family":"S","capacity":1}}' headers: cache-control: - no-cache content-length: - - '1752' + - '1754' content-type: - application/json date: - - Fri, 08 May 2026 15:53:48 GMT + - Thu, 14 May 2026 15:47:22 GMT expires: - '-1' pragma: @@ -205,7 +205,7 @@ interactions: x-ms-ratelimit-remaining-subscription-global-reads: - '16499' x-msedge-ref: - - 'Ref A: F65C0933E9A94CE38686A9BF2F5A8738 Ref B: SN4AA2022302051 Ref C: 2026-05-08T15:53:48Z' + - 'Ref A: CCEF7EA16E69407CB6B08D742399016B Ref B: SN4AA2022301011 Ref C: 2026-05-14T15:47:23Z' x-powered-by: - ASP.NET status: @@ -243,7 +243,7 @@ interactions: content-type: - application/json date: - - Fri, 08 May 2026 15:53:49 GMT + - Thu, 14 May 2026 15:47:24 GMT expires: - '-1' pragma: @@ -257,11 +257,11 @@ interactions: x-content-type-options: - nosniff x-ms-operation-identifier: - - tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47,objectId=f338340e-ef13-48e1-aa76-6cde16f7c55f/southcentralus/5a1bcdba-e317-4fcb-8c80-a5cea0b56c2e + - tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47,objectId=f338340e-ef13-48e1-aa76-6cde16f7c55f/southcentralus/58094b75-457d-4fb4-b62d-7dda51cddd87 x-ms-ratelimit-remaining-subscription-global-reads: - '16499' x-msedge-ref: - - 'Ref A: 1BE98B4E5CC04A48BEC2411A58EF6C05 Ref B: SN4AA2022303047 Ref C: 2026-05-08T15:53:49Z' + - 'Ref A: 0EA3DE02590A449E924CFD765D58248B Ref B: SN4AA2022302031 Ref C: 2026-05-14T15:47:24Z' x-powered-by: - ASP.NET status: @@ -708,7 +708,7 @@ interactions: content-type: - application/json date: - - Fri, 08 May 2026 15:53:50 GMT + - Thu, 14 May 2026 15:47:27 GMT expires: - '-1' pragma: @@ -724,7 +724,7 @@ interactions: x-ms-operation-identifier: - '' x-msedge-ref: - - 'Ref A: 82AB0EBD7D2F4EEEA343152F5234EFB3 Ref B: SN4AA2022302011 Ref C: 2026-05-08T15:53:50Z' + - 'Ref A: 7BDC4A129DFB49EEA3A2B2A7F89D900F Ref B: SN4AA2022305019 Ref C: 2026-05-14T15:47:25Z' x-powered-by: - ASP.NET status: @@ -732,8 +732,9 @@ interactions: message: OK - request: body: '{"location": "West Europe", "tags": {"web": "web1"}, "properties": {"httpsOnly": - false, "siteConfig": {"appSettings": [{"name": "WEBSITE_NODE_DEFAULT_VERSION", - "value": "~22"}], "alwaysOn": true}, "serverFarmId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002"}}' + false, "serverFarmId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002", + "siteConfig": {"appSettings": [{"name": "WEBSITE_NODE_DEFAULT_VERSION", "value": + "~22"}], "alwaysOn": true}}}' headers: Accept: - application/json @@ -756,20 +757,20 @@ interactions: response: body: string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003","name":"web-ssl-test000003","type":"Microsoft.Web/sites","kind":"app","location":"West - Europe","tags":{"web":"web1"},"properties":{"name":"web-ssl-test000003","state":"Running","hostNames":["web-ssl-test000003.azurewebsites.net"],"webSpace":"clitest.rg000001-WestEuropewebspace","selfLink":"https://waws-prod-am2-605.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/clitest.rg000001-WestEuropewebspace/sites/web-ssl-test000003","repositorySiteName":"web-ssl-test000003","owner":null,"usageState":"Normal","enabled":true,"adminEnabled":true,"siteScopedCertificatesEnabled":false,"afdEnabled":false,"enabledHostNames":["web-ssl-test000003.azurewebsites.net","web-ssl-test000003.scm.azurewebsites.net"],"siteProperties":{"metadata":null,"properties":[{"name":"LinuxFxVersion","value":""},{"name":"WindowsFxVersion","value":null}],"appSettings":null},"availabilityState":"Normal","sslCertificates":null,"csrs":[],"cers":null,"siteMode":null,"hostNameSslStates":[{"name":"web-ssl-test000003.azurewebsites.net","sslState":"Disabled","ipBasedSslResult":null,"virtualIP":null,"virtualIPv6":null,"thumbprint":null,"certificateResourceId":null,"toUpdate":null,"toUpdateIpBasedSsl":null,"ipBasedSslState":"NotConfigured","hostType":"Standard"},{"name":"web-ssl-test000003.scm.azurewebsites.net","sslState":"Disabled","ipBasedSslResult":null,"virtualIP":null,"virtualIPv6":null,"thumbprint":null,"certificateResourceId":null,"toUpdate":null,"toUpdateIpBasedSsl":null,"ipBasedSslState":"NotConfigured","hostType":"Repository"}],"hostNamePrivateStates":[],"computeMode":null,"serverFarm":null,"serverFarmId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002","reserved":false,"isXenon":false,"hyperV":false,"sandboxType":null,"lastModifiedTimeUtc":"2026-05-08T15:53:53.3566667","storageRecoveryDefaultState":"Running","contentAvailabilityState":"Normal","runtimeAvailabilityState":"Normal","dnsConfiguration":{},"containerAllocationSubnet":null,"useContainerLocalhostBindings":null,"outboundVnetRouting":{"allTraffic":false,"applicationTraffic":false,"contentShareTraffic":false,"imagePullTraffic":false,"backupRestoreTraffic":false,"managedIdentityTraffic":false},"legacyServiceEndpointTrafficEvaluation":null,"siteConfig":{"numberOfWorkers":1,"defaultDocuments":null,"netFrameworkVersion":null,"phpVersion":null,"pythonVersion":null,"nodeVersion":null,"powerShellVersion":null,"linuxFxVersion":"","windowsFxVersion":null,"sandboxType":null,"windowsConfiguredStacks":null,"requestTracingEnabled":null,"remoteDebuggingEnabled":null,"remoteDebuggingVersion":null,"httpLoggingEnabled":null,"azureMonitorLogCategories":null,"acrUseManagedIdentityCreds":false,"acrUserManagedIdentityID":null,"logsDirectorySizeLimit":null,"detailedErrorLoggingEnabled":null,"publishingUsername":null,"publishingPassword":null,"appSettings":null,"metadata":null,"connectionStrings":null,"machineKey":null,"handlerMappings":null,"documentRoot":null,"scmType":null,"use32BitWorkerProcess":null,"webSocketsEnabled":null,"alwaysOn":false,"javaVersion":null,"javaContainer":null,"javaContainerVersion":null,"appCommandLine":null,"managedPipelineMode":null,"virtualApplications":null,"winAuthAdminState":null,"winAuthTenantState":null,"customAppPoolIdentityAdminState":null,"customAppPoolIdentityTenantState":null,"runtimeADUser":null,"runtimeADUserPassword":null,"loadBalancing":null,"routingRules":null,"experiments":null,"limits":null,"autoHealEnabled":null,"autoHealRules":null,"tracingOptions":null,"vnetName":null,"vnetRouteAllEnabled":null,"vnetPrivatePortsCount":null,"publicNetworkAccess":null,"cors":null,"push":null,"apiDefinition":null,"apiManagementConfig":null,"autoSwapSlotName":null,"localMySqlEnabled":null,"managedServiceIdentityId":null,"xManagedServiceIdentityId":null,"keyVaultReferenceIdentity":null,"ipSecurityRestrictions":[{"ipAddress":"Any","action":"Allow","priority":2147483647,"name":"Allow + Europe","tags":{"web":"web1"},"properties":{"name":"web-ssl-test000003","state":"Running","hostNames":["web-ssl-test000003.azurewebsites.net"],"webSpace":"clitest.rg000001-WestEuropewebspace","selfLink":"https://waws-prod-am2-347.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/clitest.rg000001-WestEuropewebspace/sites/web-ssl-test000003","repositorySiteName":"web-ssl-test000003","owner":null,"usageState":"Normal","enabled":true,"adminEnabled":true,"siteScopedCertificatesEnabled":false,"afdEnabled":false,"enabledHostNames":["web-ssl-test000003.azurewebsites.net","web-ssl-test000003.scm.azurewebsites.net"],"siteProperties":{"metadata":null,"properties":[{"name":"LinuxFxVersion","value":""},{"name":"WindowsFxVersion","value":null}],"appSettings":null},"availabilityState":"Normal","sslCertificates":null,"csrs":[],"cers":null,"siteMode":null,"hostNameSslStates":[{"name":"web-ssl-test000003.azurewebsites.net","sslState":"Disabled","ipBasedSslResult":null,"virtualIP":null,"virtualIPv6":null,"thumbprint":null,"certificateResourceId":null,"toUpdate":null,"toUpdateIpBasedSsl":null,"ipBasedSslState":"NotConfigured","hostType":"Standard"},{"name":"web-ssl-test000003.scm.azurewebsites.net","sslState":"Disabled","ipBasedSslResult":null,"virtualIP":null,"virtualIPv6":null,"thumbprint":null,"certificateResourceId":null,"toUpdate":null,"toUpdateIpBasedSsl":null,"ipBasedSslState":"NotConfigured","hostType":"Repository"}],"hostNamePrivateStates":[],"computeMode":null,"serverFarm":null,"serverFarmId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002","reserved":false,"isXenon":false,"hyperV":false,"sandboxType":null,"lastModifiedTimeUtc":"2026-05-14T15:47:30.4566667","storageRecoveryDefaultState":"Running","contentAvailabilityState":"Normal","runtimeAvailabilityState":"Normal","dnsConfiguration":{},"containerAllocationSubnet":null,"useContainerLocalhostBindings":null,"outboundVnetRouting":{"allTraffic":false,"applicationTraffic":false,"contentShareTraffic":false,"imagePullTraffic":false,"backupRestoreTraffic":false,"managedIdentityTraffic":false},"legacyServiceEndpointTrafficEvaluation":null,"siteConfig":{"numberOfWorkers":1,"defaultDocuments":null,"netFrameworkVersion":null,"phpVersion":null,"pythonVersion":null,"nodeVersion":null,"powerShellVersion":null,"linuxFxVersion":"","windowsFxVersion":null,"sandboxType":null,"windowsConfiguredStacks":null,"requestTracingEnabled":null,"remoteDebuggingEnabled":null,"remoteDebuggingVersion":null,"httpLoggingEnabled":null,"azureMonitorLogCategories":null,"acrUseManagedIdentityCreds":false,"acrUserManagedIdentityID":null,"logsDirectorySizeLimit":null,"detailedErrorLoggingEnabled":null,"publishingUsername":null,"publishingPassword":null,"appSettings":null,"metadata":null,"connectionStrings":null,"machineKey":null,"handlerMappings":null,"documentRoot":null,"scmType":null,"use32BitWorkerProcess":null,"webSocketsEnabled":null,"alwaysOn":false,"javaVersion":null,"javaContainer":null,"javaContainerVersion":null,"appCommandLine":null,"managedPipelineMode":null,"virtualApplications":null,"winAuthAdminState":null,"winAuthTenantState":null,"customAppPoolIdentityAdminState":null,"customAppPoolIdentityTenantState":null,"runtimeADUser":null,"runtimeADUserPassword":null,"loadBalancing":null,"routingRules":null,"experiments":null,"limits":null,"autoHealEnabled":null,"autoHealRules":null,"tracingOptions":null,"vnetName":null,"vnetRouteAllEnabled":null,"vnetPrivatePortsCount":null,"publicNetworkAccess":null,"cors":null,"push":null,"apiDefinition":null,"apiManagementConfig":null,"autoSwapSlotName":null,"localMySqlEnabled":null,"managedServiceIdentityId":null,"xManagedServiceIdentityId":null,"keyVaultReferenceIdentity":null,"ipSecurityRestrictions":[{"ipAddress":"Any","action":"Allow","priority":2147483647,"name":"Allow all","description":"Allow all access"}],"ipSecurityRestrictionsDefaultAction":null,"scmIpSecurityRestrictions":[{"ipAddress":"Any","action":"Allow","priority":2147483647,"name":"Allow - all","description":"Allow all access"}],"scmIpSecurityRestrictionsDefaultAction":null,"scmIpSecurityRestrictionsUseMain":null,"http20Enabled":false,"minTlsVersion":null,"minTlsCipherSuite":null,"scmMinTlsCipherSuite":null,"supportedTlsCipherSuites":null,"scmSupportedTlsCipherSuites":null,"scmMinTlsVersion":null,"ftpsState":null,"preWarmedInstanceCount":null,"functionAppScaleLimit":null,"elasticWebAppScaleLimit":0,"healthCheckPath":null,"fileChangeAuditEnabled":null,"functionsRuntimeScaleMonitoringEnabled":null,"websiteTimeZone":null,"minimumElasticInstanceCount":0,"azureStorageAccounts":null,"http20ProxyFlag":null,"sitePort":null,"antivirusScanEnabled":null,"storageType":null,"sitePrivateLinkHostEnabled":null,"clusteringEnabled":false,"webJobsEnabled":false},"functionAppConfig":null,"daprConfig":null,"aiIntegration":null,"deploymentId":"web-ssl-test000003","slotName":null,"trafficManagerHostNames":null,"sku":"Standard","scmSiteAlsoStopped":false,"targetSwapSlot":null,"hostingEnvironment":null,"hostingEnvironmentProfile":null,"clientAffinityEnabled":true,"clientAffinityProxyEnabled":false,"useQueryStringAffinity":false,"blockPathTraversal":false,"clientCertEnabled":false,"clientCertMode":"Required","clientCertExclusionPaths":null,"clientCertExclusionEndPoints":null,"hostNamesDisabled":false,"ipMode":"IPv4","domainVerificationIdentifiers":null,"customDomainVerificationId":"E2D9FCDA6056D515F7F3C22EF62001309FEEED0A7371E8A79E5F9F09170F160C","kind":"app","managedEnvironmentId":null,"workloadProfileName":null,"resourceConfig":null,"inboundIpAddress":"20.105.232.18","possibleInboundIpAddresses":"20.105.232.18","inboundIpv6Address":"2603:1020:206:6::5d","possibleInboundIpv6Addresses":"2603:1020:206:6::5d","ftpUsername":"web-ssl-test000003\\$web-ssl-test000003","ftpsHostName":"ftps://waws-prod-am2-605.ftp.azurewebsites.windows.net/site/wwwroot","outboundIpAddresses":"20.23.122.13,20.23.126.178,20.23.126.182,20.23.124.201,20.23.126.169,20.23.126.213,20.105.232.18","possibleOutboundIpAddresses":"20.23.122.13,20.23.126.178,20.23.126.182,20.23.124.201,20.23.126.169,20.23.126.213,9.163.72.123,20.23.126.238,20.23.126.237,20.23.127.36,20.23.127.138,20.103.77.126,20.103.77.246,20.103.76.163,20.76.136.137,20.76.137.83,20.76.137.99,20.76.137.225,20.76.138.6,20.76.138.42,20.76.138.54,20.76.138.62,20.76.138.170,20.76.139.235,20.76.139.238,20.76.140.61,20.76.140.192,20.76.140.201,20.76.141.170,20.76.141.218,20.76.141.230,20.105.232.18","outboundIpv6Addresses":"2603:1020:203:17::3da,2603:1020:203:a::3f9,2603:1020:203:a::3fa,2603:1020:203:18::3bd,2603:1020:203:13::3ca,2603:1020:203:15::3b9,2603:1020:206:6::5d,2603:10e1:100:2::1469:e812","possibleOutboundIpv6Addresses":"2603:1020:203:17::3da,2603:1020:203:a::3f9,2603:1020:203:a::3fa,2603:1020:203:18::3bd,2603:1020:203:13::3ca,2603:1020:203:15::3b9,2603:1020:203:1a::3a7,2603:1020:203:9::3c9,2603:1020:203:9::3cb,2603:1020:203:18::3bf,2603:1020:203:1f::3c3,2603:1020:203:15::3bd,2603:1020:203:1e::35a,2603:1020:203:7::95,2603:1020:203:b::3db,2603:1020:203:b::3dc,2603:1020:203:9::3cf,2603:1020:203:17::3e0,2603:1020:203:f::3de,2603:1020:203:1e::35d,2603:1020:203:a::401,2603:1020:203:f::3df,2603:1020:203:18::3c8,2603:1020:203:f::3e1,2603:1020:203:16::3bc,2603:1020:203:c::40e,2603:1020:203:5::3c6,2603:1020:203:17::3e6,2603:1020:203:16::3bf,2603:1020:203:9::3d2,2603:1020:206:6::5d,2603:10e1:100:2::1469:e812","containerSize":0,"dailyMemoryTimeQuota":0,"suspendedTill":null,"siteDisabledReason":0,"functionExecutionUnitsCache":null,"maxNumberOfWorkers":null,"homeStamp":"waws-prod-am2-605","cloningInfo":null,"hostingEnvironmentId":null,"tags":{"web":"web1"},"resourceGroup":"clitest.rg000001","defaultHostName":"web-ssl-test000003.azurewebsites.net","slotSwapStatus":null,"httpsOnly":true,"endToEndEncryptionEnabled":false,"functionsRuntimeAdminIsolationEnabled":false,"redundancyMode":"None","inProgressOperationId":null,"geoDistributions":null,"privateEndpointConnections":null,"publicNetworkAccess":"Enabled","buildVersion":null,"targetBuildVersion":null,"migrationState":null,"eligibleLogCategories":"AppServiceAppLogs,AppServiceConsoleLogs,AppServiceHTTPLogs,AppServicePlatformLogs,ScanLogs,AppServiceAuthenticationLogs,AppServiceAuditLogs,AppServiceIPSecAuditLogs","inFlightFeatures":null,"storageAccountRequired":false,"virtualNetworkSubnetId":null,"keyVaultReferenceIdentity":"SystemAssigned","autoGeneratedDomainNameLabelScope":null,"privateLinkIdentifiers":null,"sshEnabled":null,"maintenanceEnabled":false}}' + all","description":"Allow all access"}],"scmIpSecurityRestrictionsDefaultAction":null,"scmIpSecurityRestrictionsUseMain":null,"http20Enabled":false,"minTlsVersion":null,"minTlsCipherSuite":null,"scmMinTlsCipherSuite":null,"supportedTlsCipherSuites":null,"scmSupportedTlsCipherSuites":null,"scmMinTlsVersion":null,"ftpsState":null,"preWarmedInstanceCount":null,"functionAppScaleLimit":null,"elasticWebAppScaleLimit":0,"healthCheckPath":null,"fileChangeAuditEnabled":null,"functionsRuntimeScaleMonitoringEnabled":null,"websiteTimeZone":null,"minimumElasticInstanceCount":0,"azureStorageAccounts":null,"http20ProxyFlag":null,"sitePort":null,"antivirusScanEnabled":null,"storageType":null,"sitePrivateLinkHostEnabled":null,"clusteringEnabled":false,"webJobsEnabled":false},"functionAppConfig":null,"daprConfig":null,"aiIntegration":null,"deploymentId":"web-ssl-test000003","slotName":null,"trafficManagerHostNames":null,"sku":"Standard","scmSiteAlsoStopped":false,"targetSwapSlot":null,"hostingEnvironment":null,"hostingEnvironmentProfile":null,"clientAffinityEnabled":true,"clientAffinityProxyEnabled":false,"useQueryStringAffinity":false,"blockPathTraversal":false,"clientCertEnabled":false,"clientCertMode":"Required","clientCertExclusionPaths":null,"clientCertExclusionEndPoints":null,"hostNamesDisabled":false,"ipMode":"IPv4","domainVerificationIdentifiers":null,"customDomainVerificationId":"E2D9FCDA6056D515F7F3C22EF62001309FEEED0A7371E8A79E5F9F09170F160C","kind":"app","managedEnvironmentId":null,"workloadProfileName":null,"resourceConfig":null,"inboundIpAddress":"20.50.2.10","possibleInboundIpAddresses":"20.50.2.10","inboundIpv6Address":"2603:1020:206:5::20","possibleInboundIpv6Addresses":"2603:1020:206:5::20","ftpUsername":"web-ssl-test000003\\$web-ssl-test000003","ftpsHostName":"ftps://waws-prod-am2-347.ftp.azurewebsites.windows.net/site/wwwroot","outboundIpAddresses":"20.50.231.80,20.50.231.126,52.143.14.31,20.50.231.181,20.50.231.193,20.50.231.240,20.50.2.10","possibleOutboundIpAddresses":"20.50.231.80,20.50.231.126,52.143.14.31,20.50.231.181,20.50.231.193,20.50.231.240,135.236.65.225,20.50.231.249,20.54.184.30,20.54.184.84,20.54.184.87,20.54.184.114,20.54.184.165,20.54.194.139,20.54.194.198,20.54.195.76,20.54.195.138,20.54.195.249,20.54.196.59,20.54.196.64,20.54.196.74,20.54.196.76,20.54.196.97,20.54.196.100,20.54.196.125,20.54.184.175,20.54.184.243,20.54.185.1,20.73.27.16,20.73.29.71,20.73.31.124,20.50.2.10","outboundIpv6Addresses":"2603:1020:203:f::379,2603:1020:203:c::39b,2603:1020:203:a::397,2603:1020:203:c::39d,2603:1020:203:5::366,2603:1020:203:1a::344,2603:1020:206:5::20,2603:10e1:100:2::1432:20a","possibleOutboundIpv6Addresses":"2603:1020:203:f::379,2603:1020:203:c::39b,2603:1020:203:a::397,2603:1020:203:c::39d,2603:1020:203:5::366,2603:1020:203:1a::344,2603:1020:203:15::355,2603:1020:203:5::368,2603:1020:203:c::39e,2603:1020:203:13::364,2603:1020:203:a::398,2603:1020:203:1a::345,2603:1020:203:d::372,2603:1020:203:6::399,2603:1020:201:10::7c8,2603:1020:203:b::389,2603:1020:203:1e::30e,2603:1020:203:17::376,2603:1020:203:c::3a0,2603:1020:201:e::7d8,2603:1020:203:10::343,2603:1020:203:1f::35e,2603:1020:203:1a::347,2603:1020:201:e::7db,2603:1020:203:20::33a,2603:1020:203:1f::35f,2603:1020:203:c::3a1,2603:1020:201:10::7c9,2603:1020:203:b::38a,2603:1020:203:18::361,2603:1020:206:5::20,2603:10e1:100:2::1432:20a","containerSize":0,"dailyMemoryTimeQuota":0,"suspendedTill":null,"siteDisabledReason":0,"functionExecutionUnitsCache":null,"maxNumberOfWorkers":null,"homeStamp":"waws-prod-am2-347","cloningInfo":null,"hostingEnvironmentId":null,"tags":{"web":"web1"},"resourceGroup":"clitest.rg000001","defaultHostName":"web-ssl-test000003.azurewebsites.net","slotSwapStatus":null,"httpsOnly":true,"endToEndEncryptionEnabled":false,"functionsRuntimeAdminIsolationEnabled":false,"redundancyMode":"None","inProgressOperationId":null,"geoDistributions":null,"privateEndpointConnections":null,"publicNetworkAccess":"Enabled","buildVersion":null,"targetBuildVersion":null,"migrationState":null,"eligibleLogCategories":"AppServiceAppLogs,AppServiceConsoleLogs,AppServiceHTTPLogs,AppServicePlatformLogs,ScanLogs,AppServiceAuthenticationLogs,AppServiceAuditLogs,AppServiceIPSecAuditLogs","inFlightFeatures":null,"storageAccountRequired":false,"virtualNetworkSubnetId":null,"keyVaultReferenceIdentity":"SystemAssigned","autoGeneratedDomainNameLabelScope":null,"privateLinkIdentifiers":null,"sshEnabled":null,"maintenanceEnabled":false}}' headers: cache-control: - no-cache content-length: - - '8834' + - '8810' content-type: - application/json date: - - Fri, 08 May 2026 15:54:13 GMT + - Thu, 14 May 2026 15:48:00 GMT etag: - - '"1DCDF02DF1CAF80"' + - '"1DCE3B8F9A1F260"' expires: - '-1' pragma: @@ -783,11 +784,11 @@ interactions: x-content-type-options: - nosniff x-ms-operation-identifier: - - tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47,objectId=f338340e-ef13-48e1-aa76-6cde16f7c55f/westeurope/6790e961-063d-44b4-93bf-342791f65c89 + - tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47,objectId=f338340e-ef13-48e1-aa76-6cde16f7c55f/westeurope/081e5bf7-5c70-40e5-8e3f-041401d47855 x-ms-ratelimit-remaining-subscription-resource-requests: - '99' x-msedge-ref: - - 'Ref A: 2F9F5250DC7D4014A692C48983F11DA3 Ref B: SN4AA2022302027 Ref C: 2026-05-08T15:53:51Z' + - 'Ref A: F962593DE6CC4509969478E839008697 Ref B: SN4AA2022303009 Ref C: 2026-05-14T15:47:28Z' x-powered-by: - ASP.NET status: @@ -822,7 +823,7 @@ interactions: SQLServerDBConnectionString="REDACTED" mySQLDBConnectionString="" hostingProviderForumLink="" controlPanelLink="https://portal.azure.com" webSystem="WebSites"> Date: Wed, 20 May 2026 14:20:36 -0500 Subject: [PATCH 2/4] update flex-migration warning --- .../cli/command_modules/appservice/_params.py | 4 +- .../cli/command_modules/appservice/custom.py | 46 ++++++++++++++----- .../tests/latest/test_functionapp_commands.py | 33 ++++++++----- 3 files changed, 57 insertions(+), 26 deletions(-) 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 c24c2adb57d..f8c32e00a29 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -718,10 +718,10 @@ def load_arguments(self, _): # Add load_to_code parameter for functionapp SSL commands that create/update certificates (Flex Consumption only) with self.argument_context('functionapp config ssl upload') as c: - c.argument('load_to_code', arg_type=get_three_state_flag(), help='For Flex Consumption apps only. Make the certificate accessible to app code. When set to true, the certificate can be loaded via the WEBSITE_LOAD_CERTIFICATES app setting.') + c.argument('load_to_code', arg_type=get_three_state_flag(), help='For Flex Consumption apps only. When set to true, the certificate is accessible to app code.') with self.argument_context('functionapp config ssl import') as c: - c.argument('load_to_code', arg_type=get_three_state_flag(), help='For Flex Consumption apps only. Make the certificate accessible to app code. When set to true, the certificate can be loaded via the WEBSITE_LOAD_CERTIFICATES app setting.') + c.argument('load_to_code', arg_type=get_three_state_flag(), help='For Flex Consumption apps only. When set to true, the certificate is accessible to app code') c.argument('enable_using_msi', arg_type=get_three_state_flag(), help='For Flex Consumption apps only. Enable Key Vault access using Managed Service Identity. When set to true, the app will use its managed identity to access Key Vault instead of service principal.') with self.argument_context('webapp config connection-string list') 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 2d28f16ca7c..f9697e9c1c6 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -1124,18 +1124,25 @@ def list_flex_migration_candidates(cmd): continue try: - if validate_flex_migration_eligibility_for_linux_consumption_app(cmd, site, flex_regions): + is_eligible, warnings = validate_flex_migration_eligibility_for_linux_consumption_app(cmd, site, flex_regions) + if is_eligible: site_entry = { 'name': site.name, 'resource_group': site.resource_group, } + notes = [] has_slots = len(list_slots(cmd, site.resource_group, site.name)) > 0 if has_slots: - slots_warning = (f"The site '{site.name}' has slots configured. This will not block migration, " - f"but please note that slots are not supported in Flex Consumption.") - site_entry['note'] = slots_warning + notes.append(f"The site '{site.name}' has slots configured. This will not block migration, " + f"but please note that slots are not supported in Flex Consumption.") + + # Add certificate-related warnings as notes + notes.extend(warnings) + + if notes: + site_entry['note'] = ' '.join(notes) eligible_sites.append(site_entry) @@ -1153,6 +1160,8 @@ def list_flex_migration_candidates(cmd): def validate_flex_migration_eligibility_for_linux_consumption_app(cmd, site, flex_regions): + warnings = [] + # Validating that the site is in a Flex Consumption-supported region normalized_site_location = _normalize_location(cmd, site.location) if normalized_site_location not in flex_regions: @@ -1170,18 +1179,26 @@ def validate_flex_migration_eligibility_for_linux_consumption_app(cmd, site, fle runtime_helper = _FlexFunctionAppStackRuntimeHelper(cmd, normalized_site_location, runtime) runtime_helper.resolve(runtime, runtime_version) - # Validating that the site does not have SSL bindings configured + # Check for SSL bindings - warn but allow migration for ssl_state in site.host_name_ssl_states or []: if ssl_state.ssl_state != 'Disabled': - raise ValidationError("The site '{}' is using TSL/SSL certificates. " - "TSL/SSL certificates are not supported in Flex Consumption.".format(site.name)) + warnings.append("The site '{}' is using TSL/SSL certificates. " + "Site-scoped TSL/SSL certificates are supported in Flex Consumption in preview. " + "Re-configure certificates after migration using the new site-scoped model. " + "Help Link: https://aka.ms/flex-site-scoped-certs-docs." + .format(site.name)) + break - # Validating that the site does not have WEBSITE_LOAD_CERTIFICATES app setting configured + # Check for WEBSITE_LOAD_CERTIFICATES app setting - warn but allow migration app_settings = get_app_settings(cmd, site.resource_group, site.name) for setting in app_settings: if setting['name'] == 'WEBSITE_LOAD_CERTIFICATES': - raise ValidationError("The site '{}' has the WEBSITE_LOAD_CERTIFICATES app setting configured. " - "Certificate loading is not supported in Flex Consumption.".format(site.name)) + warnings.append("The site '{}' has the WEBSITE_LOAD_CERTIFICATES app setting configured. " + "Site-scoped certificate loading is supported in Flex Consumption in preview. " + "Re-configure certificates after migration using the new site-scoped model. " + "Help Link: https://aka.ms/flex-site-scoped-certs-docs." + .format(site.name)) + break # Validating that the site has triggers supported in Flex Consumption functions = list_functions(cmd, site.resource_group, site.name) @@ -1200,7 +1217,7 @@ def validate_flex_migration_eligibility_for_linux_consumption_app(cmd, site, fle "Please convert these triggers to use Event Grid or replace them with Event Grid " "triggers before migration.".format(site.name, function_list)) - return True + return True, warnings def get_storage_account_from_functionapp(cmd, resource_group_name, name): @@ -1260,11 +1277,16 @@ def migrate_consumption_to_flex(cmd, source_resource_group, source_name, resourc "migration is only supported for Function Apps on Linux Consumption plans." .format(source.name)) - if validate_flex_migration_eligibility_for_linux_consumption_app(cmd, source, flex_regions): + is_eligible, warnings = validate_flex_migration_eligibility_for_linux_consumption_app(cmd, source, flex_regions) + if is_eligible: slots = list_slots(cmd, source_resource_group, source_name) if len(slots) > 0: print(f"The site '{source_name}' has slots configured. This will not block migration, " f"but please note that slots are not supported in Flex Consumption.") + + # Print certificate-related warnings + for warning in warnings: + logger.warning(warning) print(f"Source app '{source_name}' is eligible for Flex Consumption migration.") source_site_configs = get_site_configs(cmd, source_resource_group, source_name) 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 024a1fc9cf0..1e2def7316a 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 @@ -816,7 +816,8 @@ class FunctionAppFlexMigrationTest(LiveScenarioTest): def test_functionapp_flex_migration_list_candidates(self, resource_group, storage_account): eligible_functionapp_name = self.create_random_name('consumption-func', 24) noneligible_slots_functionapp_name = self.create_random_name('noneligible-slots-func', 40) - noneligible_cert_functionapp_name = self.create_random_name('noneligible-cert-func', 40) + eligible_cert_functionapp_name = self.create_random_name('eligible-cert-func', 40) + slot_name = self.create_random_name(prefix='slotname', length=24) self.cmd('functionapp create -g {} -n {} -c {} -s {} --os-type linux --runtime python --runtime-version 3.11 --functions-version 4' @@ -828,9 +829,9 @@ def test_functionapp_flex_migration_list_candidates(self, resource_group, storag self.cmd('functionapp deployment slot create -g {} -n {} --slot {}'.format(resource_group, noneligible_slots_functionapp_name, slot_name)) self.cmd('functionapp create -g {} -n {} -c {} -s {} --os-type linux --runtime python --runtime-version 3.11 --functions-version 4' - .format(resource_group, noneligible_cert_functionapp_name, FLEX_ASP_LOCATION_FUNCTIONAPP, storage_account)) + .format(resource_group, eligible_cert_functionapp_name, FLEX_ASP_LOCATION_FUNCTIONAPP, storage_account)) - self.cmd('functionapp config appsettings set -g {} -n {} --settings WEBSITE_LOAD_CERTIFICATES=*'.format(resource_group, noneligible_cert_functionapp_name)) + self.cmd('functionapp config appsettings set -g {} -n {} --settings WEBSITE_LOAD_CERTIFICATES=*'.format(resource_group, eligible_cert_functionapp_name)) candidates = self.cmd('functionapp flex-migration list').get_output_in_json().get('eligible_apps', []) @@ -838,21 +839,25 @@ def test_functionapp_flex_migration_list_candidates(self, resource_group, storag self.assertTrue(eligible_functionapp_name in candidate_names) self.assertTrue(noneligible_slots_functionapp_name in candidate_names) - self.assertTrue(noneligible_cert_functionapp_name not in candidate_names) + # Apps with WEBSITE_LOAD_CERTIFICATES are now eligible with a warning note + self.assertTrue(eligible_cert_functionapp_name in candidate_names) + cert_candidate = next((c for c in candidates if c.get('name') == eligible_cert_functionapp_name), None) + self.assertIsNotNone(cert_candidate) + self.assertIn('WEBSITE_LOAD_CERTIFICATES', cert_candidate.get('note', '')) @ResourceGroupPreparer(location=WINDOWS_ASP_LOCATION_FUNCTIONAPP) @StorageAccountPreparer() def test_functionapp_flex_migration_private_cert_list_candidates(self, resource_group, storage_account): - noneligible_privatekey_functionapp_name = self.create_random_name('noneligible-privatekey-func', 40) + eligible_ssl_functionapp_name = self.create_random_name('eligible-ssl-func', 40) pfx_file = os.path.join(TEST_DIR, 'server.pfx') cert_password = 'test' cert_thumbprint = '9E9735C45C792B03B3FFCCA614852B32EE71AD6B' self.cmd('functionapp create -g {} -n {} -c {} -s {} --os-type linux --runtime python --runtime-version 3.11 --functions-version 4' - .format(resource_group, noneligible_privatekey_functionapp_name, WINDOWS_ASP_LOCATION_FUNCTIONAPP, storage_account)) + .format(resource_group, eligible_ssl_functionapp_name, WINDOWS_ASP_LOCATION_FUNCTIONAPP, storage_account)) - self.cmd('webapp config ssl upload -g {} -n {} --certificate-file "{}" --certificate-password {} --certificate-name {}'.format(resource_group, noneligible_privatekey_functionapp_name, pfx_file, cert_password, "test123"), checks=[ + self.cmd('webapp config ssl upload -g {} -n {} --certificate-file "{}" --certificate-password {} --certificate-name {}'.format(resource_group, eligible_ssl_functionapp_name, pfx_file, cert_password, "test123"), checks=[ JMESPathCheck('thumbprint', cert_thumbprint), JMESPathCheck('name', 'test123') ]) @@ -861,20 +866,24 @@ def test_functionapp_flex_migration_private_cert_list_candidates(self, resource_ candidate_names = [candidate.get('name') for candidate in candidates if 'name' in candidate] - self.assertTrue(noneligible_privatekey_functionapp_name in candidate_names) + self.assertTrue(eligible_ssl_functionapp_name in candidate_names) - self.cmd('webapp config ssl bind -g {} -n {} --certificate-thumbprint {} --ssl-type {}'.format(resource_group, noneligible_privatekey_functionapp_name, cert_thumbprint, 'SNI'), checks=[ + self.cmd('webapp config ssl bind -g {} -n {} --certificate-thumbprint {} --ssl-type {}'.format(resource_group, eligible_ssl_functionapp_name, cert_thumbprint, 'SNI'), checks=[ JMESPathCheck("hostNameSslStates|[?name=='{}.azurewebsites.net']|[0].sslState".format( - noneligible_privatekey_functionapp_name), 'SniEnabled'), + eligible_ssl_functionapp_name), 'SniEnabled'), JMESPathCheck("hostNameSslStates|[?name=='{}.azurewebsites.net']|[0].thumbprint".format( - noneligible_privatekey_functionapp_name), cert_thumbprint) + eligible_ssl_functionapp_name), cert_thumbprint) ]) candidates = self.cmd('functionapp flex-migration list').get_output_in_json().get('eligible_apps', []) candidate_names = [candidate.get('name') for candidate in candidates if 'name' in candidate] - self.assertTrue(noneligible_privatekey_functionapp_name not in candidate_names) + # Apps with SSL bindings are now eligible with a warning note + self.assertTrue(eligible_ssl_functionapp_name in candidate_names) + ssl_candidate = next((c for c in candidates if c.get('name') == eligible_ssl_functionapp_name), None) + self.assertIsNotNone(ssl_candidate) + self.assertIn('TSL/SSL certificates', ssl_candidate.get('note', '')) @ResourceGroupPreparer(location=FLEX_ASP_LOCATION_FUNCTIONAPP) From 390ca32bf1e94fc9c67804a37f442399b27d114f Mon Sep 17 00:00:00 2001 From: Chandni Patel Date: Wed, 27 May 2026 17:55:30 -0500 Subject: [PATCH 3/4] fix pipeline errors --- .../cli/command_modules/appservice/_params.py | 12 +- .../cli/command_modules/appservice/custom.py | 10 +- .../tests/latest/test_functionapp_commands.py | 128 +++++++++++++++++- 3 files changed, 142 insertions(+), 8 deletions(-) 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 f8c32e00a29..7e8abb8e03d 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -708,7 +708,7 @@ def load_arguments(self, _): # Add optional 'name' parameter for functionapp SSL commands to support Flex Consumption apps with self.argument_context('functionapp config ssl list') as c: - c.argument('name', options_list=['--name', '-n'], help='Name of the function app. Required for Flex Consumption apps to list site-scoped certificates.') + c.argument('name', options_list=['--name', '-n'], id_part=None, help='Name of the function app. Required for Flex Consumption apps to list site-scoped certificates.') with self.argument_context('functionapp config ssl show') as c: c.argument('name', options_list=['--name', '-n'], help='Name of the function app. Required for Flex Consumption apps to show site-scoped certificates.') @@ -724,6 +724,16 @@ def load_arguments(self, _): c.argument('load_to_code', arg_type=get_three_state_flag(), help='For Flex Consumption apps only. When set to true, the certificate is accessible to app code') c.argument('enable_using_msi', arg_type=get_three_state_flag(), help='For Flex Consumption apps only. Enable Key Vault access using Managed Service Identity. When set to true, the app will use its managed identity to access Key Vault instead of service principal.') + with self.argument_context('webapp config ssl list') as c: + c.argument('name', arg_type=webapp_name_arg_type, id_part=None) + + with self.argument_context('webapp config ssl upload') as c: + c.ignore('load_to_code') + + with self.argument_context('webapp config ssl import') as c: + c.ignore('load_to_code') + c.ignore('enable_using_msi') + with self.argument_context('webapp config connection-string list') as c: c.argument('name', arg_type=webapp_name_arg_type, id_part=None) 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 f9697e9c1c6..bb44cb9c749 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -1124,7 +1124,8 @@ def list_flex_migration_candidates(cmd): continue try: - is_eligible, warnings = validate_flex_migration_eligibility_for_linux_consumption_app(cmd, site, flex_regions) + is_eligible, warnings = validate_flex_migration_eligibility_for_linux_consumption_app( + cmd, site, flex_regions) if is_eligible: site_entry = { 'name': site.name, @@ -6868,7 +6869,7 @@ def delete_ssl_cert(cmd, resource_group_name, certificate_thumbprint, name=None) def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_name, name=None, certificate_name=None, - load_to_code=None, enable_using_msi=None): + load_to_code=None, enable_using_msi=None): # pylint: disable=too-many-branches Certificate = cmd.get_models('Certificate') client = web_client_factory(cmd.cli_ctx) @@ -6935,10 +6936,7 @@ def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_n if not kv_secret_name: kv_secret_name = key_vault_certificate_name - if certificate_name: - cert_name = certificate_name - else: - cert_name = '{}-{}-{}'.format(resource_group_name, kv_name, key_vault_certificate_name) + cert_name = certificate_name or '{}-{}-{}'.format(resource_group_name, kv_name, key_vault_certificate_name) lnk = 'https://azure.github.io/AppService/2016/05/24/Deploying-Azure-Web-App-Certificate-through-Key-Vault.html' lnk_msg = 'Find more details here: {}'.format(lnk) 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 1e2def7316a..edaaaabb231 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 @@ -14,7 +14,7 @@ from azure.cli.testsdk.scenario_tests import AllowLargeResponse, record_only from azure.cli.testsdk import (ScenarioTest, LocalContextScenarioTest, LiveScenarioTest, ResourceGroupPreparer, - StorageAccountPreparer, JMESPathCheck, live_only, VirtualNetworkPreparer) + StorageAccountPreparer, KeyVaultPreparer, JMESPathCheck, live_only, VirtualNetworkPreparer) from azure.cli.testsdk.checkers import JMESPathCheckNotExists, JMESPathPatternCheck from azure.cli.core.azclierror import ValidationError, ArgumentUsageError, RequiredArgumentMissingError, MutuallyExclusiveArgumentError @@ -1611,6 +1611,132 @@ def test_functionapp_flex_update_strategy_config(self, resource_group, storage_a .format(resource_group, functionapp_name)).get_output_in_json() self.assertEqual(update_strategy_config['type'], 'RollingUpdate') + @ResourceGroupPreparer(location=FLEX_ASP_LOCATION_FUNCTIONAPP) + @StorageAccountPreparer() + def test_functionapp_flex_ssl_commands(self, resource_group, storage_account): + functionapp_name = self.create_random_name('flexssltest', 40) + pfx_file = os.path.join(TEST_DIR, 'server.pfx') + cert_password = 'test' + cert_thumbprint = '9E9735C45C792B03B3FFCCA614852B32EE71AD6B' + cert_name = 'test-flex-cert' + + # Create a Flex Consumption function app + self.cmd('functionapp create -g {} -n {} -f {} -s {} --runtime python --runtime-version 3.11' + .format(resource_group, functionapp_name, FLEX_ASP_LOCATION_FUNCTIONAPP, storage_account)) + + # Upload SSL certificate with --load-to-code parameter + self.cmd('functionapp config ssl upload -g {} -n {} --certificate-file "{}" --certificate-password {} --certificate-name {} --load-to-code true' + .format(resource_group, functionapp_name, pfx_file, cert_password, cert_name), checks=[ + JMESPathCheck('thumbprint', cert_thumbprint), + JMESPathCheck('name', cert_name) + ]) + + # List SSL certificates with -n parameter (site-scoped for Flex) + certs = self.cmd('functionapp config ssl list -g {} -n {}' + .format(resource_group, functionapp_name)).get_output_in_json() + self.assertTrue(len(certs) >= 1) + cert_names = [c['name'] for c in certs] + self.assertIn(cert_name, cert_names) + + # Show SSL certificate with -n parameter + cert = self.cmd('functionapp config ssl show -g {} -n {} --certificate-name {}' + .format(resource_group, functionapp_name, cert_name)).get_output_in_json() + self.assertEqual(cert['thumbprint'], cert_thumbprint) + + # Delete SSL certificate with -n parameter + self.cmd('functionapp config ssl delete -g {} -n {} --certificate-thumbprint {}' + .format(resource_group, functionapp_name, cert_thumbprint)) + + # Verify certificate is deleted + certs_after = self.cmd('functionapp config ssl list -g {} -n {}' + .format(resource_group, functionapp_name)).get_output_in_json() + cert_names_after = [c['name'] for c in certs_after] + self.assertNotIn(cert_name, cert_names_after) + + @ResourceGroupPreparer(location=FLEX_ASP_LOCATION_FUNCTIONAPP) + @StorageAccountPreparer() + @KeyVaultPreparer(location=FLEX_ASP_LOCATION_FUNCTIONAPP, name_prefix='kv-flex-ssl', name_len=20, additional_params='--enable-rbac-authorization false') + def test_functionapp_flex_ssl_import(self, resource_group, storage_account, key_vault): + functionapp_name = self.create_random_name('flexsslimport', 40) + pfx_file = os.path.join(TEST_DIR, 'server.pfx') + cert_password = 'test' + cert_thumbprint = '9E9735C45C792B03B3FFCCA614852B32EE71AD6B' + cert_name = 'test-flex-import-cert' + kv_cert_name = 'test-import-cert' + + # Create a Flex Consumption function app + self.cmd('functionapp create -g {} -n {} -f {} -s {} --runtime python --runtime-version 3.11' + .format(resource_group, functionapp_name, FLEX_ASP_LOCATION_FUNCTIONAPP, storage_account)) + + # Enable managed identity for the function app (required for MSI-based certificate import) + identity_result = self.cmd('functionapp identity assign -g {} -n {}'.format(resource_group, functionapp_name)).get_output_in_json() + principal_id = identity_result['principalId'] + + # Set Key Vault policy for App Service resource provider + self.cmd('keyvault set-policy -g {} --name {} --spn {} --secret-permissions get'.format( + resource_group, key_vault, 'Microsoft.Azure.WebSites')) + + # Set Key Vault policy for the function app's managed identity (required for MSI-based import) + self.cmd('keyvault set-policy -g {} --name {} --object-id {} --secret-permissions get'.format( + resource_group, key_vault, principal_id)) + + # Import certificate to Key Vault + self.cmd('keyvault certificate import --name {} --vault-name {} --file "{}" --password {}'.format( + kv_cert_name, key_vault, pfx_file, cert_password)) + + # Import SSL certificate from Key Vault with --load-to-code and --enable-using-msi parameters + self.cmd('functionapp config ssl import -g {} -n {} --key-vault {} --key-vault-certificate-name {} --certificate-name {} --load-to-code true --enable-using-msi true' + .format(resource_group, functionapp_name, key_vault, kv_cert_name, cert_name), checks=[ + JMESPathCheck('thumbprint', cert_thumbprint), + JMESPathCheck('name', cert_name) + ]) + + # Verify certificate is listed + certs = self.cmd('functionapp config ssl list -g {} -n {}' + .format(resource_group, functionapp_name)).get_output_in_json() + cert_names = [c['name'] for c in certs] + self.assertIn(cert_name, cert_names) + + @ResourceGroupPreparer(location=FLEX_ASP_LOCATION_FUNCTIONAPP) + @StorageAccountPreparer() + def test_functionapp_flex_ssl_bind_unbind(self, resource_group, storage_account): + functionapp_name = self.create_random_name('flexsslbind', 40) + pfx_file = os.path.join(TEST_DIR, 'server.pfx') + cert_password = 'test' + cert_thumbprint = '9E9735C45C792B03B3FFCCA614852B32EE71AD6B' + cert_name = 'test-flex-bind-cert' + + # Create a Flex Consumption function app + self.cmd('functionapp create -g {} -n {} -f {} -s {} --runtime python --runtime-version 3.11' + .format(resource_group, functionapp_name, FLEX_ASP_LOCATION_FUNCTIONAPP, storage_account)) + + # Upload SSL certificate + self.cmd('functionapp config ssl upload -g {} -n {} --certificate-file "{}" --certificate-password {} --certificate-name {}' + .format(resource_group, functionapp_name, pfx_file, cert_password, cert_name), checks=[ + JMESPathCheck('thumbprint', cert_thumbprint), + JMESPathCheck('name', cert_name) + ]) + + # Bind SSL certificate to the default hostname + self.cmd('functionapp config ssl bind -g {} -n {} --certificate-thumbprint {} --ssl-type {}' + .format(resource_group, functionapp_name, cert_thumbprint, 'SNI'), checks=[ + JMESPathCheck("hostNameSslStates|[?name=='{}.azurewebsites.net']|[0].sslState".format( + functionapp_name), 'SniEnabled'), + JMESPathCheck("hostNameSslStates|[?name=='{}.azurewebsites.net']|[0].thumbprint".format( + functionapp_name), cert_thumbprint) + ]) + + # Unbind SSL certificate from the hostname + self.cmd('functionapp config ssl unbind -g {} -n {} --certificate-thumbprint {}' + .format(resource_group, functionapp_name, cert_thumbprint), checks=[ + JMESPathCheck("hostNameSslStates|[?name=='{}.azurewebsites.net']|[0].sslState".format( + functionapp_name), 'Disabled') + ]) + + # Clean up - delete the certificate + self.cmd('functionapp config ssl delete -g {} -n {} --certificate-thumbprint {}' + .format(resource_group, functionapp_name, cert_thumbprint)) + class FunctionAppManagedEnvironment(ScenarioTest): @AllowLargeResponse() From 2aa7e0de2647124c3bf6dad0592cafa7e4b10190 Mon Sep 17 00:00:00 2001 From: Chandni Patel Date: Thu, 28 May 2026 09:58:53 -0500 Subject: [PATCH 4/4] typo --- src/azure-cli/azure/cli/command_modules/appservice/custom.py | 4 ++-- .../appservice/tests/latest/test_functionapp_commands.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 bb44cb9c749..dfe5db9456d 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -1183,8 +1183,8 @@ def validate_flex_migration_eligibility_for_linux_consumption_app(cmd, site, fle # Check for SSL bindings - warn but allow migration for ssl_state in site.host_name_ssl_states or []: if ssl_state.ssl_state != 'Disabled': - warnings.append("The site '{}' is using TSL/SSL certificates. " - "Site-scoped TSL/SSL certificates are supported in Flex Consumption in preview. " + warnings.append("The site '{}' is using TLS/SSL certificates. " + "Site-scoped TLS/SSL certificates are supported in Flex Consumption in preview. " "Re-configure certificates after migration using the new site-scoped model. " "Help Link: https://aka.ms/flex-site-scoped-certs-docs." .format(site.name)) 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 edaaaabb231..b24514be898 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 @@ -883,7 +883,7 @@ def test_functionapp_flex_migration_private_cert_list_candidates(self, resource_ self.assertTrue(eligible_ssl_functionapp_name in candidate_names) ssl_candidate = next((c for c in candidates if c.get('name') == eligible_ssl_functionapp_name), None) self.assertIsNotNone(ssl_candidate) - self.assertIn('TSL/SSL certificates', ssl_candidate.get('note', '')) + self.assertIn('TLS/SSL certificates', ssl_candidate.get('note', '')) @ResourceGroupPreparer(location=FLEX_ASP_LOCATION_FUNCTIONAPP)