Skip to content

Commit c44ee43

Browse files
[App Service] Added a command to switch between sitecontainers and classic custom containers
1 parent 4254e54 commit c44ee43

File tree

4 files changed

+166
-1
lines changed

4 files changed

+166
-1
lines changed

src/azure-cli/azure/cli/command_modules/appservice/_help.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2343,6 +2343,16 @@
23432343
text: az webapp sitecontainers log --name MyWebApp --resource-group MyResourceGroup --container-name MyContainer
23442344
"""
23452345

2346+
helps['webapp sitecontainers convert'] = """
2347+
type: command
2348+
short-summary: Convert a webapp from a webapp from sitecontainers to a classic custom container and vice versa.
2349+
examples:
2350+
- name: Convert a webapp to classic custom container from sitecontainers
2351+
text: az webapp sitecontainers convert --mode classic --name MyWebApp --resource-group MyResourceGroup
2352+
- name: Convert a webapp to sitecontainers from classic custom container
2353+
text: az webapp sitecontainers convert --mode sitecontainers --name MyWebApp --resource-group MyResourceGroup
2354+
"""
2355+
23462356

23472357
helps['webapp up'] = """
23482358
type: command

src/azure-cli/azure/cli/command_modules/appservice/_params.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ def load_arguments(self, _):
203203
with self.argument_context("webapp sitecontainers list") as c:
204204
c.argument('name', arg_type=webapp_name_arg_type, id_part=None, help='Name of the linux webapp')
205205

206+
with self.argument_context("webapp sitecontainers convert") as c:
207+
c.argument('mode', options_list=['--mode'], help='Mode for conversion. Allowed values: classic, sitecontainers.', choices=['classic', 'sitecontainers'])
208+
206209
with self.argument_context('webapp show') as c:
207210
c.argument('name', arg_type=webapp_name_arg_type)
208211

src/azure-cli/azure/cli/command_modules/appservice/commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def load_command_table(self, _):
150150
g.custom_command('list', 'list_webapp_sitecontainers')
151151
g.custom_command('status', 'get_webapp_sitecontainers_status')
152152
g.custom_command('log', 'get_webapp_sitecontainer_log')
153+
g.custom_command('convert', 'convert_webapp_sitecontainers')
153154

154155
with self.command_group('webapp traffic-routing') as g:
155156
g.custom_command('set', 'set_traffic_routing')

