Skip to content

App Service] az webapp sitecontainers convert: Update to convert COMP…#33131

Open
kumaramit-msft wants to merge 2 commits intoAzure:devfrom
kumaramit-msft:developer/kumaramit/composeconversionsidecar
Open

App Service] az webapp sitecontainers convert: Update to convert COMP…#33131
kumaramit-msft wants to merge 2 commits intoAzure:devfrom
kumaramit-msft:developer/kumaramit/composeconversionsidecar

Conversation

@kumaramit-msft
Copy link
Copy Markdown
Contributor

[App Service] az webapp sitecontainers convert: Add COMPOSE to Sitecontainers conversion

Related command

az webapp sitecontainers convert --mode sitecontainers

Description

This PR adds support for converting multi-container Docker Compose apps (COMPOSE|<base64> linuxFxVersion) to the Sitecontainers model. The conversion:

  • Decodes and parses the Compose YAML from linuxFxVersion
  • Maps each Compose service to a SiteContainer ARM resource with image, ports, environment variables, volumes, entrypoint/command
  • Auto-detects the main container (service with port mapping) or accepts --main-container-name
  • Determines authentication type from existing site config (Anonymous, UserCredentials, SystemIdentity, UserAssigned)
  • Creates COMPOSE_<SERVICE>_<VAR> app settings for inline environment variables
  • Maps ${WEBAPP_STORAGE_HOME} bind mounts to /home volumeSubPath, and named volumes to ephemeral local storage
  • Sets linuxFxVersion to SITECONTAINERS on success
  • Rolls back created containers on partial failure
  • Shows a production site warning with rollback steps when converting without --slot (sitecontainers mode only)
  • Displays post-conversion warnings for unsupported Compose keys, port conflicts, networking changes, and volume limitations
  • Prompts for user confirmation after displaying all warnings before proceeding

Also includes:

  • Comprehensive unit tests for parsing helpers and orchestration logic (101 tests)
  • 13 YAML test fixtures covering various Compose configurations
  • Style/lint fixes (pylint, flake8 compliance)

Testing Guide

# Convert a Compose app on a slot (recommended)
az webapp sitecontainers convert -g myRG -n myApp --mode sitecontainers --slot staging

# Convert with explicit main container
az webapp sitecontainers convert -g myRG -n myApp --mode sitecontainers --main-container-name web

# Convert back to Docker
az webapp sitecontainers convert -g myRG -n myApp --mode docker

# Run unit tests
azdev test appservice --discover --pytest-args "-k test_compose_convert -v"

History Notes
[App Service] az webapp sitecontainers convert: Add support for converting Docker Compose multi-container apps to Sitecontainers mode

This checklist is used to make sure that common guidelines for a pull request are followed.

Copilot AI review requested due to automatic review settings April 3, 2026 09:37
@azure-client-tools-bot-prd
Copy link
Copy Markdown

azure-client-tools-bot-prd bot commented Apr 3, 2026

❌AzureCLI-FullTest
️✔️acr
️✔️latest
️✔️3.12
️✔️3.13
️✔️acs
️✔️latest
️✔️3.12
️✔️3.13
️✔️advisor
️✔️latest
️✔️3.12
️✔️3.13
️✔️ams
️✔️latest
️✔️3.12
️✔️3.13
️✔️apim
️✔️latest
️✔️3.12
️✔️3.13
️✔️appconfig
️✔️latest
️✔️3.12
️✔️3.13
❌appservice
❌latest
❌3.12
Type Test Case Error Message Line
Failed test_webapp_sitecontainers_convert_to_sitecontainers self = <azure.cli.testsdk.base.ExecutionResult object at 0x7f8bb3362b40>
cli_ctx = <azure.cli.core.mock.DummyCli object at 0x7f8bb5092600>
command = 'webapp sitecontainers convert --mode sitecontainers --resource-group cli_test_webapp_sitecontainers000001 --name webapp-sitecontainers-test000002'
expect_failure = False

    def in_process_execute(self, cli_ctx, command, expect_failure=False):
        from io import StringIO
        from vcr.errors import CannotOverwriteExistingCassetteException
    
        if command.startswith('az '):
            command = command[3:]
    
        stdout_buf = StringIO()
        logging_buf = StringIO()
        try:
            # issue: stderr cannot be redirect in this form, as a result some failure information
            # is lost when command fails.
