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

containerapp move preview decorator to containerapp #6586

Merged
merged 4 commits into from
Aug 3, 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
1 change: 1 addition & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Upcoming
++++++
* 'az containerapp job start': update start execution payload format to exlude template property from API version 2023-05-01 onwards
* 'az containerapp service': add support for creation and deletion of MariaDB
* 'az containerapp create/list': support --environment-type parameter

0.3.36
++++++
Expand Down
158 changes: 158 additions & 0 deletions src/containerapp/azext_containerapp/_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
logger = get_logger(__name__)

CURRENT_API_VERSION = "2023-04-01-preview"
PREVIEW_API_VERSION = "2023-04-01-preview"
ARC_PREVIEW_API_VERSION = "2023-04-01-preview"
POLLING_TIMEOUT = 600 # how many seconds before exiting
POLLING_SECONDS = 2 # how many seconds between requests
POLLING_TIMEOUT_FOR_MANAGED_CERTIFICATE = 1500 # how many seconds before exiting
Expand Down Expand Up @@ -1509,3 +1511,159 @@ def get(cls, cmd, resource_group_name, container_app_name, auth_config_name):

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


# Clients for preview
class ContainerAppPreviewClient(ContainerAppClient):
api_version = PREVIEW_API_VERSION


class ManagedEnvironmentPreviewClient(ManagedEnvironmentClient):
api_version = PREVIEW_API_VERSION


class ConnectedEnvironmentClient():
api_version = ARC_PREVIEW_API_VERSION

@classmethod
def create(cls, cmd, resource_group_name, name, connected_environment_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/connectedEnvironments/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(connected_environment_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)
r = send_raw_request(cmd.cli_ctx, "GET", request_url)

return r.json()

@classmethod
def update(cls, cmd, resource_group_name, name, managed_environment_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/connectedEnvironments/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "PATCH", request_url, body=json.dumps(managed_environment_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 a connected environment")
else:
return response

return r.json()

@classmethod
def delete(cls, cmd, resource_group_name, 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/connectedEnvironments/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_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('Connected environment successfully deleted')
return

@classmethod
def show(cls, cmd, resource_group_name, 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/connectedEnvironments/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
name,
cls.api_version)

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

@classmethod
def list_by_subscription(cls, cmd, formatter=lambda x: x):
env_list = []

management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
sub_id = get_subscription_id(cmd.cli_ctx)
request_url = "{}/subscriptions/{}/providers/Microsoft.App/connectedEnvironments?api-version={}".format(
management_hostname.strip('/'),
sub_id,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "GET", request_url)
j = r.json()
for env in j["value"]:
formatted = formatter(env)
env_list.append(formatted)

while j.get("nextLink") is not None:
request_url = j["nextLink"]
r = send_raw_request(cmd.cli_ctx, "GET", request_url)
j = r.json()
for env in j["value"]:
formatted = formatter(env)
env_list.append(formatted)

return env_list

@classmethod
def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x):
env_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/connectedEnvironments?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "GET", request_url)
j = r.json()
for env in j["value"]:
formatted = formatter(env)
env_list.append(formatted)

while j.get("nextLink") is not None:
request_url = j["nextLink"]
r = send_raw_request(cmd.cli_ctx, "GET", request_url)
j = r.json()
for env in j["value"]:
formatted = formatter(env)
env_list.append(formatted)

return env_list
4 changes: 4 additions & 0 deletions src/containerapp/azext_containerapp/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
MANAGED_ENVIRONMENT_TYPE = "managed"
CONNECTED_ENVIRONMENT_TYPE = "connected"
MANAGED_ENVIRONMENT_RESOURCE_TYPE = "managedEnvironments"
CONNECTED_ENVIRONMENT_RESOURCE_TYPE = "connectedEnvironments"

MAXIMUM_SECRET_LENGTH = 20
MAXIMUM_CONTAINER_APP_NAME_LENGTH = 32
Expand Down
74 changes: 74 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -1695,3 +1695,77 @@
text: |
az containerapp patch interactive -g MyResourceGroup --environment MyContainerAppEnv --show-all
"""

# containerapp create for preview
helps['containerapp create'] = """
type: command
short-summary: Create a container app.
examples:
- name: Create a container app and retrieve its fully qualified domain name.
text: |
az containerapp create -n MyContainerapp -g MyResourceGroup \\
--image myregistry.azurecr.io/my-app:v1.0 --environment MyContainerappEnv \\
--ingress external --target-port 80 \\
--registry-server myregistry.azurecr.io --registry-username myregistry --registry-password $REGISTRY_PASSWORD \\
--query properties.configuration.ingress.fqdn
- name: Create a container app with resource requirements and replica count limits.
text: |
az containerapp create -n MyContainerapp -g MyResourceGroup \\
--image nginx --environment MyContainerappEnv \\
--cpu 0.5 --memory 1.0Gi \\
--min-replicas 4 --max-replicas 8
- name: Create a container app with secrets and environment variables.
text: |
az containerapp create -n MyContainerapp -g MyResourceGroup \\
--image my-app:v1.0 --environment MyContainerappEnv \\
--secrets mysecret=secretvalue1 anothersecret="secret value 2" \\
--env-vars GREETING="Hello, world" SECRETENV=secretref:anothersecret
- name: Create a container app using a YAML configuration. Example YAML configuration - https://aka.ms/azure-container-apps-yaml
text: |
az containerapp create -n MyContainerapp -g MyResourceGroup \\
--environment MyContainerappEnv \\
--yaml "path/to/yaml/file.yml"
- name: Create a container app with an http scale rule
text: |
az containerapp create -n myapp -g mygroup --environment myenv --image nginx \\
--scale-rule-name my-http-rule \\
--scale-rule-http-concurrency 50
- name: Create a container app with a custom scale rule
text: |
az containerapp create -n MyContainerapp -g MyResourceGroup \\
--image my-queue-processor --environment MyContainerappEnv \\
--min-replicas 4 --max-replicas 8 \\
--scale-rule-name queue-based-autoscaling \\
--scale-rule-type azure-queue \\
--scale-rule-metadata "accountName=mystorageaccountname" \\
"cloud=AzurePublicCloud" \\
"queueLength": "5" "queueName": "foo" \\
--scale-rule-auth "connection=my-connection-string-secret-name"
- name: Create a container app with secrets and mounts them in a volume.
text: |
az containerapp create -n MyContainerapp -g MyResourceGroup \\
--image my-app:v1.0 --environment MyContainerappEnv \\
--secrets mysecret=secretvalue1 anothersecret="secret value 2" \\
--secret-volume-mount "mnt/secrets"
- name: Create a container app hosted on a Connected Environment.
text: |
az containerapp create -n MyContainerapp -g MyResourceGroup \\
--image my-app:v1.0 --environment MyContainerappConnectedEnv \\
--environment-type connected
Greedygre marked this conversation as resolved.
Show resolved Hide resolved
"""

# containerapp list for preview
helps['containerapp list'] = """
type: command
short-summary: List container apps.
examples:
- name: List container apps in the current subscription.
text: |
az containerapp list
- name: List container apps by resource group.
text: |
az containerapp list -g MyResourceGroup
- name: List container apps by environment type.
text: |
az containerapp list --environment-type connected
"""
7 changes: 6 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_cors_max_age)
validate_storage_name_or_id, validate_cors_max_age, validate_env_name_or_id)
from ._constants import UNAUTHENTICATED_CLIENT_ACTION, FORWARD_PROXY_CONVENTION, MAXIMUM_CONTAINER_APP_NAME_LENGTH, LOG_TYPE_CONSOLE, LOG_TYPE_SYSTEM


Expand Down Expand Up @@ -508,3 +508,8 @@ def load_arguments(self, _):

with self.argument_context('containerapp job identity remove') as c:
c.argument('user_assigned', nargs='*', help="Space-separated user identities. If no user identities are specified, all user identities will be removed.")

# params for preview
with self.argument_context('containerapp') as c:
c.argument('managed_env', validator=validate_env_name_or_id, options_list=['--environment'], help="Name or resource ID of the container app's environment.")
c.argument('environment_type', arg_type=get_enum_type(["managed", "connected"]), help="Type of environment.", is_preview=True)
48 changes: 46 additions & 2 deletions src/containerapp/azext_containerapp/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
from ._clients import ContainerAppClient
from ._ssh_utils import ping_container_app
from ._utils import safe_get, is_registry_msi_system
from ._constants import ACR_IMAGE_SUFFIX, LOG_TYPE_SYSTEM

from ._constants import ACR_IMAGE_SUFFIX, LOG_TYPE_SYSTEM, CONNECTED_ENVIRONMENT_RESOURCE_TYPE, \
CONNECTED_ENVIRONMENT_TYPE, MANAGED_ENVIRONMENT_RESOURCE_TYPE, MANAGED_ENVIRONMENT_TYPE, CONTAINER_APPS_RP

logger = get_logger(__name__)

Expand Down Expand Up @@ -218,3 +218,47 @@ def validate_cors_max_age(cmd, namespace):
raise InvalidArgumentValueError("max-age must be a positive integer.")
except ValueError:
raise InvalidArgumentValueError("max-age must be an integer.")


# validate for preview
def validate_env_name_or_id(cmd, namespace):
from azure.cli.core.commands.client_factory import get_subscription_id
from msrestazure.tools import is_valid_resource_id, resource_id, parse_resource_id

if not namespace.managed_env:
return

# Set environment type
environment_type = None

if namespace.__dict__.get("environment_type"):
environment_type = namespace.environment_type

if is_valid_resource_id(namespace.managed_env):
env_dict = parse_resource_id(namespace.managed_env)
resource_type = env_dict.get("resource_type")
if resource_type:
if CONNECTED_ENVIRONMENT_RESOURCE_TYPE.lower() == resource_type.lower():
environment_type = CONNECTED_ENVIRONMENT_TYPE
if MANAGED_ENVIRONMENT_RESOURCE_TYPE.lower() == resource_type.lower():
environment_type = MANAGED_ENVIRONMENT_TYPE

# Validate resource id / format resource id
if environment_type == CONNECTED_ENVIRONMENT_TYPE:
if not is_valid_resource_id(namespace.managed_env):
namespace.managed_env = resource_id(
subscription=get_subscription_id(cmd.cli_ctx),
resource_group=namespace.resource_group_name,
namespace=CONTAINER_APPS_RP,
type=CONNECTED_ENVIRONMENT_RESOURCE_TYPE,
name=namespace.managed_env
)
else:
if not is_valid_resource_id(namespace.managed_env):
namespace.managed_env = resource_id(
subscription=get_subscription_id(cmd.cli_ctx),
resource_group=namespace.resource_group_name,
namespace=CONTAINER_APPS_RP,
type=MANAGED_ENVIRONMENT_RESOURCE_TYPE,
name=namespace.managed_env
)
Loading