Skip to content

Commit

Permalink
Support CORS policy and mTLS for CLI (#6420)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhenqxuMSFT authored Jun 26, 2023
1 parent 0e70a46 commit 8c36970
Show file tree
Hide file tree
Showing 10 changed files with 6,631 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Upcoming
* 'az containerapp create': --registry-server support registry with custom port
* 'az containerapp create': fix containerapp create not waiting for ready environment
* Add regex to fix validation for containerapp name
* Add 'az containerapp ingress cors' for CORS support
* 'az container app env create/update': support --enable-mtls parameter

0.3.33
++++++
Expand Down
44 changes: 44 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,50 @@
az containerapp ingress sticky-sessions show -n MyContainerapp -g MyResourceGroup
"""

helps['containerapp ingress cors'] = """
type: group
short-summary: Commands to manage CORS policy for a container app.
"""

helps['containerapp ingress cors enable'] = """
type: command
short-summary: Enable CORS policy for a container app.
examples:
- name: Set allowed origins and allowed methods for a container app.
text: |
az containerapp ingress cors enable -n MyContainerapp -g MyResourceGroup --allowed-origins http://www.contoso.com https://www.contoso.com --allowed-methods GET POST
- name: Set allowed origins, allowed methods and allowed headers for a container app.
text: |
az containerapp ingress cors enable -n MyContainerapp -g MyResourceGroup --allowed-origins * --allowed-methods * --allowed-headers header1 header2
"""

helps['containerapp ingress cors disable'] = """
type: command
short-summary: Disable CORS policy for a container app.
examples:
- name: Disable CORS policy for a container app.
text: |
az containerapp ingress cors disable -n MyContainerapp -g MyResourceGroup
"""

helps['containerapp ingress cors update'] = """
type: command
short-summary: Update CORS policy for a container app.
examples:
- name: Update allowed origins and allowed methods for a container app while keeping other cors settings.
text: |
az containerapp ingress cors update -n MyContainerapp -g MyResourceGroup --allowed-origins http://www.contoso.com https://www.contoso.com --allowed-methods GET POST
"""

helps['containerapp ingress cors show'] = """
type: command
short-summary: Show CORS policy for a container app.
examples:
- name: Show CORS policy for a container app.
text: |
az containerapp ingress cors show -n MyContainerapp -g MyResourceGroup
"""

# Registry Commands
helps['containerapp registry'] = """
type: group
Expand Down
13 changes: 12 additions & 1 deletion src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from ._validators import (validate_memory, validate_cpu, validate_managed_env_name_or_id, validate_registry_server,
validate_registry_user, validate_registry_pass, validate_target_port, validate_ingress,
validate_storage_name_or_id)
validate_storage_name_or_id, validate_cors_max_age)
from ._constants import UNAUTHENTICATED_CLIENT_ACTION, FORWARD_PROXY_CONVENTION, MAXIMUM_CONTAINER_APP_NAME_LENGTH, LOG_TYPE_CONSOLE, LOG_TYPE_SYSTEM


Expand Down Expand Up @@ -178,6 +178,9 @@ def load_arguments(self, _):
c.argument('certificate_file', options_list=['--custom-domain-certificate-file', '--certificate-file'], help='The filepath of the certificate file (.pfx or .pem) for the environment\'s custom domain. To manage certificates for container apps, use `az containerapp env certificate`.')
c.argument('certificate_password', options_list=['--custom-domain-certificate-password', '--certificate-password'], help='The certificate file password for the environment\'s custom domain.')

with self.argument_context('containerapp env', arg_group='Peer Authentication') as c:
c.argument('mtls_enabled', arg_type=get_three_state_flag(), options_list=['--enable-mtls'], help='Boolean indicating if mTLS peer authentication is enabled for the environment.')

with self.argument_context('containerapp service') as c:
c.argument('service_name', options_list=['--name', '-n'], help="The service name.")
c.argument('environment_name', options_list=['--environment'], help="The environment name.")
Expand Down Expand Up @@ -299,6 +302,14 @@ def load_arguments(self, _):
with self.argument_context('containerapp ingress sticky-sessions') as c:
c.argument('affinity', arg_type=get_enum_type(['sticky', 'none']), help='Whether the affinity for the container app is Sticky or None.')