>           self.exit_code = cli_ctx.invoke(shlex.split(command), out_file=stdout_buf) or 0
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

src/azure-cli-testsdk/azure/cli/testsdk/base.py:303: 
                                        
env/lib/python3.12/site-packages/knack/cli.py:245: in invoke
    exit_code = self.exception_handler(ex)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/init.py:157: in exception_handler
    return handle_exception(ex)
           ^^^^^^^^^^^^^^^^^^^^
                                        

ex = NoTTYException(), args = (), kwargs = {}

    def handle_main_exception(ex, *args, **kwargs):  # pylint: disable=unused-argument
        if isinstance(ex, CannotOverwriteExistingCassetteException):
            # This exception usually caused by a no match HTTP request. This is a product error
            # that is caused by change of SDK invocation.
            raise ex
    
>       raise CliExecutionError(ex)
E       azure.cli.testsdk.exceptions.CliExecutionError: The CLI throws exception NoTTYException during execution and fails the command.

src/azure-cli-testsdk/azure/cli/testsdk/patches.py:35: CliExecutionError

During handling of the above exception, another exception occurred:

self = <azure.cli.command_modules.appservice.tests.latest.test_webapp_commands.WebappSiteContainersTests testMethod=test_webapp_sitecontainers_convert_to_sitecontainers>
resource_group = 'cli_test_webapp_sitecontainers000001'

    @ResourceGroupPreparer(name_prefix='cli_test_webapp_sitecontainers', location='eastus')
    @AllowLargeResponse()
    def test_webapp_sitecontainers_convert_to_sitecontainers(self, resource_group):
        webapp_name = self.create_random_name('webapp-sitecontainers-test', 40)
        plan_name = self.create_random_name('webapp-sitecontainers-plan', 40)
        acr_registry_name = self.create_random_name('webappsitecontainersacr', 40)
        self.cmd('acr create --admin-enabled -g {} -n {} --sku Basic'.format(
            resource_group, acr_registry_name))
        self.cmd(
            'appservice plan create -g {} -n {} --sku S1 --is-linux' .format(resource_group, plan_name))
        creds = self.cmd('acr credential show -n {} -g {}'.format(
            acr_registry_name, resource_group)).get_output_in_json()
        self.cmd('az webapp create -g {} -n {} -p {} --container-registry-url {}.azurecr.io --container-image-name image-name:latest -s {} -w {}'.format(
            resource_group, webapp_name, plan_name, acr_registry_name, creds['username'], creds['passwords'][0]['value']
        ))
    
>       self.cmd('webapp sitecontainers convert --mode sitecontainers --resource-group {} --name {}'.
                  format(resource_group, webapp_name))

src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:3256: 
 
                                       
src/azure-cli-testsdk/azure/cli/testsdk/base.py:177: in cmd
    return execute(self.cli_ctx, command, expect_failure=expect_failure).assert_with_checks(checks)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-testsdk/azure/cli/testsdk/base.py:252: in init
    self.in_process_execute(cli_ctx, command, expect_failure=expect_failure)
src/azure-cli-testsdk/azure/cli/testsdk/base.py:315: in in_process_execute
    raise ex.exception
env/lib/python3.12/site-packages/knack/cli.py:233: in invoke
    cmd_result = self.invocation.execute(args)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:677: in execute
    raise ex