src/azure-cli/azure/cli/command_modules/appservice/custom.py

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1353,6 +1353,157 @@ def get_webapp_sitecontainer_log(cmd, name, resource_group, container_name, slot
13531353
raise AzureInternalError("Failed to fetch sitecontainer logs. Error: {}".format(str(ex)))
13541354

13551355

1356+
def convert_webapp_sitecontainers(cmd, name, resource_group, mode, slot=None):
1357+
"""
1358+
Convert a webapp between classic and sitecontainers mode.
1359+
1360+
:param cmd: CLI command context
1361+
:param name: Name of the webapp
1362+
:param resource_group: Resource group of the webapp
1363+
:param mode: Target mode, either 'classic' or 'sitecontainers'
1364+
:param slot: Optional deployment slot
1365+
"""
1366+
client = web_client_factory(cmd.cli_ctx)
1367+
if mode not in ['classic', 'sitecontainers']:
1368+
raise InvalidArgumentValueError(
1369+
"Invalid mode '{}'. Allowed values: classic, sitecontainers.".format(mode)
1370+
)
1371+
1372+
site_config = get_site_configs(cmd, resource_group, name, slot)
1373+
linux_fx_version = getattr(site_config, "linux_fx_version", None)
1374+
1375+
if mode == 'sitecontainers':
1376+
if linux_fx_version and not linux_fx_version.startswith('DOCKER|'):
1377+
raise ValidationError("Cannot convert to sitecontainers mode as site is not a classic custom container app.")
1378+
acr_use_managed_identity_creds = getattr(site_config, "acr_use_managed_identity_creds", None)
1379+
acr_user_managed_identity_id = getattr(site_config, "acr_user_managed_identity_id", None)
1380+
1381+
acr_user_name = None
1382+
acr_user_password = None
1383+
1384+
from azure.cli.core.commands.client_factory import get_subscription_id
1385+
subscription_id = get_subscription_id(cmd.cli_ctx)
1386+
url = (
1387+
f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/"
1388+
f"providers/Microsoft.Web/sites/{name}/config/appsettings/list?api-version=2023-12-01"
1389+
)
1390+
request_url = cmd.cli_ctx.cloud.endpoints.resource_manager + url
1391+
response = send_raw_request(cmd.cli_ctx, "POST", request_url)
1392+
app_settings_raw = response.json()
1393+
app_settings = app_settings_raw.get("properties", {})
1394+
1395+
acr_user_password = app_settings.get("DOCKER_REGISTRY_SERVER_PASSWORD", None)
1396+
acr_user_name = app_settings.get("DOCKER_REGISTRY_SERVER_USERNAME", None)
1397+
websites_port = app_settings.get("WEBSITES_PORT", None) or app_settings.get("PORT", None)
1398+
1399+
# Get docker image from linux_fx_version
1400+
docker_image = linux_fx_version.replace("DOCKER|", "", 1)
1401+
sidecarmainname = "main"
1402+
startup_cmd = getattr(site_config, "app_command_line", None)
1403+
1404+
# Prepare parameters for sitecontainers create
1405+
sitecontainer_kwargs = {
1406+
"name": name,
1407+
"resource_group": resource_group,
1408+
"slot": slot,
1409+
"container_name": sidecarmainname,
1410+
"image": docker_image,
1411+
"target_port": websites_port,
1412+
"startup_cmd": startup_cmd,
1413+
"is_main": True,
1414+
}
1415+
1416+
if acr_use_managed_identity_creds:
1417+
if acr_user_managed_identity_id:
1418+
# ACR with User-Assigned Managed Identity
1419+
logger.warning("Site is using User-Assigned Managed Identity for ACR authentication.")
1420+
sitecontainer_kwargs["user_assigned_identity"] = acr_user_managed_identity_id
1421+
else:
1422+
# ACR with System-Assigned Managed Identity
1423+
logger.warning("Site is using System-Assigned Managed Identity for ACR authentication.")
1424+
sitecontainer_kwargs["system_assigned_identity"] = True
1425+
else:
1426+
if acr_user_name and acr_user_password:
1427+
# ACR with User Credentials
1428+
logger.warning("Site is using User Credentials for ACR authentication.")
1429+
sitecontainer_kwargs["registry_username"] = acr_user_name
1430+
sitecontainer_kwargs["registry_password"] = acr_user_password
1431+
else:
1432+
# No ACR authentication, using anonymous access
1433+
logger.warning("Site is using anonymous access for ACR authentication.")
1434+
response = create_webapp_sitecontainers(cmd, **sitecontainer_kwargs)
1435+
if response is None:
1436+
raise AzureInternalError("Failed to create sitecontainer for conversion to sitecontainers mode.")
1437+
1438+
# Set linuxFxVersion to SITECONTAINERS
1439+
logger.warning("Setting linuxFxVersion to SITECONTAINERS")
1440+
update_site_configs(cmd, resource_group, name, slot=slot, linux_fx_version="SITECONTAINERS")
1441+
logger.warning("Webapp '%s' converted to sitecontainers mode.", name)
1442+
return {"result": "success", "mode": mode}
1443+
else: # mode == 'classic'
1444+
if linux_fx_version and not linux_fx_version.lower().startswith('sitecontainers'):
1445+
raise ValidationError("Cannot convert to classic mode as site is not a SITECONTAINERS app.")
1446+
1447+
# Get the main sitecontainer
1448+
sitecontainers = list_webapp_sitecontainers(cmd, name, resource_group, slot)
1449+
main_container = next((c for c in sitecontainers if getattr(c, "is_main", False)), None)
1450+
if not main_container:
1451+
raise ResourceNotFoundError("No main sitecontainer found. Cannot convert to classic mode.")
1452+
1453+
main_container = update_webapp_sitecontainer(cmd, name, resource_group, main_container.name, slot=slot,
1454+
image=main_container.image, target_port=main_container.target_port, is_main=main_container.is_main)
1455+
1456+
# Prepare new linux_fx_version
1457+
docker_image = getattr(main_container, "image", None)
1458+
if not docker_image:
1459+
raise ValidationError("Main sitecontainer does not have an image specified.")
1460+
1461+
linux_fx_version = _format_fx_version(docker_image)
1462+
1463+
# Prepare app settings for registry credentials if needed
1464+
settings = []
1465+
if main_container.auth_type == AuthType.USER_CREDENTIALS:
1466+
if main_container.user_name:
1467+
settings.append(f"DOCKER_REGISTRY_SERVER_USERNAME={main_container.user_name}")
1468+
if main_container.password_secret:
1469+
settings.append(f"DOCKER_REGISTRY_SERVER_PASSWORD={main_container.password_secret}")
1470+
if main_container.target_port:
1471+
settings.append(f"WEBSITES_PORT={main_container.target_port}")
1472+
elif main_container.auth_type == AuthType.SYSTEM_IDENTITY:
1473+
configs = get_site_configs(cmd, resource_group, name, slot)
1474+
setattr(configs, 'acr_use_managed_identity_creds', True)
1475+
setattr(configs, 'acr_user_managed_identity_id', "")
1476+
_generic_site_operation(cmd.cli_ctx, resource_group, name, 'update_configuration', slot, configs)
1477+
elif main_container.auth_type == AuthType.USER_ASSIGNED:
1478+
app = client.web_apps.get_slot(resource_group, name, slot) if slot else client.web_apps.get(resource_group, name)
1479+
if app.identity and app.identity.user_assigned_identities:
1480+
# Find the managed identity key whose client_id matches main_container.user_managed_identity_client_id
1481+
matched_key = None
1482+
for key, identity in app.identity.user_assigned_identities.items():
1483+
if identity.client_id == main_container.user_managed_identity_client_id:
1484+
matched_key = key
1485+
break
1486+
if not matched_key:
1487+
raise ResourceNotFoundError(
1488+
f"Could not find a user-assigned identity with client_id '{main_container.user_managed_identity_client_id}' assigned to the app."
1489+
)
1490+
update_site_configs(cmd, resource_group, name, slot=slot, acr_identity=matched_key)
1491+
elif main_container.auth_type == AuthType.Anonymous:
1492+
update_site_configs(cmd, resource_group, name, slot=slot, acr_use_identity=False)
1493+
1494+
logger.warning("Deleting all sitecontainers before converting to classic mode.")
1495+
# Remove all sitecontainers
1496+
for c in sitecontainers:
1497+
delete_webapp_sitecontainer(cmd, name, resource_group, c.name, slot)
1498+
1499+
# Set linuxFxVersion to classic custom container
1500+
update_site_configs(cmd, resource_group, name, slot=slot, linux_fx_version=linux_fx_version)
1501+
if settings:
1502+
update_app_settings(cmd, resource_group, name, settings, slot)
1503+
logger.warning("Webapp '%s' converted to classic custom container mode.", name)
1504+
return {"result": "success", "mode": mode}
1505+
1506+
13561507
# for generic updater
13571508
def get_webapp(cmd, resource_group_name, name, slot=None):
13581509
return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot)
@@ -2181,7 +2332,7 @@ def update_site_configs(cmd, resource_group_name, name, slot=None, number_of_wor
21812332
_update_webapp_current_stack_property_if_needed(cmd, resource_group_name, name, current_stack)
21822333

21832334
if linux_fx_version:
2184-
if linux_fx_version.strip().lower().startswith('docker|'):
2335+
if linux_fx_version.strip().lower().startswith('docker|') or linux_fx_version.strip().lower().startswith('sitecontainers'):
21852336
if ('WEBSITES_ENABLE_APP_SERVICE_STORAGE' not in app_settings.properties or
21862337
app_settings.properties['WEBSITES_ENABLE_APP_SERVICE_STORAGE'] != 'true'):
21872338
update_app_settings(cmd, resource_group_name, name, ["WEBSITES_ENABLE_APP_SERVICE_STORAGE=false"])

0 commit comments

Comments
 (0)