From 0ca16c34fbf7664591825d68ddd14901ce425bea Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 11 May 2022 15:13:14 -0400 Subject: [PATCH 1/8] Finished label and revision work. --- .../azext_containerapp/_clients.py | 2 +- .../azext_containerapp/_params.py | 3 +- src/containerapp/azext_containerapp/_utils.py | 86 +++++++++++--- .../azext_containerapp/commands.py | 4 + src/containerapp/azext_containerapp/custom.py | 110 +++++++++++++++++- 5 files changed, 183 insertions(+), 22 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index d217e12be5e..e60ca71dbd8 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -167,7 +167,7 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = PREVIEW_API_VERSION + api_version = STABLE_API_VERSION sub_id = get_subscription_id(cmd.cli_ctx) url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}?api-version={}" request_url = url_fmt.format( diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index fa33873602f..97d58b60493 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -186,7 +186,8 @@ def load_arguments(self, _): c.argument('target_port', type=int, validator=validate_target_port, help="The application port used for ingress traffic.") with self.argument_context('containerapp ingress traffic') as c: - c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the container app. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") + c.argument('revision_weights', nargs='*', options_list=['--revision-weight'], help="A list of revision weight(s) for the container app. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") + c.argument('label_weights', nargs='*', options_list=['--label-weight'], help="A list of label weight(s) for the container app. Space-separated values in 'label_name=weight' format.") 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' format (where 'key' cannot be longer than 20 characters).") diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index eb44906c87e..1f1a4613348 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -829,36 +829,94 @@ def update_nested_dictionary(orig_dict, new_dict): return orig_dict -def _is_valid_weight(weight): +def _validate_weight(weight): try: n = int(weight) if 0 <= n <= 100: return True - return False - except ValueError: - return False + raise ValidationError('Traffic weights must be integers between 0 and 100') + except ValueError as ex: + raise ValidationError('Traffic weights must be integers between 0 and 100') from ex -def _update_traffic_weights(containerapp_def, list_weights): - if "traffic" not in containerapp_def["properties"]["configuration"]["ingress"] or list_weights and len(list_weights): +def _update_revision_weights(containerapp_def, list_weights): + if "traffic" not in containerapp_def["properties"]["configuration"]["ingress"]: containerapp_def["properties"]["configuration"]["ingress"]["traffic"] = [] + if not list_weights: + return + for new_weight in list_weights: key_val = new_weight.split('=', 1) + if len(key_val) != 2: + raise ValidationError('Traffic weights must be in format \"= = ...\"') + revision = key_val[0] + weight = key_val[1] + _validate_weight(weight) is_existing = False + for existing_weight in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]: + if "latestRevision" in existing_weight and existing_weight["latestRevision"]: + if revision.lower() == "latest": + existing_weight["weight"] = weight + is_existing = True + break + elif "revisionName" in existing_weight and existing_weight["revisionName"].lower() == revision.lower(): + existing_weight["weight"] = weight + is_existing = True + break + if not is_existing: + containerapp_def["properties"]["configuration"]["ingress"]["traffic"].append({ + "revisionName": revision if revision.lower() != "latest" else None, + "weight": int(weight), + "latestRevision": revision.lower() == "latest" + }) + + +def _append_label_weights(containerapp_def, label_weights, revision_weights): + if "traffic" not in containerapp_def["properties"]["configuration"]["ingress"]: + containerapp_def["properties"]["configuration"]["ingress"]["traffic"] = [] + + if not label_weights: + return + + revision_weight_names = [w.split('=', 1)[0].lower() for w in revision_weights] # this is to check if we already have that revision weight passed + for new_weight in label_weights: + key_val = new_weight.split('=', 1) if len(key_val) != 2: - raise ValidationError('Traffic weights must be in format \"=weight = ...\"') + raise ValidationError('Traffic weights must be in format \"= = ...\"') + label = key_val[0] + weight = key_val[1] + _validate_weight(weight) + is_existing = False - if not _is_valid_weight(key_val[1]): - raise ValidationError('Traffic weights must be integers between 0 and 100') + for existing_weight in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]: + if "label" in existing_weight and existing_weight["label"].lower() == label.lower(): + if "revisionName" in existing_weight and existing_weight["revisionName"] and existing_weight["revisionName"].lower() in revision_weight_names: + logger.warning("Already passed value for revision {}, will not overwrite with {}.".format(existing_weight["revisionName"], new_weight)) + is_existing = True + break + revision_weights.append("{}={}".format(existing_weight["revisionName"] if "revisionName" in existing_weight and existing_weight["revisionName"] else "latest", weight)) + is_existing = True + break if not is_existing: - containerapp_def["properties"]["configuration"]["ingress"]["traffic"].append({ - "revisionName": key_val[0] if key_val[0].lower() != "latest" else None, - "weight": int(key_val[1]), - "latestRevision": key_val[0].lower() == "latest" - }) + raise ValidationError(f"No label {label} assigned to any traffic weight.") + + +def _update_weights(containerapp_def, revision_weights, label_weights): + # get name list of the ones we have changed, validation can be dropped should be caught above + revision_weight_names = [w.split('=', 1)[0] for w in revision_weights] + + print(revision_weight_names) + # sum the others + # math the others + + +def _validate_traffic_sum(revision_weights): + weight_sum = sum([int(w.split('=', 1)[1]) for w in revision_weights if len(w.split('=', 1)) == 2 and _validate_weight(w.split('=', 1)[1])]) + if weight_sum > 100: + raise ValidationError("Traffic sums may not exceed 100.") def _get_app_from_revision(revision): diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 158616ca0b2..62c01d19475 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -92,6 +92,10 @@ def load_command_table(self, _): g.custom_command('copy', 'copy_revision', exception_handler=ex_handler_factory()) g.custom_command('set-mode', 'set_revision_mode', exception_handler=ex_handler_factory()) + with self.command_group('containerapp revision label') as g: + g.custom_command('add', 'add_revision_label') + g.custom_command('remove', 'remove_revision_label') + with self.command_group('containerapp ingress') as g: g.custom_command('enable', 'enable_ingress', exception_handler=ex_handler_factory()) g.custom_command('disable', 'disable_ingress', exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 4010086bbe8..2d0f3415e17 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -50,11 +50,11 @@ parse_secret_flags, store_as_secret_and_return_secret_ref, parse_env_var_flags, _generate_log_analytics_if_not_provided, _get_existing_secrets, _convert_object_from_snake_to_camel_case, _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, + _add_or_update_env_vars, _add_or_update_tags, update_nested_dictionary, _update_revision_weights, _append_label_weights, _get_app_from_revision, raise_missing_token_suggestion, _infer_acr_credentials, _remove_registry_secret, _remove_secret, - _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _remove_env_vars, + _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _remove_env_vars, _validate_traffic_sum, _update_revision_env_secretrefs, _get_acr_cred, safe_get, await_github_action, repo_url_to_name, - validate_container_app_name) + validate_container_app_name, _update_weights) from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING) @@ -1345,6 +1345,96 @@ def set_revision_mode(cmd, resource_group_name, name, mode, no_wait=False): handle_raw_exception(e) +def add_revision_label(cmd, resource_group_name, name, revision, label, 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + if "ingress" not in containerapp_def['properties']['configuration'] and "traffic" not in containerapp_def['properties']['configuration']['ingress']: + raise ValidationError("Ingress and traffic weights are required to set labels.") + + traffic_weight = containerapp_def['properties']['configuration']['ingress']['traffic'] + + label_added = False + for weight in traffic_weight: + if "latestRevision" in weight: + if revision.lower() == "latest" and weight["latestRevision"]: + label_added = True + weight["label"] = label + break + else: + if revision.lower() == weight["revisionName"].lower(): + label_added = True + weight["label"] = label + break + + if not label_added: + raise ValidationError("Please specify a revision name with an associated traffic weight.") + + containerapp_patch_def = {} + containerapp_patch_def['properties'] = {} + containerapp_patch_def['properties']['configuration'] = {} + containerapp_patch_def['properties']['configuration']['ingress'] = {} + + containerapp_patch_def['properties']['configuration']['ingress']['traffic'] = traffic_weight + + try: + r = ContainerAppClient.update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_patch_def, no_wait=no_wait) + return r['properties']['configuration']['ingress']['traffic'] + except Exception as e: + handle_raw_exception(e) + + +def remove_revision_label(cmd, resource_group_name, name, label, 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + if "ingress" not in containerapp_def['properties']['configuration'] and "traffic" not in containerapp_def['properties']['configuration']['ingress']: + raise ValidationError("Ingress and traffic weights are required to set labels.") + + traffic_weight = containerapp_def['properties']['configuration']['ingress']['traffic'] + + label_removed = False + for weight in traffic_weight: + if "label" in weight and weight["label"].lower() == label.lower(): + label_removed = True + weight["label"] = None + break + + if not label_removed: + raise ValidationError("Please specify a label name with an associated traffic weight.") + + containerapp_patch_def = {} + containerapp_patch_def['properties'] = {} + containerapp_patch_def['properties']['configuration'] = {} + containerapp_patch_def['properties']['configuration']['ingress'] = {} + + containerapp_patch_def['properties']['configuration']['ingress']['traffic'] = traffic_weight + + try: + r = ContainerAppClient.update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_patch_def, no_wait=no_wait) + return r['properties']['configuration']['ingress']['traffic'] + except Exception as e: + handle_raw_exception(e) + + def show_ingress(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -1428,8 +1518,10 @@ def disable_ingress(cmd, name, resource_group_name, no_wait=False): handle_raw_exception(e) -def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait=False): +def set_ingress_traffic(cmd, name, resource_group_name, label_weights=None, revision_weights=None, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") + if not label_weights and not revision_weights: + raise ValidationError("Must specify either --label-weight or --revision-weight.") containerapp_def = None try: @@ -1445,8 +1537,14 @@ def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait except Exception as e: raise ValidationError("Ingress must be enabled to set ingress traffic. Try running `az containerapp ingress -h` for more info.") from e - if traffic_weights is not None: - _update_traffic_weights(containerapp_def, traffic_weights) + + _append_label_weights(containerapp_def, label_weights, revision_weights) + _validate_traffic_sum(revision_weights) + + if revision_weights is not None: + _update_revision_weights(containerapp_def, revision_weights) + + _update_weights(containerapp_def, revision_weights, label_weights) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) From 9b73856d779a54353a6429056acaf60876dfe8b0 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 11 May 2022 16:04:31 -0400 Subject: [PATCH 2/8] Finished dynamic weight update work. --- src/containerapp/azext_containerapp/_utils.py | 37 +++++++++++++++---- src/containerapp/azext_containerapp/custom.py | 14 +++++-- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 1f1a4613348..6be1c9a77ee 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -840,6 +840,7 @@ def _validate_weight(weight): def _update_revision_weights(containerapp_def, list_weights): + old_weight_sum = 0 if "traffic" not in containerapp_def["properties"]["configuration"]["ingress"]: containerapp_def["properties"]["configuration"]["ingress"]["traffic"] = [] @@ -858,10 +859,12 @@ def _update_revision_weights(containerapp_def, list_weights): for existing_weight in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]: if "latestRevision" in existing_weight and existing_weight["latestRevision"]: if revision.lower() == "latest": + old_weight_sum += existing_weight["weight"] existing_weight["weight"] = weight is_existing = True break elif "revisionName" in existing_weight and existing_weight["revisionName"].lower() == revision.lower(): + old_weight_sum += existing_weight["weight"] existing_weight["weight"] = weight is_existing = True break @@ -871,6 +874,7 @@ def _update_revision_weights(containerapp_def, list_weights): "weight": int(weight), "latestRevision": revision.lower() == "latest" }) + return old_weight_sum def _append_label_weights(containerapp_def, label_weights, revision_weights): @@ -904,19 +908,36 @@ def _append_label_weights(containerapp_def, label_weights, revision_weights): raise ValidationError(f"No label {label} assigned to any traffic weight.") -def _update_weights(containerapp_def, revision_weights, label_weights): - # get name list of the ones we have changed, validation can be dropped should be caught above - revision_weight_names = [w.split('=', 1)[0] for w in revision_weights] - - print(revision_weight_names) - # sum the others - # math the others +def _update_weights(containerapp_def, revision_weights, old_weight_sum): + + new_weight_sum = sum([int(w.split('=', 1)[1]) for w in revision_weights]) + revision_weight_names = [w.split('=', 1)[0].lower() for w in revision_weights] + existing_weight_sum = sum([int(w["weight"]) for w in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]]) - new_weight_sum + round_up = True + scale_factor = (old_weight_sum-new_weight_sum)/existing_weight_sum + 1 + + for existing_weight in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]: + if "latestRevision" in existing_weight and existing_weight["latestRevision"]: + if "latest" not in revision_weight_names: + print(str(scale_factor*existing_weight["weight"])) + existing_weight["weight"], round_up = round(scale_factor*existing_weight["weight"], round_up) + elif "revisionName" in existing_weight and existing_weight["revisionName"].lower() not in revision_weight_names: + print(str(scale_factor*existing_weight["weight"])) + existing_weight["weight"], round_up = round(scale_factor*existing_weight["weight"], round_up) + + +def round(number, round_up): + import math + if round_up: + return math.ceil(number), not round_up + return math.floor(number), not round_up def _validate_traffic_sum(revision_weights): weight_sum = sum([int(w.split('=', 1)[1]) for w in revision_weights if len(w.split('=', 1)) == 2 and _validate_weight(w.split('=', 1)[1])]) if weight_sum > 100: - raise ValidationError("Traffic sums may not exceed 100.") + #raise ValidationError("Traffic sums may not exceed 100.") + pass def _get_app_from_revision(revision): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2d0f3415e17..67f6eb68d68 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1534,17 +1534,23 @@ def set_ingress_traffic(cmd, name, resource_group_name, label_weights=None, revi try: containerapp_def["properties"]["configuration"]["ingress"] + containerapp_def["properties"]["configuration"]["ingress"]["traffic"] except Exception as e: raise ValidationError("Ingress must be enabled to set ingress traffic. Try running `az containerapp ingress -h` for more info.") from e - + if not revision_weights: + revision_weights = [] + + # convert label weights to appropriate revision name _append_label_weights(containerapp_def, label_weights, revision_weights) + + # validate sum is less than 100 _validate_traffic_sum(revision_weights) - if revision_weights is not None: - _update_revision_weights(containerapp_def, revision_weights) + # update revision weights to containerapp, get the old weight sum + old_weight_sum = _update_revision_weights(containerapp_def, revision_weights) - _update_weights(containerapp_def, revision_weights, label_weights) + _update_weights(containerapp_def, revision_weights, old_weight_sum) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) From f6b68d788f80867842c07342b940573f265ead47 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 11 May 2022 16:42:33 -0400 Subject: [PATCH 3/8] Fixed small bug. --- src/containerapp/azext_containerapp/_utils.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 6be1c9a77ee..afe0ed4b9de 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -912,20 +912,23 @@ def _update_weights(containerapp_def, revision_weights, old_weight_sum): new_weight_sum = sum([int(w.split('=', 1)[1]) for w in revision_weights]) revision_weight_names = [w.split('=', 1)[0].lower() for w in revision_weights] - existing_weight_sum = sum([int(w["weight"]) for w in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]]) - new_weight_sum + divisor = sum([int(w["weight"]) for w in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]]) - new_weight_sum round_up = True - scale_factor = (old_weight_sum-new_weight_sum)/existing_weight_sum + 1 + # if there is no change to be made, don't even try (also can't divide by zero) + if divisor == 0: + return + + scale_factor = (old_weight_sum-new_weight_sum)/divisor + 1 for existing_weight in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]: if "latestRevision" in existing_weight and existing_weight["latestRevision"]: if "latest" not in revision_weight_names: - print(str(scale_factor*existing_weight["weight"])) existing_weight["weight"], round_up = round(scale_factor*existing_weight["weight"], round_up) elif "revisionName" in existing_weight and existing_weight["revisionName"].lower() not in revision_weight_names: - print(str(scale_factor*existing_weight["weight"])) existing_weight["weight"], round_up = round(scale_factor*existing_weight["weight"], round_up) +# required because what if .5, .5? We need sum to be 100, so can't round up or down both times def round(number, round_up): import math if round_up: @@ -936,8 +939,7 @@ def round(number, round_up): def _validate_traffic_sum(revision_weights): weight_sum = sum([int(w.split('=', 1)[1]) for w in revision_weights if len(w.split('=', 1)) == 2 and _validate_weight(w.split('=', 1)[1])]) if weight_sum > 100: - #raise ValidationError("Traffic sums may not exceed 100.") - pass + raise ValidationError("Traffic sums may not exceed 100.") def _get_app_from_revision(revision): From c766df8f33139cf2f1541966fbc2de35965e9a84 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 11 May 2022 16:50:15 -0400 Subject: [PATCH 4/8] Added more items to revision table output. --- src/containerapp/azext_containerapp/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 62c01d19475..d6c60a9c063 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -27,7 +27,7 @@ def transform_containerapp_list_output(apps): def transform_revision_output(rev): - props = ['name', 'active', 'createdTime', 'trafficWeight'] + props = ['name', 'active', 'createdTime', 'trafficWeight', 'healthState', 'provisioningState', 'replicas'] result = {k: rev['properties'][k] for k in rev['properties'] if k in props} if 'name' in rev: From 9c6d802cadcec98d273d4a9bce845f1ba23977ca Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 11 May 2022 17:38:43 -0400 Subject: [PATCH 5/8] Use patch instead of create or update API for traffic set. --- src/containerapp/azext_containerapp/custom.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 67f6eb68d68..3796293706d 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1552,12 +1552,16 @@ def set_ingress_traffic(cmd, name, resource_group_name, label_weights=None, revi _update_weights(containerapp_def, revision_weights, old_weight_sum) - _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + containerapp_patch_def = {} + containerapp_patch_def['properties'] = {} + containerapp_patch_def['properties']['configuration'] = {} + containerapp_patch_def['properties']['configuration']['ingress'] = {} + containerapp_patch_def['properties']['configuration']['ingress']['traffic'] = containerapp_def["properties"]["configuration"]["ingress"]["traffic"] 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"]["configuration"]["ingress"]["traffic"] + r = ContainerAppClient.update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_patch_def, no_wait=no_wait) + return r['properties']['configuration']['ingress']['traffic'] except Exception as e: handle_raw_exception(e) From 375e3d15b70a7713962d1918f1f625c080758839 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 11 May 2022 19:28:35 -0400 Subject: [PATCH 6/8] Fixed small bug related to floats not rounding. --- src/containerapp/azext_containerapp/_utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index afe0ed4b9de..8696a8ba799 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -923,14 +923,15 @@ def _update_weights(containerapp_def, revision_weights, old_weight_sum): for existing_weight in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]: if "latestRevision" in existing_weight and existing_weight["latestRevision"]: if "latest" not in revision_weight_names: - existing_weight["weight"], round_up = round(scale_factor*existing_weight["weight"], round_up) + existing_weight["weight"], round_up = _round(scale_factor*existing_weight["weight"], round_up) elif "revisionName" in existing_weight and existing_weight["revisionName"].lower() not in revision_weight_names: - existing_weight["weight"], round_up = round(scale_factor*existing_weight["weight"], round_up) + existing_weight["weight"], round_up = _round(scale_factor*existing_weight["weight"], round_up) # required because what if .5, .5? We need sum to be 100, so can't round up or down both times -def round(number, round_up): +def _round(number, round_up): import math + number = round(number, 2) # required because we are dealing with floats if round_up: return math.ceil(number), not round_up return math.floor(number), not round_up From 17145f2ca770db9b8de2ed4c42306d8f26c83282 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 11 May 2022 19:50:19 -0400 Subject: [PATCH 7/8] Added help. Updated resourcenotfounderror. Fixed existing bug related to no name given for revision command and latest passed. Added revision name inference from --revision to revision label add. --- src/containerapp/azext_containerapp/_help.py | 38 +++++++++++++++++-- .../azext_containerapp/_params.py | 9 ++++- src/containerapp/azext_containerapp/_utils.py | 3 +- src/containerapp/azext_containerapp/custom.py | 13 ++++--- 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index ab4cde705eb..573610efecf 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -263,6 +263,32 @@ az containerapp revision copy -n MyContainerapp -g MyResourceGroup --cpu 0.75 --memory 1.5Gi """ +helps['containerapp revision label'] = """ + type: group + short-summary: Manage revision labels assigned to traffic weights. +""" + +helps['containerapp revision label add'] = """ + type: command + short-summary: Set a revision label to a revision with an associated traffic weight. + examples: + - name: Add a label to the latest revision. + text: | + az containerapp revision label add -n MyContainerapp -g MyResourceGroup --label myLabel --revision latest + - name: Add a label to a previous revision. + text: | + az containerapp revision label add -g MyResourceGroup --label myLabel --revision revisionName +""" + +helps['containerapp revision label remove'] = """ + type: command + short-summary: Remove a revision label from a revision with an associated traffic weight. + examples: + - name: Remove a label. + text: | + az containerapp revision label remove -n MyContainerapp -g MyResourceGroup --label myLabel +""" + # Environment Commands helps['containerapp env'] = """ type: group @@ -459,12 +485,18 @@ type: command short-summary: Configure traffic-splitting for a container app. examples: - - name: Route 100%% of a container app's traffic to its latest revision. + - name: Route 100% of a container app's traffic to its latest revision. text: | - az containerapp ingress traffic set -n MyContainerapp -g MyResourceGroup --traffic-weight latest=100 + az containerapp ingress traffic set -n MyContainerapp -g MyResourceGroup --revision-weight latest=100 - name: Split a container app's traffic between two revisions. text: | - az containerapp ingress traffic set -n MyContainerapp -g MyResourceGroup --traffic-weight latest=80 MyRevisionName=20 + az containerapp ingress traffic set -n MyContainerapp -g MyResourceGroup --revision-weight latest=80 MyRevisionName=20 + - name: Split a container app's traffic between two labels. + text: | + az containerapp ingress traffic set -n MyContainerapp -g MyResourceGroup --label-weight myLabel=80 myLabel2=20 + - name: Split a container app's traffic between a label and a revision. + text: | + az containerapp ingress traffic set -n MyContainerapp -g MyResourceGroup --revision-weight latest=80 --label-weight myLabel=20 """ helps['containerapp ingress traffic show'] = """ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index fe609bc43e2..8dafd6da9c0 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -179,6 +179,11 @@ def load_arguments(self, _): c.argument('from_revision', help='Revision to copy from. Default: latest revision.') c.argument('image', options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") + with self.argument_context('containerapp revision label') as c: + c.argument('name', id_part=None) + c.argument('revision', help='Name of the revision.') + c.argument('label', help='Name of the label.') + with self.argument_context('containerapp ingress') as c: c.argument('allow_insecure', help='Allow insecure connections for ingress traffic.') c.argument('type', validator=validate_ingress, arg_type=get_enum_type(['internal', 'external']), help="The ingress type.") @@ -186,8 +191,8 @@ def load_arguments(self, _): c.argument('target_port', type=int, validator=validate_target_port, help="The application port used for ingress traffic.") with self.argument_context('containerapp ingress traffic') as c: - c.argument('revision_weights', nargs='*', options_list=['--revision-weight'], help="A list of revision weight(s) for the container app. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") - c.argument('label_weights', nargs='*', options_list=['--label-weight'], help="A list of label weight(s) for the container app. Space-separated values in 'label_name=weight' format.") + c.argument('revision_weights', nargs='+', options_list=['--revision-weight'], help="A list of revision weight(s) for the container app. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") + c.argument('label_weights', nargs='+', options_list=['--label-weight'], help="A list of label weight(s) for the container app. Space-separated values in 'label_name=weight' format.") 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' format (where 'key' cannot be longer than 20 characters).") diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index dd068bc5e8a..f832ceb9185 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -947,7 +947,8 @@ def _validate_traffic_sum(revision_weights): def _get_app_from_revision(revision): if not revision: raise ValidationError('Invalid revision. Revision must not be empty') - + if revision.lower() == "latest": + raise ValidationError('Please provide a name for your containerapp. Cannot lookup name of containerapp without a full revision name.') revision = revision.split('--') revision.pop() revision = "--".join(revision) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ecf87b118d8..2d4b119cc2b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1344,9 +1344,12 @@ def set_revision_mode(cmd, resource_group_name, name, mode, no_wait=False): handle_raw_exception(e) -def add_revision_label(cmd, resource_group_name, name, revision, label, no_wait=False): +def add_revision_label(cmd, resource_group_name, revision, label, name=None, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") - + + if not name: + name = _get_app_from_revision(revision) + containerapp_def = None try: containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) @@ -1354,7 +1357,7 @@ def add_revision_label(cmd, resource_group_name, name, revision, label, no_wait= pass if not containerapp_def: - raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError(f"The containerapp '{name}' does not exist in group '{resource_group_name}'") if "ingress" not in containerapp_def['properties']['configuration'] and "traffic" not in containerapp_def['properties']['configuration']['ingress']: raise ValidationError("Ingress and traffic weights are required to set labels.") @@ -1402,7 +1405,7 @@ def remove_revision_label(cmd, resource_group_name, name, label, no_wait=False): pass if not containerapp_def: - raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError(f"The containerapp '{name}' does not exist in group '{resource_group_name}'") if "ingress" not in containerapp_def['properties']['configuration'] and "traffic" not in containerapp_def['properties']['configuration']['ingress']: raise ValidationError("Ingress and traffic weights are required to set labels.") @@ -1529,7 +1532,7 @@ def set_ingress_traffic(cmd, name, resource_group_name, label_weights=None, revi pass if not containerapp_def: - raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError(f"The containerapp '{name}' does not exist in group '{resource_group_name}'") try: containerapp_def["properties"]["configuration"]["ingress"] From 800df7652d1b729e412908f87ca9f40836ea391b Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 12 May 2022 13:21:19 -0400 Subject: [PATCH 8/8] Fixed style. --- src/containerapp/azext_containerapp/_utils.py | 10 +++++----- src/containerapp/azext_containerapp/custom.py | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index f832ceb9185..93c6c79328d 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -846,7 +846,7 @@ def _update_revision_weights(containerapp_def, list_weights): containerapp_def["properties"]["configuration"]["ingress"]["traffic"] = [] if not list_weights: - return + return 0 for new_weight in list_weights: key_val = new_weight.split('=', 1) @@ -898,7 +898,7 @@ def _append_label_weights(containerapp_def, label_weights, revision_weights): for existing_weight in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]: if "label" in existing_weight and existing_weight["label"].lower() == label.lower(): if "revisionName" in existing_weight and existing_weight["revisionName"] and existing_weight["revisionName"].lower() in revision_weight_names: - logger.warning("Already passed value for revision {}, will not overwrite with {}.".format(existing_weight["revisionName"], new_weight)) + logger.warning("Already passed value for revision {}, will not overwrite with {}.".format(existing_weight["revisionName"], new_weight)) # pylint: disable=logging-format-interpolation is_existing = True break revision_weights.append("{}={}".format(existing_weight["revisionName"] if "revisionName" in existing_weight and existing_weight["revisionName"] else "latest", weight)) @@ -919,14 +919,14 @@ def _update_weights(containerapp_def, revision_weights, old_weight_sum): if divisor == 0: return - scale_factor = (old_weight_sum-new_weight_sum)/divisor + 1 + scale_factor = (old_weight_sum - new_weight_sum) / divisor + 1 for existing_weight in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]: if "latestRevision" in existing_weight and existing_weight["latestRevision"]: if "latest" not in revision_weight_names: - existing_weight["weight"], round_up = _round(scale_factor*existing_weight["weight"], round_up) + existing_weight["weight"], round_up = _round(scale_factor * existing_weight["weight"], round_up) elif "revisionName" in existing_weight and existing_weight["revisionName"].lower() not in revision_weight_names: - existing_weight["weight"], round_up = _round(scale_factor*existing_weight["weight"], round_up) + existing_weight["weight"], round_up = _round(scale_factor * existing_weight["weight"], round_up) # required because what if .5, .5? We need sum to be 100, so can't round up or down both times diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2d4b119cc2b..acb34e70008 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1418,7 +1418,6 @@ def remove_revision_label(cmd, resource_group_name, name, label, no_wait=False): label_removed = True weight["label"] = None break - if not label_removed: raise ValidationError("Please specify a label name with an associated traffic weight.")