with self.argument_context('containerapp ingress cors') as c:
c.argument('allowed_origins', nargs='*', options_list=['--allowed-origins', '-r'], help="A list of allowed origin(s) for the container app. Values are space-separated. Empty string to clear existing values.")
c.argument('allowed_methods', nargs='*', options_list=['--allowed-methods', '-m'], help="A list of allowed method(s) for the container app. Values are space-separated. Empty string to clear existing values.")
c.argument('allowed_headers', nargs='*', options_list=['--allowed-headers', '-a'], help="A list of allowed header(s) for the container app. Values are space-separated. Empty string to clear existing values.")
c.argument('expose_headers', nargs='*', options_list=['--expose-headers', '-e'], help="A list of expose header(s) for the container app. Values are space-separated. Empty string to clear existing values.")
c.argument('allow_credentials', options_list=['--allow-credentials'], arg_type=get_three_state_flag(), help='Whether the credential is allowed for the container app.')
c.argument('max_age', nargs='?', const='', validator=validate_cors_max_age, help="The maximum age of the allowed origin in seconds. Only postive integer or empty string are allowed. Empty string resets max_age to null.")

with self.argument_context('containerapp secret') as c:
c.argument('secrets', nargs='+', options_list=['--secrets', '-s'], help="A list of secret(s) for the container app. Space-separated values in 'key=value' or 'key=keyvaultref:keyvaulturl,identityref:identity' format (where 'key' cannot be longer than 20 characters).")
c.argument('secret_name', help="The name of the secret to show.")
Expand Down
13 changes: 13 additions & 0 deletions src/containerapp/azext_containerapp/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,16 @@ def validate_ssh(cmd, namespace):
_validate_revision_exists(cmd, namespace)
_validate_replica_exists(cmd, namespace)
_validate_container_exists(cmd, namespace)


def validate_cors_max_age(cmd, namespace):
if namespace.max_age:
try:
if namespace.max_age == "":
return

max_age = int(namespace.max_age)
if max_age < 0:
raise InvalidArgumentValueError("max-age must be a positive integer.")
except ValueError:
raise InvalidArgumentValueError("max-age must be an integer.")
6 changes: 6 additions & 0 deletions src/containerapp/azext_containerapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ def load_command_table(self, _):
g.custom_command('remove', 'remove_ip_restriction')
g.custom_show_command('list', 'show_ip_restrictions')

with self.command_group('containerapp ingress cors') as g:
g.custom_command('enable', 'enable_cors_policy', exception_handler=ex_handler_factory())
g.custom_command('disable', 'disable_cors_policy', exception_handler=ex_handler_factory())
g.custom_command('update', 'update_cors_policy', exception_handler=ex_handler_factory())
g.custom_show_command('show', 'show_cors_policy')

with self.command_group('containerapp registry') as g:
g.custom_command('set', 'set_registry', exception_handler=ex_handler_factory())
g.custom_show_command('show', 'show_registry')
Expand Down
121 changes: 121 additions & 0 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1319,6 +1319,7 @@ def create_managed_environment(cmd,
certificate_file=None,
certificate_password=None,
enable_workload_profiles=False,
mtls_enabled=None,
no_wait=False):
if zone_redundant:
if not infrastructure_subnet_resource_id:
Expand Down Expand Up @@ -1395,6 +1396,8 @@ def create_managed_environment(cmd,
raise ValidationError('Infrastructure subnet resource ID needs to be supplied for internal only environments.')
managed_env_def["properties"]["vnetConfiguration"]["internal"] = True

if mtls_enabled is not None:
safe_set(managed_env_def, "properties", "peerAuthentication", "mtls", "enabled", value=mtls_enabled)
try:
r = ManagedEnvironmentClient.create(
cmd=cmd, resource_group_name=resource_group_name, name=name, managed_environment_envelope=managed_env_def, no_wait=no_wait)
Expand Down Expand Up @@ -1429,6 +1432,7 @@ def update_managed_environment(cmd,
workload_profile_name=None,
min_nodes=None,
max_nodes=None,
mtls_enabled=None,
no_wait=False):
if logs_destination == "log-analytics" or logs_customer_id or logs_key:
if logs_destination != "log-analytics":
Expand Down Expand Up @@ -1499,6 +1503,9 @@ def update_managed_environment(cmd,

safe_set(env_def, "properties", "workloadProfiles", value=workload_profiles)

if mtls_enabled is not None:
safe_set(env_def, "properties", "peerAuthentication", "mtls", "enabled", value=mtls_enabled)

try:
r = ManagedEnvironmentClient.update(
cmd=cmd, resource_group_name=resource_group_name, name=name, managed_environment_envelope=env_def, no_wait=no_wait)
Expand Down Expand Up @@ -3526,6 +3533,120 @@ def show_ingress_sticky_session(cmd, name, resource_group_name):
raise ValidationError("Ingress must be enabled to enable sticky sessions. Try running `az containerapp ingress -h` for more info.") from e


def enable_cors_policy(cmd, name, resource_group_name, allowed_origins, allowed_methods=None, allowed_headers=None, expose_headers=None, allow_credentials=None, max_age=None, no_wait=False):
_validate_subscription_registered(cmd, CONTAINER_APPS_RP)

if not allowed_origins:
raise ValidationError("Allowed origins must be specified.")

containerapp_def = None
try:
containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name)
except:
pass

if not containerapp_def:
raise ResourceNotFoundError(f"The containerapp '{name}' does not exist in group '{resource_group_name}'")

containerapp_patch = {}
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "allowedOrigins", value=allowed_origins)
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "allowedMethods", value=allowed_methods)
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "allowedHeaders", value=allowed_headers)
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "exposeHeaders", value=expose_headers)
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "allowCredentials", value=allow_credentials)
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "maxAge", value=max_age)
try:
r = ContainerAppClient.update(
cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_patch, no_wait=no_wait)
return safe_get(r, "properties", "configuration", "ingress", "corsPolicy", default={})
except Exception as e:
handle_raw_exception(e)


