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

Introduce App Resiliency & Dapr Component Resiliency #6967

Merged
merged 4 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -5,6 +5,8 @@ Release History
upcoming
++++++
* 'az containerapp add-on': support for az containerapp add-on commands; deprecation of az containerapp service commands
* 'az containerapp env dapr-component resiliency': Add Dapr Component Resiliency commands
* 'az containerapp resiliency': Add Container App Resiliency commands
* 'az containerapp env create': Support --enable-dedicated-gpu
* 'az containerapp job create': fix problem of parsing parameters minExecutions and maxExecutions from --yaml
* 'az containerapp env dapr-component init': support initializing Dapr components and dev services for an environment
Expand Down
229 changes: 229 additions & 0 deletions src/containerapp/azext_containerapp/_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,235 @@ class ContainerAppsJobPreviewClient(ContainerAppsJobClient):
api_version = PREVIEW_API_VERSION


class ContainerAppsResiliencyPreviewClient():
api_version = PREVIEW_API_VERSION

@classmethod
def create_or_update(cls, cmd, resource_group_name, name, container_app_name, container_app_resiliency_envelope, no_wait=False):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/resiliencyPolicies/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
container_app_name,
name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(container_app_resiliency_envelope))

if no_wait:
return r.json()
elif r.status_code == 201:
operation_url = r.headers.get(HEADER_AZURE_ASYNC_OPERATION)
poll_status(cmd, operation_url)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/resiliencyPolicies/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
container_app_name,
name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "GET", request_url)

return r.json()

@classmethod
def update(cls, cmd, resource_group_name, name, container_app_name, container_app_resiliency_envelope, no_wait=False):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager

sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/resiliencyPolicies/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
container_app_name,
name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "PATCH", request_url, body=json.dumps(container_app_resiliency_envelope))

if no_wait:
return
elif r.status_code == 202:
operation_url = r.headers.get(HEADER_LOCATION)
response = poll_results(cmd, operation_url)
if response is None:
raise ResourceNotFoundError("Could not find the app resiliency policy")
else:
return response

return r.json()

@classmethod
def delete(cls, cmd, resource_group_name, name, container_app_name, no_wait=False):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/resiliencyPolicies/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
container_app_name,
name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "DELETE", request_url)

if no_wait:
return # API doesn't return JSON (it returns no content)
elif r.status_code in [200, 201, 202, 204]:
if r.status_code == 202:
operation_url = r.headers.get(HEADER_LOCATION)
poll_results(cmd, operation_url)
logger.warning('App Resiliency Policy successfully deleted')

@classmethod
def show(cls, cmd, resource_group_name, name, container_app_name):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/resiliencyPolicies/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
container_app_name,
name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "GET", request_url)

return r.json()

@classmethod
def list(cls, cmd, resource_group_name, container_app_name):
policy_list = []
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/resiliencyPolicies?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
container_app_name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "GET", request_url)
r = r.json()

for policy in r["value"]:
policy_list.append(policy)

return policy_list


class DaprComponentResiliencyPreviewClient():
api_version = PREVIEW_API_VERSION

@classmethod
def create_or_update(cls, cmd, name, resource_group_name, dapr_component_name, environment_name, component_resiliency_envelope, no_wait=False):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}/resiliencyPolicies/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
environment_name,
dapr_component_name,
name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(component_resiliency_envelope))

if no_wait:
return r.json()
elif r.status_code == 201:
operation_url = r.headers.get(HEADER_AZURE_ASYNC_OPERATION)
poll_status(cmd, operation_url)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}/resiliencyPolicies/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
environment_name,
dapr_component_name,
name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "GET", request_url)

return r.json()

@classmethod
def delete(cls, cmd, name, resource_group_name, dapr_component_name, environment_name, no_wait=False):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}/resiliencyPolicies/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
environment_name,
dapr_component_name,
name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "DELETE", request_url)

if no_wait:
return # API doesn't return JSON (it returns no content)
elif r.status_code in [200, 201, 202, 204]:
if r.status_code == 202:
operation_url = r.headers.get(HEADER_LOCATION)
poll_results(cmd, operation_url)
logger.warning('Dapr Component Resiliency Policy successfully deleted')

@classmethod
def show(cls, cmd, name, resource_group_name, dapr_component_name, environment_name):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}/resiliencyPolicies/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
environment_name,
dapr_component_name,
name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "GET", request_url)

