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

SDK Migration. #121

Closed
wants to merge 15 commits into from
29 changes: 28 additions & 1 deletion scripts/ci/credscan/CredScanSuppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,38 @@
"src\\containerapp\\azext_containerapp\\tests\\latest\\recordings\\test_containerapp_dapr_e2e.yaml",
"src\\containerapp\\azext_containerapp\\tests\\latest\\recordings\\test_containerapp_up_image_e2e.yaml",
"src\\containerapp\\azext_containerapp\\tests\\latest\\recordings\\test_containerapp_custom_domains_e2e.yaml",
"src\\containerapp\\azext_containerapp\\tests\\latest\\recordings\\test_containerapp_revision_label_e2e.yaml",
"src\\containerapp\\azext_containerapp\\tests\\latest\\cert.pfx",
"src\\containerapp\\azext_containerapp\\tests\\latest\\test_containerapp_commands.py",
"src\\containerapp\\azext_containerapp\\tests\\latest\test_containerapp_env_commands.py"
"src\\containerapp\\azext_containerapp\\tests\\latest\\test_containerapp_env_commands.py"
],
"_justification": "Dummy resources' keys left during testing Microsoft.App (required for log-analytics to create managedEnvironments)"
},
{
"file": [
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_basic_no_existing_resources.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_resources_from_both_cpus_and_deploy_cpu.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_environment.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_resources_from_deploy_cpu.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_environment_prompt.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_resources_from_service_cpus.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_ingress_both.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_secrets.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_ingress_external.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_secrets_and_existing_environment.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_ingress_internal.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_secrets_and_existing_environment_conflict.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_ingress_prompt.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_transport_arg.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_registry_all_args.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_with_command_list.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_registry_server_arg_only.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_with_command_list_and_entrypoint.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_replicas_global_scale.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_with_command_string.yaml",
"src\\containerapp-compose\\azext_containerapp_compose\\tests\\latest\\recordings\\test_containerapp_compose_create_with_replicas_replicated_mode.yaml"
],
"_justification": "Dummy resources' tokens left during testing."
}
]
}
14 changes: 14 additions & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
Release History
===============

0.3.7
++++++

0.3.6
++++++
* BREAKING CHANGE: 'az containerapp revision list' now shows only active revisions by default, added flag --all to show all revisions
* BREAKING CHANGE: 'az containerapp env certificate upload' does not prompt by default when re-uploading an existing certificate. Added --show-prompt to show prompts on re-upload.
* Added parameter --environment to 'az containerapp list'
* Added 'az containerapp revision label swap' to swap traffic labels
* Fixed bug with 'az containerapp up' where custom domains would be removed when updating existing containerapp
* Fixed bug with 'az containerapp auth update' when using --unauthenticated-client-action
* Fixed bug with 'az containerapp env certificate upload' where it shows a misleading message for invalid certificate name
* 'az containerapp registry set': allow authenticating with managed identity (MSI) instead of ACR username & password

0.3.5
++++++
* Add parameter --zone-redundant to 'az containerapp env create'
Expand Down
9 changes: 6 additions & 3 deletions src/containerapp/azext_containerapp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@
from azure.cli.core import AzCommandsLoader

from azext_containerapp._help import helps # pylint: disable=unused-import

from azext_containerapp._client_factory import app_client_factory

class ContainerappCommandsLoader(AzCommandsLoader):

def __init__(self, cli_ctx=None):
from azure.cli.core.commands import CliCommandType
from azure.cli.core.profiles import ResourceType

containerapp_custom = CliCommandType(
operations_tmpl='azext_containerapp.custom#{}',
client_factory=None)
client_factory=app_client_factory)
super(ContainerappCommandsLoader, self).__init__(cli_ctx=cli_ctx,
custom_command_type=containerapp_custom)
custom_command_type=containerapp_custom,
resource_type=ResourceType.MGMT_APPCONTAINERS)

def load_command_table(self, args):
from azext_containerapp.commands import load_command_table
Expand Down
32 changes: 32 additions & 0 deletions src/containerapp/azext_containerapp/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def handle_raw_exception(e):
import json

stringErr = str(e)
if "response" in stringErr.lower():
stringErr = stringErr[stringErr.lower().rindex("response"):]

if "{" in stringErr and "}" in stringErr:
jsonError = stringErr[stringErr.index("{"):stringErr.rindex("}") + 1]
Expand Down Expand Up @@ -73,3 +75,33 @@ def log_analytics_shared_key_client_factory(cli_ctx):
from azure.mgmt.loganalytics import LogAnalyticsManagementClient

return get_mgmt_service_client(cli_ctx, LogAnalyticsManagementClient).shared_keys


