Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support CORS policy and mTLS for CLI #6420

Merged
merged 10 commits into from
Jun 26, 2023
Merged
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
2 changes: 2 additions & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Upcoming
* 'az containerapp service': add support for creation and deletion of kafka
* 'az containerapp create': --registry-server support registry with custom port
* 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:
zhenqxuMSFT marked this conversation as resolved.
Show resolved Hide resolved
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())
zhenqxuMSFT marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -1314,6 +1314,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 @@ -1390,6 +1391,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 @@ -1424,6 +1427,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 @@ -1494,6 +1498,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 @@ -3521,6 +3528,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.")
zhenqxuMSFT marked this conversation as resolved.
Show resolved Hide resolved

containerapp_def = None
try:
containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name)
except:
pass
zhenqxuMSFT marked this conversation as resolved.
Show resolved Hide resolved

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
zhenqxuMSFT marked this conversation as resolved.
Show resolved Hide resolved

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:
zhenqxuMSFT marked this conversation as resolved.
Show resolved Hide resolved
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
zhenqxuMSFT marked this conversation as resolved.
Show resolved Hide resolved

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