return r.json()

@classmethod
def list(cls, cmd, resource_group_name, dapr_component_name, environment_name):
policy_list = []
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}/resiliencyPolicies?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
environment_name,
dapr_component_name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "GET", request_url)
r = r.json()

for policy in r["value"]:
policy_list.append(policy)

return policy_list


class SubscriptionPreviewClient():
api_version = PREVIEW_API_VERSION

Expand Down
19 changes: 19 additions & 0 deletions src/containerapp/azext_containerapp/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@

MAXIMUM_SECRET_LENGTH = 20
MAXIMUM_CONTAINER_APP_NAME_LENGTH = 32
MAXIMUM_APP_RESILIENCY_NAME_LENGTH = 30
MAXIMUM_COMPONENT_RESILIENCY_NAME_LENGTH = 30

DEFAULT_HTTP_RETRY_MAX = 3
DEFAULT_HTTP_RETRY_DELAY_IN_MILLISECONDS = 1000
DEFAULT_HTTP_RETRY_INTERVAL_IN_MILLISECONDS = 10000
DEFAULT_HTTP_RETRY_ERRORS = ['5xx']

DEFAULT_RESPONSE_TIMEOUT = 60
DEFAULT_CONNECTION_TIMEOUT = 5
DEFAULT_CONSECUTIVE_ERRORS = 5
DEFAULT_INTERVAL = 10
DEFAULT_MAX_EJECTION = 100
DEFAULT_HTTP1_MAX_PENDING_REQ = 1024
DEFAULT_HTTP2_MAX_REQ = 1024

DEFAULT_COMPONENT_HTTP_RETRY_MAX = 3
DEFAULT_COMPONENT_HTTP_RETRY_BACKOFF_INITIAL_DELAY = 1000
DEFAULT_COMPONENT_HTTP_RETRY_BACKOFF_MAX_DELAY = 10000

SHORT_POLLING_INTERVAL_SECS = 3
LONG_POLLING_INTERVAL_SECS = 10
Expand Down
45 changes: 45 additions & 0 deletions src/containerapp/azext_containerapp/_decorator_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,48 @@ def process_loaded_yaml(yaml_containerapp):
del yaml_containerapp['properties']['managedEnvironmentId']

return yaml_containerapp


def process_containerapp_resiliency_yaml(containerapp_resiliency):

if type(containerapp_resiliency) != dict: # pylint: disable=unidiomatic-typecheck
raise ValidationError('Invalid YAML provided. Please provide a valid container app resiliency YAML spec.')
if 'additionalProperties' in containerapp_resiliency and not containerapp_resiliency['additionalProperties']:
raise ValidationError('Invalid YAML provided. Please provide a valid containerapp resiliency YAML spec.')
if not containerapp_resiliency.get('properties'):
containerapp_resiliency['properties'] = {}

nested_properties = ["timeoutPolicy",
"httpRetryPolicy",
"tcpRetryPolicy",
"circuitBreakerPolicy",
"tcpConnectionPool",
"httpConnectionPool"]
for nested_property in nested_properties:
# Fix this and remove additionalProperties after flattening is avoided
tmp = containerapp_resiliency['additionalProperties'].get(nested_property)
if nested_property in containerapp_resiliency:
containerapp_resiliency['properties'][nested_property] = tmp
del containerapp_resiliency[nested_property]

return containerapp_resiliency


def process_dapr_component_resiliency_yaml(dapr_component_resiliency):

if type(dapr_component_resiliency) != dict: # pylint: disable=unidiomatic-typecheck
raise ValidationError('Invalid YAML provided. Please provide a valid dapr component resiliency YAML spec.')
if 'additionalProperties' in dapr_component_resiliency and not dapr_component_resiliency['additionalProperties']:
raise ValidationError('Invalid YAML provided. Please provide a valid dapr component resiliency YAML spec.')
if not dapr_component_resiliency.get('properties'):
dapr_component_resiliency['properties'] = {}

nested_properties = ["inboundPolicy",
"outboundPolicy"]
for nested_property in nested_properties:
tmp = dapr_component_resiliency['additionalProperties'].get(nested_property)
if nested_property in dapr_component_resiliency:
dapr_component_resiliency['properties'][nested_property] = tmp
del dapr_component_resiliency[nested_property]

return dapr_component_resiliency
Loading