def disable_cors_policy(cmd, name, resource_group_name, no_wait=False):
_validate_subscription_registered(cmd, CONTAINER_APPS_RP)

containerapp_def = None
try:
containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name)
except:
pass

if not containerapp_def:
raise ResourceNotFoundError(f"The containerapp '{name}' does not exist in group '{resource_group_name}'")

containerapp_patch = {}
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", value=None)
try:
r = ContainerAppClient.update(
cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_patch, no_wait=no_wait)
return safe_get(r, "properties", "configuration", "ingress", default={})
except Exception as e:
handle_raw_exception(e)


def update_cors_policy(cmd, name, resource_group_name, allowed_origins=None, allowed_methods=None, allowed_headers=None, expose_headers=None, allow_credentials=None, max_age=None, no_wait=False):
_validate_subscription_registered(cmd, CONTAINER_APPS_RP)

containerapp_def = None
try:
containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name)
except:
pass

if not containerapp_def:
raise ResourceNotFoundError(f"The containerapp '{name}' does not exist in group '{resource_group_name}'")

if allowed_origins is not None and len(allowed_origins) == 0:
raise RequiredArgumentMissingError("allowed-origins must be specified if provided.")

reset_max_age = False
if max_age == "":
reset_max_age = True

containerapp_patch = {}
if allowed_origins is not None:
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "allowedOrigins", value=allowed_origins)
if allowed_methods is not None:
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "allowedMethods", value=allowed_methods)
if allowed_headers is not None:
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "allowedHeaders", value=allowed_headers)
if expose_headers is not None:
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "exposeHeaders", value=expose_headers)
if allow_credentials is not None:
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "allowCredentials", value=allow_credentials)
if max_age is not None:
if reset_max_age:
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "maxAge", value=None)
else:
safe_set(containerapp_patch, "properties", "configuration", "ingress", "corsPolicy", "maxAge", value=max_age)

try:
r = ContainerAppClient.update(
cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_patch, no_wait=no_wait)
return safe_get(r, "properties", "configuration", "ingress", "corsPolicy", default={})
except Exception as e:
handle_raw_exception(e)


def show_cors_policy(cmd, name, resource_group_name):
_validate_subscription_registered(cmd, CONTAINER_APPS_RP)

containerapp_def = None
try:
containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name)
except:
pass

if not containerapp_def:
raise ResourceNotFoundError(f"The containerapp '{name}' does not exist in group '{resource_group_name}'")

try:
return safe_get(containerapp_def, "properties", "configuration", "ingress", "corsPolicy", default={})
except Exception as e:
raise ValidationError("CORS must be enabled to enable CORS policy. Try running `az containerapp ingress cors enable -h` for more info.") from e


def show_registry(cmd, name, resource_group_name, server):
_validate_subscription_registered(cmd, CONTAINER_APPS_RP)

Expand Down
Loading

0 comments on commit 8c36970

Please sign in to comment.