src/azure-cli-core/azure/cli/core/commands/init.py:820: in run_jobs_serially
    results.append(self.run_job(expanded_arg, cmd_copy))
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:789: in run_job
    result = cmd_copy(params)
             ^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:335: in call
    return self.handler(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/command_operation.py:120: in handler
    return op(**command_args)
           ^^^^^^^^^^^^^^^^^^
src/azure-cli/azure/cli/command_modules/appservice/custom.py:2161: in convert_webapp_sitecontainers
    if not prompt_y_n("Do you want to continue with the conversion on the production site?"):
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/knack/prompting.py:71: in prompt_y_n
    return prompt_bool(msg, 'y', 'n', default=default, help_string=help_string)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/knack/prompting.py:79: in prompt_bool
    verify_is_a_tty()
 
 
 
 
 
 
                                 _ 

    def verify_is_a_tty():
        if not sys.stdin.isatty():
            logger.debug('No tty available.')
>           raise NoTTYException()
E           knack.prompting.NoTTYException

env/lib/python3.12/site-packages/knack/prompting.py:28: NoTTYException
azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:3239
❌3.13
Type Test Case Error Message Line
Failed test_webapp_sitecontainers_convert_to_sitecontainers self = <azure.cli.testsdk.base.ExecutionResult object at 0x7fe44c866060>
cli_ctx = <azure.cli.core.mock.DummyCli object at 0x7fe44e6ab390>
command = 'webapp sitecontainers convert --mode sitecontainers --resource-group cli_test_webapp_sitecontainers000001 --name webapp-sitecontainers-test000002'
expect_failure = False

    def in_process_execute(self, cli_ctx, command, expect_failure=False):
        from io import StringIO
        from vcr.errors import CannotOverwriteExistingCassetteException
    
        if command.startswith('az '):
            command = command[3:]
    
        stdout_buf = StringIO()
        logging_buf = StringIO()
        try:
            # issue: stderr cannot be redirect in this form, as a result some failure information
            # is lost when command fails.
>           self.exit_code = cli_ctx.invoke(shlex.split(command), out_file=stdout_buf) or 0
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

src/azure-cli-testsdk/azure/cli/testsdk/base.py:303: 
                                        
env/lib/python3.13/site-packages/knack/cli.py:245: in invoke
    exit_code = self.exception_handler(ex)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/init.py:157: in exception_handler
    return handle_exception(ex)
           ^^^^^^^^^^^^^^^^^^^^
                                        

ex = NoTTYException(), args = (), kwargs = {}

    def handle_main_exception(ex, *args, **kwargs):  # pylint: disable=unused-argument
        if isinstance(ex, CannotOverwriteExistingCassetteException):
            # This exception usually caused by a no match HTTP request. This is a product error
            # that is caused by change of SDK invocation.
            raise ex
    
>       raise CliExecutionError(ex)
E       azure.cli.testsdk.exceptions.CliExecutionError: The CLI throws exception NoTTYException during execution and fails the command.

src/azure-cli-testsdk/azure/cli/testsdk/patches.py:35: CliExecutionError

During handling of the above exception, another exception occurred:

self = <azure.cli.command_modules.appservice.tests.latest.test_webapp_commands.WebappSiteContainersTests testMethod=test_webapp_sitecontainers_convert_to_sitecontainers>
resource_group = 'cli_test_webapp_sitecontainers000001'

    @ResourceGroupPreparer(name_prefix='cli_test_webapp_sitecontainers', location='eastus')
    @AllowLargeResponse()
    def test_webapp_sitecontainers_convert_to_sitecontainers(self, resource_group):
        webapp_name = self.create_random_name('webapp-sitecontainers-test', 40)
        plan_name = self.create_random_name('webapp-sitecontainers-plan', 40)
        acr_registry_name = self.create_random_name('webappsitecontainersacr', 40)
        self.cmd('acr create --admin-enabled -g {} -n {} --sku Basic'.format(
            resource_group, acr_registry_name))
        self.cmd(
            'appservice plan create -g {} -n {} --sku S1 --is-linux' .format(resource_group, plan_name))
        creds = self.cmd('acr credential show -n {} -g {}'.format(
            acr_registry_name, resource_group)).get_output_in_json()
        self.cmd('az webapp create -g {} -n {} -p {} --container-registry-url {}.azurecr.io --container-image-name image-name:latest -s {} -w {}'.format(
            resource_group, webapp_name, plan_name, acr_registry_name, creds['username'], creds['passwords'][0]['value']
        ))
    
>       self.cmd('webapp sitecontainers convert --mode sitecontainers --resource-group {} --name {}'.
                  format(resource_group, webapp_name))

src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:3256: 
 
                                       
src/azure-cli-testsdk/azure/cli/testsdk/base.py:177: in cmd
    return execute(self.cli_ctx, command, expect_failure=expect_failure).assert_with_checks(checks)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-testsdk/azure/cli/testsdk/base.py:252: in init
    self.in_process_execute(cli_ctx, command, expect_failure=expect_failure)
src/azure-cli-testsdk/azure/cli/testsdk/base.py:315: in in_process_execute
    raise ex.exception
env/lib/python3.13/site-packages/knack/cli.py:233: in invoke
    cmd_result = self.invocation.execute(args)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:677: in execute
    raise ex
src/azure-cli-core/azure/cli/core/commands/init.py:820: in run_jobs_serially
    results.append(self.run_job(expanded_arg, cmd_copy))
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:789: in run_job
    result = cmd_copy(params)
             ^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:335: in call
    return self.handler(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/command_operation.py:120: in handler
    return op(**command_args)
           ^^^^^^^^^^^^^^^^^^
src/azure-cli/azure/cli/command_modules/appservice/custom.py:2161: in convert_webapp_sitecontainers
    if not prompt_y_n("Do you want to continue with the conversion on the production site?"):
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/knack/prompting.py:71: in prompt_y_n
    return prompt_bool(msg, 'y', 'n', default=default, help_string=help_string)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/knack/prompting.py:79: in prompt_bool
    verify_is_a_tty()
 
 
 
 
 
 
                                 _ 

    def verify_is_a_tty():
        if not sys.stdin.isatty():
            logger.debug('No tty available.')
>           raise NoTTYException()
E           knack.prompting.NoTTYException

env/lib/python3.13/site-packages/knack/prompting.py:28: NoTTYException
azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py:3239
️✔️aro
️✔️latest
️✔️3.12
️✔️3.13
️✔️backup
️✔️latest
️✔️3.12
️✔️3.13
️✔️batch
️✔️latest
️✔️3.12
️✔️3.13
️✔️batchai
️✔️latest
️✔️3.12
️✔️3.13
️✔️billing
️✔️latest
️✔️3.12
️✔️3.13
️✔️botservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️cdn
️✔️latest
️✔️3.12
️✔️3.13
️✔️cloud
️✔️latest
️✔️3.12
️✔️3.13
️✔️cognitiveservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️compute_recommender
️✔️latest
️✔️3.12
️✔️3.13
️✔️computefleet
️✔️latest
️✔️3.12
️✔️3.13
️✔️config
️✔️latest
️✔️3.12
️✔️3.13
️✔️configure
️✔️latest
️✔️3.12
️✔️3.13
️✔️consumption
️✔️latest
️✔️3.12
️✔️3.13
️✔️container
️✔️latest
️✔️3.12
️✔️3.13
️✔️containerapp
️✔️latest
️✔️3.12
️✔️3.13
️✔️core
️✔️latest
️✔️3.12
️✔️3.13
️✔️cosmosdb
️✔️latest
️✔️3.12
️✔️3.13
️✔️databoxedge
️✔️latest
️✔️3.12
️✔️3.13
️✔️dls
️✔️latest
️✔️3.12
️✔️3.13
️✔️dms
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventgrid
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventhubs
️✔️latest
️✔️3.12
️✔️3.13
️✔️feedback
️✔️latest
️✔️3.12
️✔️3.13
️✔️find
️✔️latest
️✔️3.12
️✔️3.13
️✔️hdinsight
️✔️latest
️✔️3.12
️✔️3.13
️✔️identity
️✔️latest
️✔️3.12
️✔️3.13
️✔️iot
️✔️latest
️✔️3.12
️✔️3.13
️✔️keyvault
️✔️latest
️✔️3.12
️✔️3.13
️✔️lab
️✔️latest
️✔️3.12
️✔️3.13
️✔️managedservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️maps
️✔️latest
️✔️3.12
️✔️3.13
️✔️marketplaceordering
️✔️latest
️✔️3.12
️✔️3.13
️✔️monitor
️✔️latest
️✔️3.12
️✔️3.13
️✔️mysql
️✔️latest
️✔️3.12
️✔️3.13
️✔️netappfiles
️✔️latest
️✔️3.12
️✔️3.13
️✔️network
️✔️latest
️✔️3.12
️✔️3.13
️✔️policyinsights
️✔️latest
️✔️3.12
️✔️3.13
️✔️postgresql
️✔️latest
️✔️3.12
️✔️3.13
️✔️privatedns
️✔️latest
️✔️3.12
️✔️3.13
️✔️profile
️✔️latest
️✔️3.12
️✔️3.13
️✔️rdbms
️✔️latest
️✔️3.12
️✔️3.13
️✔️redis
️✔️latest
️✔️3.12
️✔️3.13
️✔️relay
️✔️latest
️✔️3.12
️✔️3.13
️✔️resource
️✔️latest
️✔️3.12
️✔️3.13
️✔️role
️✔️latest
️✔️3.12
️✔️3.13
️✔️search
️✔️latest
️✔️3.12
️✔️3.13
️✔️security
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicebus
️✔️latest
️✔️3.12
️✔️3.13
️✔️serviceconnector
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicefabric
️✔️latest
️✔️3.12
️✔️3.13
️✔️signalr
️✔️latest
️✔️3.12
️✔️3.13
️✔️sql
️✔️latest
️✔️3.12
️✔️3.13
️✔️sqlvm
️✔️latest
️✔️3.12
️✔️3.13
️✔️storage
️✔️latest
️✔️3.12
️✔️3.13
️✔️synapse
️✔️latest
️✔️3.12
️✔️3.13
️✔️telemetry
️✔️latest
️✔️3.12
️✔️3.13
️✔️util
️✔️latest
️✔️3.12
️✔️3.13
️✔️vm
️✔️latest
️✔️3.12
️✔️3.13

@azure-client-tools-bot-prd
Copy link
Copy Markdown

azure-client-tools-bot-prd bot commented Apr 3, 2026

⚠️AzureCLI-BreakingChangeTest
⚠️appservice
rule cmd_name rule_message suggest_message
⚠️ 1006 - ParaAdd webapp sitecontainers convert cmd webapp sitecontainers convert added parameter main_container_name

@yonzhan
Copy link
Copy Markdown
Collaborator

yonzhan commented Apr 3, 2026

Thank you for your contribution! We will review the pull request and get back to you soon.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 3, 2026

The git hooks are available for azure-cli and azure-cli-extensions repos. They could help you run required checks before creating the PR.

Please sync the latest code with latest dev branch (for azure-cli) or main branch (for azure-cli-extensions).
After that please run the following commands to enable git hooks:

pip install azdev --upgrade
azdev setup -c <your azure-cli repo path> -r <your azure-cli-extensions repo path>

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds COMPOSE (linuxFxVersion=COMPOSE|<base64>) support to az webapp sitecontainers convert --mode sitecontainers, enabling conversion of multi-container Docker Compose webapps into the Sitecontainers model, plus a large suite of unit tests and YAML fixtures to validate parsing and orchestration behavior.

Changes:

  • Extend convert_webapp_sitecontainers to convert both DOCKER and COMPOSE apps to sitecontainers (including --main-container-name, warnings, and rollback-on-failure behavior).
  • Introduce Compose parsing/mapping helpers for environment variables, ports, volumes, entrypoint/command → startUpCommand, and service name sanitization.
  • Add comprehensive unit tests + sample Compose YAML fixtures covering supported/unsupported scenarios.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/azure-cli/azure/cli/command_modules/appservice/custom.py Implements COMPOSE→sitecontainers conversion flow, parsing helpers, and production-site warning prompt.
src/azure-cli/azure/cli/command_modules/appservice/_params.py Adds --main-container-name parameter for conversion command.
src/azure-cli/azure/cli/command_modules/appservice/_help.py Updates help text/examples to mention COMPOSE support and main-container selection.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_compose_convert.py Adds unit tests for helper parsing and conversion orchestration (mocked ARM calls).
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-basic.yml Fixture: basic multi-container compose.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-env-mapping.yml Fixture: env mapping format.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-env-sequence.yml Fixture: env sequence format.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-entrypoint-command.yml Fixture: entrypoint/command variants.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-volumes-bind.yml Fixture: ${WEBAPP_STORAGE_HOME} bind mounts.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-volumes-long.yml Fixture: long-syntax volumes.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-volumes-named.yml Fixture: named volumes.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-unsupported-bind.yml Fixture: unsupported host/relative bind mounts.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-port-conflict.yml Fixture: port conflict scenario.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-no-ports.yml Fixture: no ports → main-container fallback.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-multi-port.yml Fixture: multiple ports per service.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-full.yml Fixture: full realistic multi-service compose.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/compose-convert-underscore-names.yml Fixture: service name sanitization cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

url = (
f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/"
f"providers/Microsoft.Web/sites/{name}{slot_segment}/config/appsettings/list?api-version=2023-12-01"
f"providers/Microsoft.Web/sites/{name}{slot_segment}/config/appsettings/list?api-version=2024-11-01"
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The app settings list ARM call hard-codes api-version=2024-11-01. This is a compatibility risk (e.g., older clouds / API profiles) and it’s also the only place in this module using that version. Consider reverting to the previously used API version for this endpoint or using the existing get_app_settings(...) / _get_site_props_for_sitecontainer_app_internal(...) helpers instead of a raw request.

Suggested change
f"providers/Microsoft.Web/sites/{name}{slot_segment}/config/appsettings/list?api-version=2024-11-01"
f"providers/Microsoft.Web/sites/{name}{slot_segment}/config/appsettings/list"
f"?api-version={VERSION_2022_09_01}"

Copilot uses AI. Check for mistakes.
ComposeFileParser.ParsePorts). Returns a list of tuples.
"""
if ports_node is None:
return []
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

_parse_compose_ports assumes ports_node is iterable and will iterate over characters if a scalar string is provided (e.g. ports: "80:80"). Add a type check similar to the env/volumes parsers (accept list, optionally accept scalar string by wrapping it in a list) to avoid silently mis-parsing ports.

Suggested change
return []
return []
if isinstance(ports_node, str):
ports_node = [ports_node]
elif not isinstance(ports_node, list):
logger.warning(" [ports] Unexpected ports format (not list or string). Skipping.")
return []

Copilot uses AI. Check for mistakes.
Comment on lines +3136 to +3162
# Create the SiteContainer directly (not via create_webapp_sitecontainers)
# because environment_variables and volume_mounts are not exposed as
# individual kwargs on the higher-level create function.
auth_type = AuthType.ANONYMOUS
if auth_kwargs.get("system_assigned_identity"):
auth_type = AuthType.SYSTEM_IDENTITY
elif auth_kwargs.get("user_assigned_identity"):
auth_type = AuthType.USER_ASSIGNED
elif auth_kwargs.get("registry_username") and auth_kwargs.get("registry_password"):
auth_type = AuthType.USER_CREDENTIALS

sitecontainer = SiteContainer(
image=spec["image"],
target_port=spec["target_port"],
start_up_command=spec["startup_command"],
is_main=is_main,
auth_type=auth_type,
user_name=auth_kwargs.get("registry_username"),
password_secret=auth_kwargs.get("registry_password"),
user_managed_identity_client_id=auth_kwargs.get("user_assigned_identity"),
volume_mounts=spec["volume_mounts"],
environment_variables=spec["env_variables"],
# Non-main (sidecar) containers should NOT inherit the webapp's
# app settings and connection strings by default. They receive
# only the env vars explicitly declared in the compose file.
inherit_app_settings_and_connection_strings=None if is_main else False,
)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The COMPOSE conversion path calls _create_or_update_webapp_sitecontainer_internal(...) directly, which bypasses _validate_sitecontainer_internal(...) (targetPort range/uniqueness, env var app-setting reference validation, and managed identity enablement checks). Consider reusing the same validation pipeline as create_webapp_sitecontainers(...) (e.g., call _get_site_props_for_sitecontainer_app_internal(...) once and validate each SiteContainerSpec before making ARM create calls) so failures are caught early and errors are more actionable.

Copilot uses AI. Check for mistakes.
Comment on lines +3105 to +3107
# -----------------------------------------------------------------------
if new_app_settings:
logger.warning("Creating %d app setting(s) for environment variable references...", len(new_app_settings))
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

App settings are updated (Step 6) before any sitecontainer resources are created, but the rollback logic on failure only deletes created containers. If a later container create fails, the conversion leaves behind newly added/modified COMPOSE_... app settings. Consider either (a) applying app settings after all container validation passes and/or (b) tracking previous values and reverting the changes on rollback.

Suggested change
# -----------------------------------------------------------------------
if new_app_settings:
logger.warning("Creating %d app setting(s) for environment variable references...", len(new_app_settings))
# -----------------------------------------------------------------------
previous_new_app_settings = {}
if new_app_settings:
logger.warning("Creating %d app setting(s) for environment variable references...", len(new_app_settings))
client = web_client_factory(cmd.cli_ctx)
if slot:
current_app_settings = client.web_apps.list_application_settings_slot(
resource_group_name=resource_group,
name=name,
slot=slot
).properties or {}
else:
current_app_settings = client.web_apps.list_application_settings(
resource_group_name=resource_group,
name=name
).properties or {}
previous_new_app_settings = {
key: current_app_settings[key]
for key in new_app_settings
if key in current_app_settings
}

Copilot uses AI. Check for mistakes.
import os
import unittest
from base64 import b64encode
from unittest.mock import MagicMock, patch, call
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

call is imported but never used in this test module. This can trip pylint/flake8 unused-import checks; please remove it.

Suggested change
from unittest.mock import MagicMock, patch, call
from unittest.mock import MagicMock, patch

Copilot uses AI. Check for mistakes.
_make_named_volume_mount,
_sanitize_container_name,
_convert_compose_to_sitecontainers,
_COMPOSE_WEBAPP_STORAGE_HOME,
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

_COMPOSE_WEBAPP_STORAGE_HOME is imported from custom but not referenced anywhere in this test file, which is likely to fail unused-import linting. Please remove it or add a test that exercises/uses it.

Suggested change
_COMPOSE_WEBAPP_STORAGE_HOME,

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +20
API_KEY: "s3cret-key-value"
db:
image: "postgres:15-alpine"
ports:
- "5432:5432"
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: "p@ssw0rd"
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

These fixtures include password/API-key-like literal values (e.g., API_KEY, POSTGRES_PASSWORD) which can trigger repository secret scanning and fail CI. Consider replacing with clearly fake placeholders (e.g., not-a-real-password) or otherwise aligning with the repo’s secret-scan suppression approach for test data.

Suggested change
API_KEY: "s3cret-key-value"
db:
image: "postgres:15-alpine"
ports:
- "5432:5432"
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: "p@ssw0rd"
API_KEY: "not-a-real-api-key"
db:
image: "postgres:15-alpine"
ports:
- "5432:5432"
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: "not-a-real-password"

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +47
environment:
WORDPRESS_DB_HOST: "localhost:3306"
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: "wp_s3cret"
WORDPRESS_DB_NAME: wordpress
WORDPRESS_TABLE_PREFIX: wp_
WORDPRESS_CONFIG_EXTRA: |
define('WP_REDIS_HOST', 'localhost');
define('WP_REDIS_PORT', 6379);
restart: always

mariadb:
image: "mariadb:11"
ports:
- "3306:3306"
volumes:
- "${WEBAPP_STORAGE_HOME}/mysql/data:/var/lib/mysql"
environment:
- MYSQL_ROOT_PASSWORD=rootpass123
- MYSQL_DATABASE=wordpress
- MYSQL_USER=wp_user
- MYSQL_PASSWORD=wp_s3cret
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This sample compose file contains password-like values (e.g., WORDPRESS_DB_PASSWORD, MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD). These strings may be flagged by automated secret scanners in CI. Consider swapping them to obvious non-secret placeholders to avoid false positives in security tooling.

Copilot uses AI. Check for mistakes.
- "80:80"
environment:
- REDIS_URL=redis://localhost:6379
- APP_SECRET=mysecretvalue
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

APP_SECRET=mysecretvalue and similar literals in fixtures can trigger secret scanning (even though they’re test-only). Consider using clearly fake placeholders that won’t match common secret patterns.

Suggested change
- APP_SECRET=mysecretvalue
- APP_SECRET=not-a-real-secret

Copilot uses AI. Check for mistakes.
volumes:
- "${WEBAPP_STORAGE_HOME}/mysql/data:/var/lib/mysql"
environment:
MYSQL_ROOT_PASSWORD: rootpass
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

MYSQL_ROOT_PASSWORD: rootpass looks like a real secret and may be flagged by CI secret scanning despite being test data. Consider replacing with an explicit placeholder value (e.g., example-password) that won’t match common password heuristics.

Suggested change
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_ROOT_PASSWORD: example-password

Copilot uses AI. Check for mistakes.
@yonzhan yonzhan assigned yanzhudd and unassigned zhoxing-ms Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants