From c039d95b7a1eedceceedeeb4db4584887700902e Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Mon, 14 Mar 2022 18:53:35 -0400 Subject: [PATCH] Dapr Commands (#23) * Added ingress subgroup. * Added help for ingress. * Fixed ingress traffic help. * Added registry commands. * Updated registry remove util to clear secrets if none remaining. Added warning when updating existing registry. Added registry help. * Changed registry delete to remove. * Added error message if user tries to remove non assigned registry. * Changed registry add back to registry set. * Added secret subgroup commands. * Removed yaml support from secret set. * Changed secret add to secret set. Updated consistency between secret set and secret delete. Added secret help. Require at least one secret passed with --secrets for secret commands. * Changed param name for secret delete from --secrets to --secret-names. Updated help. * Changed registry remove to registry delete. * Fixed bug in registry delete. * Added revision mode set and revision copy. * Added dapr enable and dapr disable. Need to test more. * Added list, show, set dapr component. Added dapr enable, disable. * Added delete dapr delete. * Added helps and param text. * Changed dapr delete to dapr remove to match with dapr set. * Commented out managed identity for whl file. * Uncommented. Co-authored-by: Haroon Feisal --- .../azext_containerapp/_clients.py | 119 +++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 78 +++++++++++ .../azext_containerapp/_models.py | 20 ++- .../azext_containerapp/_params.py | 7 + src/containerapp/azext_containerapp/_utils.py | 17 +++ .../azext_containerapp/commands.py | 9 ++ src/containerapp/azext_containerapp/custom.py | 122 +++++++++++++++++- 7 files changed, 369 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 8184e6d86e2..5a1e597523e 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -603,3 +603,122 @@ def delete(cls, cmd, resource_group_name, name, headers, no_wait=False): pass logger.warning('Containerapp github action successfully deleted') return + +class DaprComponentClient(): + @classmethod + def create_or_update(cls, cmd, resource_group_name, environment_name, name, dapr_component_envelope, no_wait=False): + #create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.App/managedEnvironments/{environmentName}/daprComponents/{name}'} + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(dapr_component_envelope)) + + if no_wait: + return r.json() + elif r.status_code == 201: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_name, + name, + api_version) + return poll(cmd, request_url, "inprogress") + + return r.json() + + @classmethod + def delete(cls, cmd, resource_group_name, environment_name, name, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_name, + name, + 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]: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_name, + name, + api_version) + + if r.status_code == 202: + from azure.cli.core.azclierror import ResourceNotFoundError + try: + poll(cmd, request_url, "cancelled") + except ResourceNotFoundError: + pass + logger.warning('Dapr component successfully deleted') + return + + @classmethod + def show(cls, cmd, resource_group_name, environment_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + return r.json() + + @classmethod + def list(cls, cmd, resource_group_name, environment_name, formatter=lambda x: x): + app_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + request_url = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents?api-version={}".format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + j = r.json() + for app in j["value"]: + formatted = formatter(app) + app_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 app in j["value"]: + formatted = formatter(app) + app_list.append(formatted) + + return app_list + diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 724335e8711..3a91fb32aca 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -164,6 +164,24 @@ --from-revision PreviousRevisionName --cpu 0.75 --memory 1.5Gi """ +helps['containerapp revision mode set'] = """ + type: command + short-summary: Set the revision mode of a Containerapp. + examples: + - name: Set the revision mode of a Containerapp. + text: | + az containerapp revision set --mode Single -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp revision copy'] = """ + type: command + short-summary: Create a revision based on a previous revision. + examples: + - name: Create a revision based on a previous revision. + text: | + az containerapp revision copy -n MyContainerapp -g MyResourceGroup --cpu 0.75 --memory 1.5Gi +""" + # Environment Commands helps['containerapp env'] = """ type: group @@ -456,4 +474,64 @@ examples: - name: Show the GitHub Actions configuration on a Containerapp. text: az containerapp github-action show -g MyResourceGroup -n MyContainerapp +""" + +# Dapr Commands +helps['containerapp dapr'] = """ + type: group + short-summary: Commands to manage Containerapp dapr. +""" + +helps['containerapp dapr enable'] = """ + type: command + short-summary: Enable dapr for a Containerapp. + examples: + - name: Enable dapr for a Containerapp. + text: | + az containerapp dapr enable -n MyContainerapp -g MyResourceGroup --dapr-app-id my-app-id --dapr-app-port 8080 +""" + +helps['containerapp dapr disable'] = """ + type: command + short-summary: Disable dapr for a Containerapp. + examples: + - name: Disable dapr for a Containerapp. + text: | + az containerapp dapr disable -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp dapr list'] = """ + type: command + short-summary: List dapr components for a Containerapp environment. + examples: + - name: List dapr components for a Containerapp environment. + text: | + az containerapp dapr list -g MyResourceGroup --environment-name MyEnvironment +""" + +helps['containerapp dapr show'] = """ + type: command + short-summary: Show the details of a dapr component. + examples: + - name: Show the details of a dapr component. + text: | + az containerapp dapr show -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment +""" + +helps['containerapp dapr set'] = """ + type: command + short-summary: Create or update a dapr component. + examples: + - name: Create a dapr component. + text: | + az containerapp dapr set -g MyResourceGroup --environment-name MyEnv --yaml MyYAMLPath --name MyDaprName +""" + +helps['containerapp dapr remove'] = """ + type: command + short-summary: Remove a dapr componenet from a Containerapp environment. + examples: + - name: Remove a dapr componenet from a Containerapp environment. + text: | + az containerapp dapr delete -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment """ \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 6440c677635..14d8e1a8fb3 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -181,6 +181,24 @@ "tags": None } +DaprComponent = { + "properties": { + "componentType": None, #String + "version": None, + "ignoreErrors": None, + "initTimeout": None, + "secrets": None, + "metadata": None, + "scopes": None + } +} + +DaprMetadata = { + "key": None, #str + "value": None, #str + "secret_ref": None #str +} + SourceControl = { "properties": { "repoUrl": None, @@ -211,4 +229,4 @@ "clientSecret": None, # str "tenantId": None, #str "subscriptionId": None #str -} \ No newline at end of file +} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 8435659f4d0..ab1ebe848bd 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -149,3 +149,10 @@ def load_arguments(self, _): with self.argument_context('containerapp secret delete') as c: c.argument('secret_names', nargs='+', help="A list of secret(s) for the container app. Space-separated secret values names.") + + with self.argument_context('containerapp dapr') as c: + c.argument('dapr_app_id', help="The dapr app id.") + c.argument('dapr_app_port', help="The port of your app.") + c.argument('dapr_app_protocol', help="Tells Dapr which protocol your application is using. Allowed values: grpc, http.") + c.argument('dapr_component_name', help="The dapr component name.") + c.argument('environment_name', help="The dapr component environment name.") diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 54994a71578..14816a07915 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -414,6 +414,23 @@ def _remove_readonly_attributes(containerapp_def): elif unneeded_property in containerapp_def['properties']: del containerapp_def['properties'][unneeded_property] +def _remove_dapr_readonly_attributes(daprcomponent_def): + unneeded_properties = [ + "id", + "name", + "type", + "systemData", + "provisioningState", + "latestRevisionName", + "latestRevisionFqdn", + "customDomainVerificationId", + "outboundIpAddresses", + "fqdn" + ] + + for unneeded_property in unneeded_properties: + if unneeded_property in daprcomponent_def: + del daprcomponent_def[unneeded_property] def update_nested_dictionary(orig_dict, new_dict): # Recursively update a nested dictionary. If the value is a list, replace the old list with new list diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 9a83db9df7e..95e165d7e63 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -99,3 +99,12 @@ def load_command_table(self, _): g.custom_command('show', 'show_secret') g.custom_command('delete', 'delete_secrets', exception_handler=ex_handler_factory()) g.custom_command('set', 'set_secrets', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp dapr') as g: + g.custom_command('enable', 'enable_dapr', exception_handler=ex_handler_factory()) + g.custom_command('disable', 'disable_dapr', exception_handler=ex_handler_factory()) + g.custom_command('list', 'list_dapr_components') + g.custom_command('show', 'show_dapr_component') + g.custom_command('set', 'create_or_update_dapr_component') + g.custom_command('remove', 'remove_dapr_component') + diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index db5bdb00db2..13d42b6be6b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -17,7 +17,7 @@ from urllib.parse import urlparse from ._client_factory import handle_raw_exception -from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient +from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient from ._sdk_models import * from ._github_oauth import get_github_access_token from ._models import ( @@ -45,7 +45,7 @@ _object_to_dict, _add_or_update_secrets, _remove_additional_attributes, _remove_readonly_attributes, _add_or_update_env_vars, _add_or_update_tags, update_nested_dictionary, _update_traffic_Weights, _get_app_from_revision, raise_missing_token_suggestion, _infer_acr_credentials, _remove_registry_secret, _remove_secret, - _ensure_identity_resource_id) + _ensure_identity_resource_id, _remove_dapr_readonly_attributes) logger = get_logger(__name__) @@ -1949,4 +1949,122 @@ def set_secrets(cmd, name, resource_group_name, secrets, except Exception as e: handle_raw_exception(e) +def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port=None, dapr_app_protocol=None, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + if 'dapr' not in containerapp_def['properties']: + containerapp_def['properties']['dapr'] = {} + + if dapr_app_id: + containerapp_def['properties']['dapr']['dapr_app_id'] = dapr_app_id + + if dapr_app_port: + containerapp_def['properties']['dapr']['dapr_app_port'] = dapr_app_port + + if dapr_app_protocol: + containerapp_def['properties']['dapr']['dapr_app_protocol'] = dapr_app_protocol + + containerapp_def['properties']['dapr']['enabled'] = True + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + return r["properties"]['dapr'] + except Exception as e: + handle_raw_exception(e) + +def disable_dapr(cmd, name, resource_group_name, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + containerapp_def['properties']['dapr']['enabled'] = False + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + return r["properties"]['dapr'] + except Exception as e: + handle_raw_exception(e) + +def list_dapr_components(cmd, resource_group_name, environment_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + return DaprComponentClient.list(cmd, resource_group_name, environment_name) + +def show_dapr_component(cmd, resource_group_name, dapr_component_name, environment_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + return DaprComponentClient.show(cmd, resource_group_name, environment_name, name=dapr_component_name) + +def create_or_update_dapr_component(cmd, resource_group_name, environment_name, dapr_component_name, yaml): + _validate_subscription_registered(cmd, "Microsoft.App") + + yaml_containerapp = load_yaml_file(yaml) + if type(yaml_containerapp) != dict: + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + # Deserialize the yaml into a DaprComponent object. Need this since we're not using SDK + daprcomponent_def = None + try: + deserializer = create_deserializer() + + daprcomponent_def = deserializer('DaprComponent', yaml_containerapp) + except DeserializationError as ex: + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + #daprcomponent_def = _object_to_dict(daprcomponent_def) + daprcomponent_def = _convert_object_from_snake_to_camel_case(_object_to_dict(daprcomponent_def)) + + # Remove "additionalProperties" and read-only attributes that are introduced in the deserialization. Need this since we're not using SDK + _remove_additional_attributes(daprcomponent_def) + _remove_dapr_readonly_attributes(daprcomponent_def) + + if not daprcomponent_def["ignoreErrors"]: + daprcomponent_def["ignoreErrors"] = False + + dapr_component_envelope = {} + + dapr_component_envelope["properties"] = daprcomponent_def + + try: + r = DaprComponentClient.create_or_update(cmd, resource_group_name=resource_group_name, environment_name=environment_name, dapr_component_envelope=dapr_component_envelope, name=dapr_component_name) + return r + except Exception as e: + handle_raw_exception(e) + +def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environment_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + DaprComponentClient.show(cmd, resource_group_name, environment_name, name=dapr_component_name) + except: + raise CLIError("Dapr component not found.") + + try: + r = DaprComponentClient.delete(cmd, resource_group_name, environment_name, name=dapr_component_name) + logger.warning("Dapr componenet successfully deleted.") + return r + except Exception as e: + handle_raw_exception(e)