Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 61 additions & 6 deletions src/azure-cli/azure/cli/command_modules/appservice/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -1570,9 +1570,10 @@
type: command
short-summary: Get a web app's connection strings.
examples:
- name: Get a web app's connection strings. (autogenerated)
- name: Get a web app's connection strings.
text: az webapp config connection-string list --name MyWebapp --resource-group MyResourceGroup
crafted: true
- name: Get a web app's connection strings using a resource ID.
text: az webapp config connection-string list --ids /subscriptions/{SubID}/resourceGroups/{ResourceGroup}/providers/Microsoft.Web/sites/{WebApp}
"""

helps['webapp config connection-string set'] = """
Expand Down Expand Up @@ -1778,6 +1779,53 @@
text: az webapp config ssl create --resource-group MyResourceGroup --name MyWebapp --hostname cname.mycustomdomain.com
"""

helps['webapp config public-cert'] = """
type: group
short-summary: Manage public certificates for a web app.
"""

helps['webapp config public-cert upload'] = """
type: command
short-summary: Upload a public certificate (.cer or .crt) to a web app.
long-summary: >
Public certificates are used to allow the web app to make outbound calls to services that
require certificate-based authentication. Unlike SSL certificates, public certificates do not
bind to a hostname.
examples:
- name: Upload a public certificate to a web app.
text: >
az webapp config public-cert upload -g MyResourceGroup -n MyWebapp
--public-certificate-name MyCert --certificate-file /path/to/cert.cer
- name: Upload a public certificate to a deployment slot.
text: >
az webapp config public-cert upload -g MyResourceGroup -n MyWebapp --slot staging
--public-certificate-name MyCert --certificate-file /path/to/cert.cer
"""

helps['webapp config public-cert list'] = """
type: command
short-summary: List public certificates for a web app.
examples:
- name: List public certificates for a web app.
text: az webapp config public-cert list -g MyResourceGroup -n MyWebapp
"""

helps['webapp config public-cert show'] = """
type: command
short-summary: Show the details of a public certificate for a web app.
examples:
- name: Show a public certificate.
text: az webapp config public-cert show -g MyResourceGroup -n MyWebapp --public-certificate-name MyCert
"""

helps['webapp config public-cert delete'] = """
type: command
short-summary: Delete a public certificate from a web app.
examples:
- name: Delete a public certificate from a web app.
text: az webapp config public-cert delete -g MyResourceGroup -n MyWebapp --public-certificate-name MyCert
"""

helps['webapp config storage-account'] = """
type: group
short-summary: Manage a web app's Azure storage account configurations.
Expand Down Expand Up @@ -2285,13 +2333,20 @@
helps['webapp log config'] = """
type: command
short-summary: Configure logging for a web app.
long-summary: >
Use this command to turn on or off application logging, web server logging, and docker container logging.
For Linux and custom container web apps, --docker-container-logging controls whether STDOUT and STDERR
from the container are collected. Use 'filesystem' to enable (logs viewable via `az webapp log tail` or
downloadable via `az webapp log download`) or 'off' to disable.
examples:
- name: Configure logging for a web app. (autogenerated)
- name: Turn off web server logging.
text: az webapp log config --name MyWebapp --resource-group MyResourceGroup --web-server-logging off
crafted: true
- name: Configure logging for a web app. (autogenerated)
- name: Disable docker container logging for a Linux/container web app.
text: az webapp log config --docker-container-logging off --name MyWebapp --resource-group MyResourceGroup
crafted: true
- name: Enable docker container logging (write to filesystem) for a Linux/container web app.
text: az webapp log config --docker-container-logging filesystem --name MyWebapp --resource-group MyResourceGroup
- name: Configure application logging to write to the filesystem.
text: az webapp log config --application-logging filesystem --name MyWebapp --resource-group MyResourceGroup
"""

