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

Added label support #101

Merged
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
2 changes: 1 addition & 1 deletion src/containerapp/azext_containerapp/_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
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