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 5 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
3 changes: 2 additions & 1 deletion src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).")
Expand Down
109 changes: 95 additions & 14 deletions src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -829,36 +829,117 @@ 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

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))
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
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):
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
128 changes: 118 additions & 10 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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))
haroonf marked this conversation as resolved.
Show resolved Hide resolved

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")

Expand Down Expand Up @@ -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:
Expand All @@ -1442,18 +1534,34 @@ def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait

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 traffic_weights is not None:
_update_traffic_weights(containerapp_def, traffic_weights)
if not revision_weights:
revision_weights = []

_get_existing_secrets(cmd, resource_group_name, name, containerapp_def)
# 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)

# 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, old_weight_sum)

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)

Expand Down