helps['webapp log download'] = """
Expand Down
34 changes: 23 additions & 11 deletions src/azure-cli/azure/cli/command_modules/appservice/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ def load_arguments(self, _):
c.argument('startup_file', help="Linux only. The web's startup file")
c.argument('sitecontainers_app', help="If true, a webapp which supports sitecontainers will be created", arg_type=get_three_state_flag())
c.argument('deployment_container_image_name', options_list=['--deployment-container-image-name', '-i'], help='Container image name from container registry, e.g. publisher/image-name:tag', deprecate_info=c.deprecate(target='--deployment-container-image-name'))
c.argument('container_registry_url', options_list=['--container-registry-url'], help='The container registry server url')
c.argument('container_registry_url', options_list=['--container-registry-url', c.deprecate(target='--docker-registry-server-url', redirect='--container-registry-url')], help='The container registry server url')
c.argument('container_image_name', options_list=['--container-image-name', '-c'],
help='The container custom image name and optionally the tag name (e.g., `<registry-name>/<image-name>:<tag>`)')
c.argument('container_registry_user', options_list=['--container-registry-user', '-s', c.deprecate(target='--docker-registry-server-user', redirect='--container-registry-user')], help='The container registry server username')
Expand Down Expand Up @@ -521,6 +521,21 @@ def load_arguments(self, _):
c.argument('hostname', help='The custom domain name')
c.argument('name', options_list=['--name', '-n'], help='Name of the web app.')
c.argument('resource-group', options_list=['--resource-group', '-g'], help='Name of resource group.')
with self.argument_context(scope + ' config public-cert') as c:
c.argument('public_certificate_name', options_list=['--public-certificate-name'],
help='The name of the public certificate.')
c.argument('slot', options_list=['--slot', '-s'],
help='The name of the slot. Default to the productions slot if not specified')
with self.argument_context(scope + ' config public-cert upload') as c:
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

public_certificate_name is defined only as an optional flag (--public-certificate-name) and is not marked required. For upload/show/delete, this parameter is mandatory; without required=True (or making it positional), the CLI will accept the command without it and fail later when calling the SDK with None. Suggest marking it required for the specific subcommands (override in ... public-cert list to keep it optional there).

Suggested change
with self.argument_context(scope + ' config public-cert upload') as c:
with self.argument_context(scope + ' config public-cert upload') as c:
c.argument('public_certificate_name', options_list=['--public-certificate-name'],
help='The name of the public certificate.', required=True)

Copilot uses AI. Check for mistakes.
c.argument('certificate_file', type=file_type,
help='The filepath for the .cer or .crt public certificate file')
c.argument('public_certificate_location', options_list=['--certificate-location'],
help='Location (certificate store) for the public certificate',
arg_type=get_enum_type(['CurrentUserMy', 'LocalMachineMy', 'Unknown']),
default='CurrentUserMy')
with self.argument_context(scope + ' config public-cert list') as c:
c.argument('name', arg_type=(webapp_name_arg_type if scope == 'webapp' else functionapp_name_arg_type),
id_part=None)
with self.argument_context(scope + ' config hostname') as c:
c.argument('hostname', completer=get_hostname_completion_list,
help="hostname assigned to the site, such as custom domains", id_part='child_name_1')
Expand Down Expand Up @@ -682,7 +697,7 @@ def load_arguments(self, _):
c.argument('settings', nargs='+', help="space-separated configuration for the number of pre-allocated instances in the format `<name>=<value>`")

with self.argument_context('webapp config connection-string list') as c:
c.argument('name', arg_type=webapp_name_arg_type, id_part=None)
c.argument('name', arg_type=webapp_name_arg_type)

with self.argument_context('webapp config storage-account list') as c:
c.argument('name', arg_type=webapp_name_arg_type, id_part=None)
Expand Down Expand Up @@ -751,7 +766,12 @@ def load_arguments(self, _):
arg_type=get_enum_type(['error', 'warning', 'information', 'verbose']))
c.argument('web_server_logging', help='configure Web server logging',
arg_type=get_enum_type(['off', 'filesystem']))
c.argument('docker_container_logging', help='configure gathering STDOUT and STDERR output from container',
c.argument('docker_container_logging',
help="Configure gathering STDOUT and STDERR output from container. "
"'filesystem' enables collection and storage on the web app's file system "
"(accessible via log stream and log download). "
"'off' disables container output collection. "
"Applies to Linux web apps and Windows container web apps.",
arg_type=get_enum_type(['off', 'filesystem']))

with self.argument_context('webapp log tail') as c:
Expand Down Expand Up @@ -795,14 +815,6 @@ def load_arguments(self, _):
with self.argument_context('webapp config connection-string') as c:
c.argument('connection_string_type', options_list=['--connection-string-type', '-t'],
help='connection string type', arg_type=get_enum_type(ConnectionStringType))
c.argument('ids', options_list=['--ids'],
help="One or more resource IDs (space delimited). If provided no other 'Resource Id' arguments should be specified.",
required=True)
c.argument('resource_group', options_list=['--resource-group', '-g'],
help='Name of resource group. You can configure the default group using `az configure --default-group=<name>`. If `--ids` is provided this should NOT be specified.')
c.argument('name', options_list=['--name', '-n'],
help='Name of the web app. You can configure the default using `az configure --defaults web=<name>`. If `--ids` is provided this should NOT be specified.',
local_context_attribute=LocalContextAttribute(name='web_name', actions=[LocalContextAction.GET]))

with self.argument_context('webapp config storage-account') as c:
c.argument('custom_id', options_list=['--custom-id', '-i'], help='name of the share configured within the web app')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ def load_command_table(self, _):
g.custom_command('import', 'import_ssl_cert', exception_handler=ex_handler_factory())
g.custom_command('create', 'create_managed_ssl_cert', exception_handler=ex_handler_factory(), is_preview=True)

with self.command_group('webapp config public-cert') as g:
g.custom_command('upload', 'upload_public_cert')
g.custom_command('list', 'list_public_certs')
g.custom_show_command('show', 'show_public_cert')
g.custom_command('delete', 'delete_public_cert', confirmation=True)

Comment on lines +206 to +211
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

New webapp config public-cert commands are added, but there don't appear to be any tests covering them in the existing appservice test suite (no references in test_webapp_commands*.py). Given these commands are new surface area, please add at least mock tests for upload/list/show/delete (and slot variants) to prevent regressions and to validate the request payload shape (e.g., the blob encoding and public_certificate_location).

Copilot uses AI. Check for mistakes.
with self.command_group('webapp config backup') as g:
g.custom_command('list', 'list_backups')
g.custom_show_command('show', 'show_backup_configuration')
Expand Down
54 changes: 54 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,15 @@ def create_webapp(cmd, resource_group_name, name, plan, runtime=None, startup_fi
if name_validation.name_available:
site_config.app_settings.append(NameValuePair(name="WEBSITES_ENABLE_APP_SERVICE_STORAGE",
value="false"))
if container_registry_url:
site_config.app_settings.append(NameValuePair(name="DOCKER_REGISTRY_SERVER_URL",
value=container_registry_url))
if container_registry_user:
site_config.app_settings.append(NameValuePair(name="DOCKER_REGISTRY_SERVER_USERNAME",
value=container_registry_user))
if container_registry_password:
site_config.app_settings.append(NameValuePair(name="DOCKER_REGISTRY_SERVER_PASSWORD",
value=container_registry_password))
Comment on lines +287 to +295
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

In create_webapp (Linux), the registry app settings are only set in the container_image_name branch. If the user creates a Linux multi-container webapp (--multicontainer-config-type/--multicontainer-config-file) with private registry credentials, those DOCKER_REGISTRY_SERVER_* settings still won't be applied, which is the main scenario in #17674. Consider moving the DOCKER_REGISTRY_SERVER_URL/USERNAME/PASSWORD app-setting logic so it also runs for the multicontainer path (and any other Linux container paths that need registry auth).

Copilot uses AI. Check for mistakes.
elif multicontainer_config_type and multicontainer_config_file:
encoded_config_file = _get_linux_multicontainer_encoded_config_from_file(multicontainer_config_file)
site_config.linux_fx_version = _format_fx_version(encoded_config_file, multicontainer_config_type)
Expand Down Expand Up @@ -5877,6 +5886,51 @@ def delete_ssl_cert(cmd, resource_group_name, certificate_thumbprint):
raise ResourceNotFoundError("Certificate for thumbprint '{}' not found".format(certificate_thumbprint))


def upload_public_cert(cmd, resource_group_name, name, public_certificate_name,
certificate_file, slot=None,
public_certificate_location='CurrentUserMy'):
PublicCertificate = cmd.get_models('PublicCertificate')
client = web_client_factory(cmd.cli_ctx)
with open(certificate_file, 'rb') as f:
cert_contents = f.read()
import base64
cert_blob = base64.b64encode(cert_contents)
public_cert = PublicCertificate(
blob=cert_blob,
Comment on lines +5896 to +5899
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

upload_public_cert base64-encodes the certificate bytes before assigning to PublicCertificate(blob=...). Other appservice certificate uploads (e.g., Certificate(pfx_blob=cert_contents)) pass raw bytes and let the SDK serializer handle base64 encoding for bytearray fields. As written, this risks double-encoding the cert and sending invalid content to the service. Prefer passing the raw cert_contents (or, if the SDK field is actually a string, explicitly .decode('utf-8') after base64 encoding) but avoid sending base64 bytes.

Suggested change
import base64
cert_blob = base64.b64encode(cert_contents)
public_cert = PublicCertificate(
blob=cert_blob,
public_cert = PublicCertificate(
blob=cert_contents,

Copilot uses AI. Check for mistakes.
public_certificate_location=public_certificate_location
)
if slot:
return client.web_apps.create_or_update_public_certificate_slot(
resource_group_name, name, public_certificate_name, public_cert, slot)
return client.web_apps.create_or_update_public_certificate(
resource_group_name, name, public_certificate_name, public_cert)


def list_public_certs(cmd, resource_group_name, name, slot=None):
client = web_client_factory(cmd.cli_ctx)
if slot:
return client.web_apps.list_public_certificates_slot(resource_group_name, name, slot)
return client.web_apps.list_public_certificates(resource_group_name, name)


def delete_public_cert(cmd, resource_group_name, name, public_certificate_name, slot=None):
client = web_client_factory(cmd.cli_ctx)
if slot:
return client.web_apps.delete_public_certificate_slot(
resource_group_name, name, public_certificate_name, slot)
return client.web_apps.delete_public_certificate(
resource_group_name, name, public_certificate_name)


def show_public_cert(cmd, resource_group_name, name, public_certificate_name, slot=None):
client = web_client_factory(cmd.cli_ctx)
if slot:
return client.web_apps.get_public_certificate_slot(
resource_group_name, name, public_certificate_name, slot)
return client.web_apps.get_public_certificate(
resource_group_name, name, public_certificate_name)


def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_name, name=None, certificate_name=None):
Certificate = cmd.get_models('Certificate')
client = web_client_factory(cmd.cli_ctx)
Expand Down
Loading