diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index 013baabc681..70d23c6be8c 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -41,7 +41,8 @@ create_service_principal_for_rbac, repo_url_to_name, get_container_app_if_exists, - trigger_workflow + trigger_workflow, + _ensure_location_allowed ) from ._constants import MAXIMUM_SECRET_LENGTH @@ -62,6 +63,8 @@ def __init__(self, cmd, name: str, location: str, exists: bool = None): self.cmd = cmd self.name = name self.location = _get_default_containerapps_location(cmd, location) + if self.location.lower() == "northcentralusstage": + self.location = "eastus" self.exists = exists self.check_exists() @@ -151,7 +154,7 @@ def __init__( rg = parse_resource_id(name)["resource_group"] if resource_group.name != rg: self.resource_group = ResourceGroup(cmd, rg, location) - self.location = _get_default_containerapps_location(cmd, location) + self.location = location self.logs_key = logs_key self.logs_customer_id = logs_customer_id @@ -164,7 +167,7 @@ def set_name(self, name_or_rid): self.resource_group = ResourceGroup( self.cmd, rg, - _get_default_containerapps_location(self.cmd, self.location), + self.location, ) else: self.name = name_or_rid @@ -188,6 +191,7 @@ def create_if_needed(self, app_name): ) # TODO use .info() def create(self): + self.location = validate_environment_location(self.cmd, self.location) env = create_managed_environment( self.cmd, self.name, @@ -291,7 +295,7 @@ def create_acr(self): url = self.registry_server registry_name = url[: url.rindex(".azurecr.io")] registry_def = create_new_acr( - self.cmd, registry_name, registry_rg.name, self.env.location + self.cmd, registry_name, registry_rg.name, self.env.location if self.env.location.lower() != "northcentralusstage" else "eastus" ) self.registry_server = registry_def.login_server @@ -782,3 +786,69 @@ def find_existing_acr(cmd, app: "ContainerApp"): app.should_create_acr = False return acr.name, parse_resource_id(acr.id)["resource_group"] return None, None + + +def validate_environment_location(cmd, location): + MAX_ENV_PER_LOCATION = 2 + env_list = list_managed_environments(cmd) + + locations = [l["location"] for l in env_list] + locations = list(set(locations)) # remove duplicates + + location_count = {} + for loc in locations: + location_count[loc] = len([e for e in env_list if e["location"] == loc]) + + disallowed_locations = [] + for _, value in enumerate(location_count): + if location_count[value] > MAX_ENV_PER_LOCATION - 1: + disallowed_locations.append(value) + + res_locations = list_environment_locations(cmd) + res_locations = [l for l in res_locations if l not in disallowed_locations] + + allowed_locs = ", ".join(res_locations) + + if location: + try: + _ensure_location_allowed(cmd, location, "Microsoft.App", "managedEnvironments") + except Exception: # pylint: disable=broad-except + raise ValidationError("You cannot create a Containerapp environment in location {}. List of eligible locations: {}.".format(location, allowed_locs)) + + if len(res_locations) > 0: + if not location: + logger.warning("Creating environment on location {}.".format(res_locations[0])) + return res_locations[0] + if location in disallowed_locations: + raise ValidationError("You have more than {} environments in location {}. List of eligible locations: {}.".format(MAX_ENV_PER_LOCATION, location, allowed_locs)) + return location + else: + raise ValidationError("You cannot create any more environments. Environments are limited to {} per location in a subscription. Please specify an existing environment using --environment.".format(MAX_ENV_PER_LOCATION)) + + +def list_environment_locations(cmd): + from ._utils import providers_client_factory + providers_client = providers_client_factory(cmd.cli_ctx, get_subscription_id(cmd.cli_ctx)) + resource_types = getattr(providers_client.get("Microsoft.App"), 'resource_types', []) + res_locations = [] + for res in resource_types: + if res and getattr(res, 'resource_type', "") == "managedEnvironments": + res_locations = getattr(res, 'locations', []) + + res_locations = [res_loc.lower().replace(" ", "").replace("(", "").replace(")", "") for res_loc in res_locations if res_loc.strip()] + + return res_locations + + +def check_env_name_on_rg(cmd, managed_env, resource_group_name, location): + if location: + _ensure_location_allowed(cmd, location, "Microsoft.App", "managedEnvironments") + if managed_env and resource_group_name and location: + env_def = None + try: + env_def = ManagedEnvironmentClient.show(cmd, resource_group_name, parse_resource_id(managed_env)["name"]) + except: + pass + if env_def: + if location != env_def["location"]: + raise ValidationError("Environment {} already exists in resource group {} on location {}, cannot change location of existing environment to {}.".format(parse_resource_id(managed_env)["name"], resource_group_name, env_def["location"], location)) \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ce52bd8e5d8..8160b7b7d10 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2015,12 +2015,14 @@ def containerapp_up(cmd, service_principal_tenant_id=None): from ._up_utils import (_validate_up_args, _reformat_image, _get_dockerfile_content, _get_ingress_and_target_port, ResourceGroup, ContainerAppEnvironment, ContainerApp, _get_registry_from_app, - _get_registry_details, _create_github_action, _set_up_defaults, up_output, AzureContainerRegistry) + _get_registry_details, _create_github_action, _set_up_defaults, up_output, AzureContainerRegistry, + check_env_name_on_rg) HELLOWORLD = "mcr.microsoft.com/azuredocs/containerapps-helloworld" dockerfile = "Dockerfile" # for now the dockerfile name must be "Dockerfile" (until GH actions API is updated) _validate_up_args(source, image, repo, registry_server) validate_container_app_name(name) + check_env_name_on_rg(cmd, managed_env, resource_group_name, location) image = _reformat_image(source, repo, image) token = None if not repo else get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"], token)