Skip to content

Commit

Permalink
Added label support (Azure#101)
Browse files Browse the repository at this point in the history
* Finished label and revision work.

* Finished dynamic weight update work.

* Fixed small bug.

* Added more items to revision table output.

* Use patch instead of create or update API for traffic set.

* Fixed small bug related to floats not rounding.

* 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.

* Fixed style.

Co-authored-by: Haroon Feisal <haroonfeisal@microsoft.com>
  • Loading branch information
runefa and Haroon Feisal authored May 12, 2022
1 parent b475d7d commit 914a32b
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 31 deletions.
38 changes: 35 additions & 3 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'] = """
Expand Down
8 changes: 7 additions & 1 deletion src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,20 @@ 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.")
c.argument('transport', arg_type=get_enum_type(['auto', 'http', 'http2']), help="The transport protocol used for ingress traffic.")
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).")
Expand Down
113 changes: 98 additions & 15 deletions src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,42 +830,125 @@ 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):
old_weight_sum = 0
if "traffic" not in containerapp_def["properties"]["configuration"]["ingress"]:
containerapp_def["properties"]["configuration"]["ingress"]["traffic"] = []

if not list_weights:
return 0

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>=<weight> <revision2>=<weight2> ...\"')
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":
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
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"
})
return old_weight_sum


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 \"<revision>=weight <revision2>=<weigh2> ...\"')
raise ValidationError('Traffic weights must be in format \"<revision>=<weight> <revision2>=<weight2> ...\"')
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)) # 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))
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, 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]
divisor = sum([int(w["weight"]) for w in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]]) - new_weight_sum
round_up = True
# 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:
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)


# 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
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


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):
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)
Expand Down
6 changes: 5 additions & 1 deletion src/containerapp/azext_containerapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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())
Expand Down
Loading

0 comments on commit 914a32b

Please sign in to comment.