def app_client_factory(cli_ctx, *_):
from azure.mgmt.appcontainers import ContainerAppsAPIClient
from azure.cli.core.commands.client_factory import get_mgmt_service_client
return get_mgmt_service_client(cli_ctx, ContainerAppsAPIClient)


def cf_containerapps(cli_ctx, *_):
return app_client_factory(cli_ctx).container_apps


def cf_managedenvs(cli_ctx, *_):
return app_client_factory(cli_ctx).managed_environments


def cf_revisions(cli_ctx, *_):
return app_client_factory(cli_ctx).container_apps_revisions

def cf_replicas(cli_ctx, *_):
return app_client_factory(cli_ctx).container_apps_revision_replicas

def cf_dapr_components(cli_ctx, *_):
return app_client_factory(cli_ctx).dapr_components

def cf_certificates(cli_ctx, *_):
return app_client_factory(cli_ctx).certificates

def cf_namespaces(cli_ctx, *_):
return app_client_factory(cli_ctx).namespaces
7 changes: 6 additions & 1 deletion src/containerapp/azext_containerapp/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
SHORT_POLLING_INTERVAL_SECS = 3
LONG_POLLING_INTERVAL_SECS = 10

ACR_IMAGE_SUFFIX = ".azurecr.io"

LOG_ANALYTICS_RP = "Microsoft.OperationalInsights"
CONTAINER_APPS_RP = "Microsoft.App"

Expand All @@ -21,6 +23,9 @@
MSA_SECRET_SETTING_NAME = "msa-provider-authentication-secret"
TWITTER_SECRET_SETTING_NAME = "twitter-provider-authentication-secret"
APPLE_SECRET_SETTING_NAME = "apple-provider-authentication-secret"
UNAUTHENTICATED_CLIENT_ACTION = ['RedirectToLoginPage', 'AllowAnonymous', 'RejectWith401', 'RejectWith404']
UNAUTHENTICATED_CLIENT_ACTION = ['RedirectToLoginPage', 'AllowAnonymous', 'Return401', 'Return403']
FORWARD_PROXY_CONVENTION = ['NoProxy', 'Standard', 'Custom']
CHECK_CERTIFICATE_NAME_AVAILABILITY_TYPE = "Microsoft.App/managedEnvironments/certificates"

NAME_INVALID = "Invalid"
NAME_ALREADY_EXISTS = "AlreadyExists"
13 changes: 13 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,15 @@
az containerapp revision label remove -n MyContainerapp -g MyResourceGroup --label myLabel
"""

helps['containerapp revision label swap'] = """
type: command
short-summary: Swap a revision label between two revisions with associated traffic weights.
examples:
- name: Swap a revision label between two revisions.
text: |
az containerapp revision label swap -n MyContainerapp -g MyResourceGroup --source myLabel1 --target myLabel2
"""

# Environment Commands
helps['containerapp env'] = """
type: group
Expand All @@ -303,6 +312,10 @@
text: |
az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\
--location eastus2
- name: Create a zone-redundant environment
text: |
az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\
--location eastus2 --zone-redundant
- name: Create an environment with an existing Log Analytics workspace.
text: |
az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\
Expand Down
9 changes: 9 additions & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def load_arguments(self, _):
c.argument('platform_reserved_cidr', options_list=['--platform-reserved-cidr'], help='IP range in CIDR notation that can be reserved for environment infrastructure IP addresses. It must not overlap with any other Subnet IP ranges')
c.argument('platform_reserved_dns_ip', options_list=['--platform-reserved-dns-ip'], help='An IP address from the IP range defined by Platform Reserved CIDR that will be reserved for the internal DNS server.')
c.argument('internal_only', arg_type=get_three_state_flag(), options_list=['--internal-only'], help='Boolean indicating the environment only has an internal load balancer. These environments do not have a public static IP resource, therefore must provide infrastructureSubnetResourceId if enabling this property')

with self.argument_context('containerapp env create') as c:
c.argument('zone_redundant', options_list=["--zone-redundant", "-z"], help="Enable zone redundancy on the environment. Cannot be used without --infrastructure-subnet-resource-id. If used with --location, the subnet's location must match")

Expand All @@ -155,6 +156,7 @@ def load_arguments(self, _):
c.argument('certificate_file', options_list=['--certificate-file', '-f'], help='The filepath of the .pfx or .pem file')
c.argument('certificate_name', options_list=['--certificate-name', '-c'], help='Name of the certificate which should be unique within the Container Apps environment.')
c.argument('certificate_password', options_list=['--password', '-p'], help='The certificate file password')
c.argument('prompt', options_list=['--show-prompt'], action='store_true', help='Show prompt to upload an existing certificate.')

with self.argument_context('containerapp env certificate list') as c:
c.argument('name', id_part=None)
Expand Down Expand Up @@ -200,6 +202,7 @@ def load_arguments(self, _):

with self.argument_context('containerapp revision') as c:
c.argument('revision_name', options_list=['--revision'], help='Name of the revision.')
c.argument('all', help='Show inactive revisions.', action='store_true')

with self.argument_context('containerapp revision copy') as c:
c.argument('from_revision', help='Revision to copy from. Default: latest revision.')
Expand All @@ -209,6 +212,11 @@ def load_arguments(self, _):
c.argument('name', id_part=None)
c.argument('revision', help='Name of the revision.')
c.argument('label', help='Name of the label.')
c.argument('yes', options_list=['--no-prompt', '--yes', '-y'], help='Do not prompt for confirmation.', action='store_true')

with self.argument_context('containerapp revision label') as c:
c.argument('source_label', options_list=['--source'], help='Source label to be swapped.')
c.argument('target_label', options_list=['--target'], help='Target label to be swapped to.')

with self.argument_context('containerapp ingress') as c:
c.argument('allow_insecure', help='Allow insecure connections for ingress traffic.')
Expand Down Expand Up @@ -241,6 +249,7 @@ def load_arguments(self, _):
c.argument('server', help="The container registry server, e.g. myregistry.azurecr.io")
c.argument('username', help='The username of the registry. If using Azure Container Registry, we will try to infer the credentials if not supplied')
c.argument('password', help='The password of the registry. If using Azure Container Registry, we will try to infer the credentials if not supplied')
c.argument('identity', help="The managed identity with which to authenticate to the Azure Container Registry (instead of username/password). Use 'system' for a system-defined identity or a resource id for a user-defined identity. The managed identity should have been assigned acrpull permissions on the ACR before deployment (use 'az role assignment create --role acrpull ...').")

with self.argument_context('containerapp registry list') as c:
c.argument('name', id_part=None)
Expand Down
51 changes: 34 additions & 17 deletions src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,13 +857,27 @@ def _update_revision_weights(containerapp_def, list_weights):
return old_weight_sum


def _validate_revision_name(cmd, revision, resource_group_name, name):
if revision.lower() == "latest":
return
revision_def = None
try:
revision_def = ContainerAppClient.show_revision(cmd, resource_group_name, name, revision)
except: # pylint: disable=bare-except
pass

if not revision_def:
raise ValidationError(f"Revision '{revision}' is not a valid revision name.")


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

bad_labels = []
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)
Expand All @@ -885,15 +899,17 @@ def _append_label_weights(containerapp_def, label_weights, revision_weights):
break

if not is_existing:
raise ValidationError(f"No label {label} assigned to any traffic weight.")
bad_labels.append(label)

if len(bad_labels) > 0:
raise ValidationError(f"No labels '{', '.join(bad_labels)}' 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
Expand All @@ -903,18 +919,17 @@ 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(scale_factor * existing_weight["weight"])
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(scale_factor * existing_weight["weight"])

# 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
total_sum = sum([int(w["weight"]) for w in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]])
index = 0
while total_sum < 100:
weight = containerapp_def["properties"]["configuration"]["ingress"]["traffic"][index % len(containerapp_def["properties"]["configuration"]["ingress"]["traffic"])]
index += 1
total_sum += 1
weight["weight"] += 1


def _validate_traffic_sum(revision_weights):
Expand Down Expand Up @@ -1255,14 +1270,16 @@ def load_cert_file(file_path, cert_password=None):


def check_cert_name_availability(cmd, resource_group_name, name, cert_name):
name_availability_request = {}
name_availability_request["name"] = cert_name
name_availability_request["type"] = CHECK_CERTIFICATE_NAME_AVAILABILITY_TYPE
from ._client_factory import cf_namespaces
from azure.mgmt.appcontainers.models import CheckNameAvailabilityRequest
client = cf_namespaces(cmd.cli_ctx)

name_availability_request = CheckNameAvailabilityRequest(name=cert_name, type=CHECK_CERTIFICATE_NAME_AVAILABILITY_TYPE)
try:
r = ManagedEnvironmentClient.check_name_availability(cmd, resource_group_name, name, name_availability_request)
r = client.check_name_availability(resource_group_name=resource_group_name, environment_name=name, check_name_availability_request=name_availability_request)
except CLIError as e:
handle_raw_exception(e)
return r["nameAvailable"]
return r


def validate_hostname(cmd, resource_group_name, name, hostname):
Expand Down
Loading