diff --git a/src/appservice-kube/HISTORY.rst b/src/appservice-kube/HISTORY.rst index 2dc9269a52b..3e3b15de4b6 100644 --- a/src/appservice-kube/HISTORY.rst +++ b/src/appservice-kube/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +0.1.4 +++++++ +* Ensure compatibility of 'az webapp create' and 'az functionapp create' with CLI version 2.34.0 + 0.1.3 ++++++ * Update functionapp runtimes and support v4 functionapps diff --git a/src/appservice-kube/azext_appservice_kube/_constants.py b/src/appservice-kube/azext_appservice_kube/_constants.py index a9a73120ee4..ee8eed8f8d8 100644 --- a/src/appservice-kube/azext_appservice_kube/_constants.py +++ b/src/appservice-kube/azext_appservice_kube/_constants.py @@ -13,83 +13,6 @@ KUBE_FUNCTION_APP_KIND = 'linux,kubernetes,functionapp' KUBE_FUNCTION_CONTAINER_APP_KIND = 'linux,kubernetes,functionapp,container' -LINUX_RUNTIMES = ['dotnet', 'node', 'python', 'java', 'powershell', 'dotnet-isolated', 'custom'] -WINDOWS_RUNTIMES = ['dotnet', 'node', 'java', 'powershell', 'dotnet-isolated', 'custom'] - -NODE_VERSION_DEFAULT = "10.14" -NODE_VERSION_NEWER = "12-lts" -NODE_EXACT_VERSION_DEFAULT = "10.14.1" -NETCORE_VERSION_DEFAULT = "2.2" -DOTNET_VERSION_DEFAULT = "4.7" -PYTHON_VERSION_DEFAULT = "3.7" -NETCORE_RUNTIME_NAME = "dotnetcore" -DOTNET_RUNTIME_NAME = "aspnet" -NODE_RUNTIME_NAME = "node" -PYTHON_RUNTIME_NAME = "python" -OS_DEFAULT = "Windows" -STATIC_RUNTIME_NAME = "static" # not an official supported runtime but used for CLI logic -NODE_VERSIONS = ['4.4', '4.5', '6.2', '6.6', '6.9', '6.11', '8.0', '8.1', '8.9', '8.11', '10.1', '10.10', '10.14'] -PYTHON_VERSIONS = ['3.7', '3.6', '2.7'] -NETCORE_VERSIONS = ['1.0', '1.1', '2.1', '2.2'] -DOTNET_VERSIONS = ['3.5', '4.7'] - -LINUX_SKU_DEFAULT = "P1V2" -FUNCTIONS_VERSIONS = ['2', '3', '4'] - -# functions version : default node version -FUNCTIONS_VERSION_TO_DEFAULT_NODE_VERSION = { - '2': '~10', - '3': '~12' -} -# functions version -> runtime : default runtime version -FUNCTIONS_VERSION_TO_DEFAULT_RUNTIME_VERSION = { - '2': { - 'node': '8', - 'dotnet': '2', - 'python': '3.7', - 'java': '8' - }, - '3': { - 'node': '12', - 'dotnet': '3', - 'python': '3.7', - 'java': '8' - } -} -# functions version -> runtime : runtime versions -FUNCTIONS_VERSION_TO_SUPPORTED_RUNTIME_VERSIONS = { - '2': { - 'node': ['8', '10'], - 'python': ['3.6', '3.7'], - 'dotnet': ['2'], - 'java': ['8'] - }, - '3': { - 'node': ['10', '12'], - 'python': ['3.6', '3.7', '3.8'], - 'dotnet': ['3'], - 'java': ['8'] - } -} -# dotnet runtime version : dotnet linuxFxVersion -DOTNET_RUNTIME_VERSION_TO_DOTNET_LINUX_FX_VERSION = { - '2': '2.2', - '3': '3.1' -} - MULTI_CONTAINER_TYPES = ['COMPOSE', 'KUBE'] OS_TYPES = ['Windows', 'Linux'] - -CONTAINER_APPSETTING_NAMES = ['DOCKER_REGISTRY_SERVER_URL', 'DOCKER_REGISTRY_SERVER_USERNAME', - 'DOCKER_REGISTRY_SERVER_PASSWORD', "WEBSITES_ENABLE_APP_SERVICE_STORAGE"] -APPSETTINGS_TO_MASK = ['DOCKER_REGISTRY_SERVER_PASSWORD'] - - -FUNCTIONS_STACKS_API_JSON_PATHS = { - 'windows': os.path.abspath(os.path.join(os.path.abspath(__file__), '../resources/WindowsFunctionsStacks.json')), - 'linux': os.path.abspath(os.path.join(os.path.abspath(__file__), '../resources/LinuxFunctionsStacks.json')) -} - -FUNCTIONS_LINUX_RUNTIME_VERSION_REGEX = r"^.*\|(.*)$" -FUNCTIONS_WINDOWS_RUNTIME_VERSION_REGEX = r"^~(.*)$" diff --git a/src/appservice-kube/azext_appservice_kube/_create_util.py b/src/appservice-kube/azext_appservice_kube/_create_util.py index 740512fd9fa..c4c42e5b3ca 100644 --- a/src/appservice-kube/azext_appservice_kube/_create_util.py +++ b/src/appservice-kube/azext_appservice_kube/_create_util.py @@ -3,291 +3,13 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import os -import zipfile -from knack.log import get_logger -from azure.mgmt.web.models import SkuDescription -from azure.cli.core.azclierror import ValidationError, ArgumentUsageError +from knack.log import get_logger -from ._constants import (NETCORE_VERSION_DEFAULT, NETCORE_VERSIONS, NODE_VERSION_DEFAULT, - NODE_VERSIONS, NETCORE_RUNTIME_NAME, NODE_RUNTIME_NAME, DOTNET_RUNTIME_NAME, - DOTNET_VERSION_DEFAULT, DOTNET_VERSIONS, STATIC_RUNTIME_NAME, - PYTHON_RUNTIME_NAME, PYTHON_VERSION_DEFAULT, LINUX_SKU_DEFAULT, OS_DEFAULT) -from ._client_factory import web_client_factory, resource_client_factory +from ._client_factory import web_client_factory logger = get_logger(__name__) -def get_runtime_version_details(file_path, lang_name, is_kube=False): - version_detected = None - version_to_create = None - if lang_name.lower() == NETCORE_RUNTIME_NAME: - # method returns list in DESC, pick the first - version_detected = parse_netcore_version(file_path)[0] - version_to_create = detect_netcore_version_tocreate(version_detected) - elif lang_name.lower() == DOTNET_RUNTIME_NAME: - # method returns list in DESC, pick the first - version_detected = parse_dotnet_version(file_path) - version_to_create = detect_dotnet_version_tocreate(version_detected) - if is_kube: - raise ValidationError("Dotnet runtime is not supported for Kube Environments") - elif lang_name.lower() == NODE_RUNTIME_NAME: - if file_path == '': - version_detected = "-" - version_to_create = NODE_VERSION_DEFAULT - else: - version_detected = parse_node_version(file_path)[0] - version_to_create = detect_node_version_tocreate(version_detected) - elif lang_name.lower() == PYTHON_RUNTIME_NAME: - version_detected = "-" - version_to_create = PYTHON_VERSION_DEFAULT - elif lang_name.lower() == STATIC_RUNTIME_NAME: - version_detected = "-" - version_to_create = "-" - return {'detected': version_detected, 'to_create': version_to_create} - - -def zip_contents_from_dir(dirPath, lang, is_kube=None): - relroot = os.path.abspath(os.path.join(dirPath, os.pardir)) - - path_and_file = os.path.splitdrive(dirPath)[1] - file_val = os.path.split(path_and_file)[1] - zip_file_path = relroot + os.path.sep + file_val + ".zip" - abs_src = os.path.abspath(dirPath) - with zipfile.ZipFile("{}".format(zip_file_path), "w", zipfile.ZIP_DEFLATED) as zf: - for dirname, subdirs, files in os.walk(dirPath): - # skip node_modules folder for Node apps, - # since zip_deployment will perform the build operation - # TODO: Add .deployment support in Kube BuildSVC and remove this check - if not is_kube: - if lang.lower() == NODE_RUNTIME_NAME: - subdirs[:] = [d for d in subdirs if 'node_modules' not in d] - elif lang.lower() == NETCORE_RUNTIME_NAME: - subdirs[:] = [d for d in subdirs if d not in ['obj', 'bin']] - elif lang.lower() == PYTHON_RUNTIME_NAME: - subdirs[:] = [d for d in subdirs if 'env' not in d] # Ignores dir that contain env - for filename in files: - absname = os.path.abspath(os.path.join(dirname, filename)) - arcname = absname[len(abs_src) + 1:] - zf.write(absname, arcname) - return zip_file_path - - -def create_resource_group(cmd, rg_name, location): - from azure.cli.core.profiles import ResourceType, get_sdk - rcf = resource_client_factory(cmd.cli_ctx) - resource_group = get_sdk(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, 'ResourceGroup', mod='models') - rg_params = resource_group(location=location) - return rcf.resource_groups.create_or_update(rg_name, rg_params) - - -def _check_resource_group_exists(cmd, rg_name): - rcf = resource_client_factory(cmd.cli_ctx) - return rcf.resource_groups.check_existence(rg_name) - - -def _check_resource_group_supports_os(cmd, rg_name, is_linux): - # get all appservice plans from RG - client = web_client_factory(cmd.cli_ctx) - plans = list(client.app_service_plans.list_by_resource_group(rg_name)) - for item in plans: - # for Linux if an app with reserved==False exists, ASP doesn't support Linux - if is_linux and not item.reserved: - return False - if not is_linux and item.reserved: - return False - return True - - -def get_num_apps_in_asp(cmd, rg_name, asp_name): - client = web_client_factory(cmd.cli_ctx) - return len(list(client.app_service_plans.list_web_apps(rg_name, asp_name))) - - -# pylint:disable=unexpected-keyword-arg -def get_lang_from_content(src_path, html=False): - # NODE: package.json should exist in the application root dir - # NETCORE & DOTNET: *.csproj should exist in the application dir - # NETCORE: netcoreapp2.0 - # DOTNET: v4.5.2 - runtime_details_dict = dict.fromkeys(['language', 'file_loc', 'default_sku']) - package_json_file = os.path.join(src_path, 'package.json') - package_python_file = os.path.join(src_path, 'requirements.txt') - static_html_file = "" - package_netcore_file = "" - runtime_details_dict['language'] = '' - runtime_details_dict['file_loc'] = '' - runtime_details_dict['default_sku'] = 'F1' - import fnmatch - for _dirpath, _dirnames, files in os.walk(src_path): - for file in files: - if html and (fnmatch.fnmatch(file, "*.html") or fnmatch.fnmatch(file, "*.htm") or - fnmatch.fnmatch(file, "*shtml.")): - static_html_file = os.path.join(src_path, file) - break - if fnmatch.fnmatch(file, "*.csproj"): - package_netcore_file = os.path.join(src_path, file) - break - - if html: - if static_html_file: - runtime_details_dict['language'] = STATIC_RUNTIME_NAME - runtime_details_dict['file_loc'] = static_html_file - runtime_details_dict['default_sku'] = 'F1' - else: - raise ArgumentUsageError("The html flag was passed, but could not find HTML files, " - "see 'https://go.microsoft.com/fwlink/?linkid=2109470' for more information") - elif os.path.isfile(package_python_file): - runtime_details_dict['language'] = PYTHON_RUNTIME_NAME - runtime_details_dict['file_loc'] = package_python_file - runtime_details_dict['default_sku'] = LINUX_SKU_DEFAULT - elif os.path.isfile(package_json_file) or os.path.isfile('server.js') or os.path.isfile('index.js'): - runtime_details_dict['language'] = NODE_RUNTIME_NAME - runtime_details_dict['file_loc'] = package_json_file if os.path.isfile(package_json_file) else '' - runtime_details_dict['default_sku'] = LINUX_SKU_DEFAULT - elif package_netcore_file: - runtime_lang = detect_dotnet_lang(package_netcore_file) - runtime_details_dict['language'] = runtime_lang - runtime_details_dict['file_loc'] = package_netcore_file - runtime_details_dict['default_sku'] = 'F1' - else: # TODO: Update the doc when the detection logic gets updated - raise ValidationError("Could not auto-detect the runtime stack of your app, " - "see 'https://go.microsoft.com/fwlink/?linkid=2109470' for more information") - return runtime_details_dict - - -def detect_dotnet_lang(csproj_path): - import xml.etree.ElementTree as ET - import re - parsed_file = ET.parse(csproj_path) - root = parsed_file.getroot() - version_lang = '' - for target_ver in root.iter('TargetFramework'): - version_lang = re.sub(r'([^a-zA-Z\s]+?)', '', target_ver.text) - if 'netcore' in version_lang.lower(): - return NETCORE_RUNTIME_NAME - return DOTNET_RUNTIME_NAME - - -def parse_dotnet_version(file_path): - version_detected = ['4.7'] - try: - from xml.dom import minidom - import re - xmldoc = minidom.parse(file_path) - framework_ver = xmldoc.getElementsByTagName('TargetFrameworkVersion') - target_ver = framework_ver[0].firstChild.data - non_decimal = re.compile(r'[^\d.]+') - # reduce the version to '5.7.4' from '5.7' - if target_ver is not None: - # remove the string from the beginning of the version value - c = non_decimal.sub('', target_ver) - version_detected = c[:3] - except: # pylint: disable=bare-except - version_detected = version_detected[0] - return version_detected - - -def parse_netcore_version(file_path): - import xml.etree.ElementTree as ET - import re - version_detected = ['0.0'] - parsed_file = ET.parse(file_path) - root = parsed_file.getroot() - for target_ver in root.iter('TargetFramework'): - version_detected = re.findall(r"\d+\.\d+", target_ver.text) - # incase of multiple versions detected, return list in descending order - version_detected = sorted(version_detected, key=float, reverse=True) - return version_detected - - -def parse_node_version(file_path): - # from node experts the node value in package.json can be found here "engines": { "node": ">=10.6.0"} - import json - import re - version_detected = [] - with open(file_path) as data_file: - data = json.load(data_file) - for key, value in data.items(): - if key == 'engines' and 'node' in value: - value_detected = value['node'] - non_decimal = re.compile(r'[^\d.]+') - # remove the string ~ or > that sometimes exists in version value - c = non_decimal.sub('', value_detected) - # reduce the version to '6.0' from '6.0.0' - if '.' in c: # handle version set as 4 instead of 4.0 - num_array = c.split('.') - num = num_array[0] + "." + num_array[1] - else: - num = c + ".0" - version_detected.append(num) - return version_detected or ['0.0'] - - -def detect_netcore_version_tocreate(detected_ver): - if detected_ver in NETCORE_VERSIONS: - return detected_ver - return NETCORE_VERSION_DEFAULT - - -def detect_dotnet_version_tocreate(detected_ver): - min_ver = DOTNET_VERSIONS[0] - if detected_ver in DOTNET_VERSIONS: - return detected_ver - if detected_ver < min_ver: - return min_ver - return DOTNET_VERSION_DEFAULT - - -def detect_node_version_tocreate(detected_ver): - if detected_ver in NODE_VERSIONS: - return detected_ver - # get major version & get the closest version from supported list - major_ver = int(detected_ver.split('.')[0]) - node_ver = NODE_VERSION_DEFAULT - if major_ver < 4: - node_ver = NODE_VERSION_DEFAULT - elif 4 <= major_ver < 6: - node_ver = '4.5' - elif 6 <= major_ver < 8: - node_ver = '6.9' - elif 8 <= major_ver < 10: - node_ver = NODE_VERSION_DEFAULT - elif major_ver >= 10: - node_ver = '10.14' - return node_ver - - -def find_key_in_json(json_data, key): - for k, v in json_data.items(): - if key in k: - yield v - elif isinstance(v, dict): - for id_val in find_key_in_json(v, key): - yield id_val - - -def set_location(cmd, sku, location): - client = web_client_factory(cmd.cli_ctx) - if location is None: - locs = client.list_geo_regions(sku, True) - available_locs = [] - for loc in locs: - available_locs.append(loc.name) - loc = available_locs[0] - else: - loc = location - return loc.replace(" ", "").lower() - - -# check if the RG value to use already exists and follows the OS requirements or new RG to be created -def should_create_new_rg(cmd, rg_name, is_linux): - if (_check_resource_group_exists(cmd, rg_name) and - _check_resource_group_supports_os(cmd, rg_name, is_linux)): - return False - return True - - def get_site_availability(cmd, name): """ This is used by az webapp up to verify if a site needs to be created or should just be deployed""" client = web_client_factory(cmd.cli_ctx) @@ -302,14 +24,6 @@ def get_site_availability(cmd, name): return availability -def does_app_already_exist(cmd, name): - """ This is used by az webapp up to verify if a site needs to be created or should just be deployed""" - client = web_client_factory(cmd.cli_ctx) - site_availability = client.check_name_availability(name, 'Microsoft.Web/sites') - # check availability returns true to name_available == site does not exist - return site_availability.name_available - - def get_app_details(cmd, name, resource_group): client = web_client_factory(cmd.cli_ctx) return client.web_apps.get(resource_group, name) @@ -322,64 +36,6 @@ def get_app_details(cmd, name, resource_group): # ''' -def get_rg_to_use(cmd, user, loc, os_name, rg_name=None): - default_rg = "{}_rg_{}_{}".format(user, os_name, loc.replace(" ", "").lower()) - # check if RG exists & can be used - if rg_name is not None and _check_resource_group_exists(cmd, rg_name): - if _check_resource_group_supports_os(cmd, rg_name, os_name.lower() == 'linux'): - return rg_name - raise ArgumentUsageError("The ResourceGroup '{}' cannot be used with the os '{}'. " - "Use a different RG".format(rg_name, os_name)) - if rg_name is None: - rg_name = default_rg - return rg_name - - -def get_profile_username(): - from azure.cli.core._profile import Profile - user = Profile().get_current_account_user() - user = user.split('@', 1)[0] - if len(user.split('#', 1)) > 1: # on cloudShell user is in format live.com#user@domain.com - user = user.split('#', 1)[1] - return user - - -def get_sku_to_use(src_dir, html=False, sku=None): - if sku is None: - lang_details = get_lang_from_content(src_dir, html) - return lang_details.get("default_sku") - logger.info("Found sku argument, skipping use default sku") - return sku - - -def set_language(src_dir, html=False): - lang_details = get_lang_from_content(src_dir, html) - return lang_details.get('language') - - -def detect_os_form_src(src_dir, html=False): - lang_details = get_lang_from_content(src_dir, html) - language = lang_details.get('language') - return "Linux" if language is not None and language.lower() == NODE_RUNTIME_NAME \ - or language.lower() == PYTHON_RUNTIME_NAME else OS_DEFAULT - - -def get_plan_to_use(cmd, user, os_name, loc, sku, create_rg, resource_group_name, plan=None): - _default_asp = "{}_asp_{}_{}_0".format(user, os_name, loc) - if plan is None: # --plan not provided by user - # get the plan name to use - return _determine_if_default_plan_to_use(cmd, _default_asp, resource_group_name, loc, sku, create_rg) - return plan - - -def get_kube_plan_to_use(cmd, kube_environment, loc, sku, create_rg, resource_group_name, plan=None): - _default_asp = "{}_asp_{}_{}_0".format(kube_environment, sku, resource_group_name) - if plan is None: # --plan not provided by user - # get the plan name to use - return _determine_if_default_plan_to_use(cmd, _default_asp, resource_group_name, loc, sku, create_rg) - return plan - - # Portal uses the current_stack property in the app metadata to display the correct stack # This value should be one of: ['dotnet', 'dotnetcore', 'node', 'php', 'python', 'java'] def get_current_stack_from_runtime(runtime): @@ -389,40 +45,6 @@ def get_current_stack_from_runtime(runtime): return language -# if plan name not provided we need to get a plan name based on the OS, location & SKU -def _determine_if_default_plan_to_use(cmd, plan_name, resource_group_name, loc, sku, create_rg): - client = web_client_factory(cmd.cli_ctx) - if create_rg: # if new RG needs to be created use the default name - return plan_name - # get all ASPs in the RG & filter to the ones that contain the plan_name - _asp_generic = plan_name[:-len(plan_name.split("_")[4])] - _asp_list = (list(filter(lambda x: _asp_generic in x.name, - client.app_service_plans.list_by_resource_group(resource_group_name)))) - _num_asp = len(_asp_list) - if _num_asp: - # check if we have at least one app that can be used with the combination of loc, sku & os - selected_asp = next((a for a in _asp_list if isinstance(a.sku, SkuDescription) and - a.sku.name.lower() == sku.lower() and - (a.location.replace(" ", "").lower() == loc.lower())), None) - if selected_asp is not None: - return selected_asp.name - # from the sorted data pick the last one & check if a new ASP needs to be created - # based on SKU or not - data_sorted = sorted(_asp_list, key=lambda x: x.name) - _plan_info = data_sorted[_num_asp - 1] - _asp_num = int(_plan_info.name.split('_')[4]) + 1 # default asp created by CLI can be of type plan_num - return '{}_{}'.format(_asp_generic, _asp_num) - return plan_name - - -def should_create_new_app(cmd, rg_name, app_name): # this is currently referenced by an extension command - client = web_client_factory(cmd.cli_ctx) - for item in list(client.web_apps.list_by_resource_group(rg_name)): - if item.name.lower() == app_name.lower(): - return False - return True - - def generate_default_app_service_plan_name(webapp_name): import uuid random_uuid = str(uuid.uuid4().hex) diff --git a/src/appservice-kube/azext_appservice_kube/_params.py b/src/appservice-kube/azext_appservice_kube/_params.py index 9b8184d33df..a0ab334f57f 100644 --- a/src/appservice-kube/azext_appservice_kube/_params.py +++ b/src/appservice-kube/azext_appservice_kube/_params.py @@ -10,9 +10,9 @@ from azure.cli.core.commands.parameters import (resource_group_name_type, get_location_type, get_resource_name_completion_list, get_three_state_flag, get_enum_type, tags_type) +from azure.cli.command_modules.appservice._constants import FUNCTIONS_VERSIONS -from ._constants import (FUNCTIONS_VERSIONS, FUNCTIONS_VERSION_TO_SUPPORTED_RUNTIME_VERSIONS, - LINUX_RUNTIMES, WINDOWS_RUNTIMES, MULTI_CONTAINER_TYPES, OS_TYPES) +from ._constants import MULTI_CONTAINER_TYPES, OS_TYPES from ._validators import validate_asp_create, validate_timeout_value @@ -28,20 +28,6 @@ def load_arguments(self, _): self.get_models('K8SENetworkPlugin') - # combine all runtime versions for all functions versions - functionapp_runtime_to_version = {} - for functions_version in FUNCTIONS_VERSION_TO_SUPPORTED_RUNTIME_VERSIONS.values(): - for runtime, val in functions_version.items(): - # dotnet version is not configurable, so leave out of help menu - if runtime != 'dotnet': - functionapp_runtime_to_version[runtime] = functionapp_runtime_to_version.get(runtime, set()).union(val) - - functionapp_runtime_to_version_texts = [] - for runtime, runtime_versions in functionapp_runtime_to_version.items(): - runtime_versions_list = list(runtime_versions) - runtime_versions_list.sort(key=float) - functionapp_runtime_to_version_texts.append(runtime + ' -> [' + ', '.join(runtime_versions_list) + ']') - with self.argument_context('webapp') as c: c.ignore('app_instance') c.argument('resource_group_name', arg_type=resource_group_name_type) @@ -98,10 +84,9 @@ def load_arguments(self, _): help='Provide a string value of a Storage Account in the provided Resource Group. Or Resource ID of a Storage Account in a different Resource Group') c.argument('consumption_plan_location', options_list=['--consumption-plan-location', '-c'], help="Geographic location where Function App will be hosted. Use `az functionapp list-consumption-locations` to view available locations.") - c.argument('functions_version', help='The functions app version.', arg_type=get_enum_type(FUNCTIONS_VERSIONS)) - c.argument('runtime', help='The functions runtime stack.', arg_type=get_enum_type(set(LINUX_RUNTIMES).union(set(WINDOWS_RUNTIMES)))) - c.argument('runtime_version', help='The version of the functions runtime stack. ' - 'Allowed values for each --runtime are: ' + ', '.join(functionapp_runtime_to_version_texts)) + c.argument('functions_version', help='The functions app version. Use "az functionapp list-runtimes" to check compatibility with runtimes and runtime versions', arg_type=get_enum_type(FUNCTIONS_VERSIONS)) + c.argument('runtime', help='The functions runtime stack. Use "az functionapp list-runtimes" to check supported runtimes and versions') + c.argument('runtime_version', help='The version of the functions runtime stack. Use "az functionapp list-runtimes" to check supported runtimes and versions') c.argument('os_type', arg_type=get_enum_type(OS_TYPES), help="Set the OS type for the app to be created.") c.argument('app_insights_key', help="Instrumentation key of App Insights to be added.") c.argument('app_insights', help="Name of the existing App Insights project to be added to the Function app. Must be in the same resource group.") diff --git a/src/appservice-kube/azext_appservice_kube/azext_metadata.json b/src/appservice-kube/azext_appservice_kube/azext_metadata.json index 4cc78b7edd0..76cc1bebeb0 100644 --- a/src/appservice-kube/azext_appservice_kube/azext_metadata.json +++ b/src/appservice-kube/azext_appservice_kube/azext_metadata.json @@ -1,5 +1,4 @@ { "azext.isPreview": true, - "azext.minCliCoreVersion": "2.33.0", - "azext.maxCliCoreVersion": "2.33.1" + "azext.minCliCoreVersion": "2.34.0" } diff --git a/src/appservice-kube/azext_appservice_kube/custom.py b/src/appservice-kube/azext_appservice_kube/custom.py index b47aaff0001..9a950bc4efd 100644 --- a/src/appservice-kube/azext_appservice_kube/custom.py +++ b/src/appservice-kube/azext_appservice_kube/custom.py @@ -12,7 +12,7 @@ from knack.util import CLIError from knack.log import get_logger -from azure.cli.core.util import send_raw_request, sdk_no_wait, get_json_object, get_file_json +from azure.cli.core.util import send_raw_request, sdk_no_wait, get_json_object from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.command_modules.appservice.custom import ( update_container_settings, @@ -37,6 +37,7 @@ _validate_and_get_connection_string, _get_linux_multicontainer_encoded_config_from_file, _StackRuntimeHelper, + _FunctionAppStackRuntimeHelper, upload_zip_to_storage, is_plan_consumption, _configure_default_logging, @@ -46,7 +47,7 @@ list_hostnames, _convert_camel_to_snake_case, _get_content_share_name) -from azure.cli.command_modules.appservice._constants import FUNCTIONS_STACKS_API_KEYS, FUNCTIONS_NO_V2_REGIONS +from azure.cli.command_modules.appservice._constants import LINUX_OS_NAME, FUNCTIONS_NO_V2_REGIONS from azure.cli.command_modules.appservice.utils import retryable_method from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.cli.core.commands import LongRunningOperation @@ -57,12 +58,8 @@ from msrestazure.tools import is_valid_resource_id, parse_resource_id -from ._constants import (FUNCTIONS_VERSION_TO_DEFAULT_RUNTIME_VERSION, FUNCTIONS_VERSION_TO_DEFAULT_NODE_VERSION, - FUNCTIONS_VERSION_TO_SUPPORTED_RUNTIME_VERSIONS, NODE_EXACT_VERSION_DEFAULT, - DOTNET_RUNTIME_VERSION_TO_DOTNET_LINUX_FX_VERSION, KUBE_DEFAULT_SKU, - KUBE_ASP_KIND, KUBE_APP_KIND, KUBE_FUNCTION_APP_KIND, KUBE_FUNCTION_CONTAINER_APP_KIND, - KUBE_CONTAINER_APP_KIND, LINUX_RUNTIMES, WINDOWS_RUNTIMES, FUNCTIONS_STACKS_API_JSON_PATHS, - FUNCTIONS_WINDOWS_RUNTIME_VERSION_REGEX, FUNCTIONS_LINUX_RUNTIME_VERSION_REGEX) +from ._constants import (KUBE_DEFAULT_SKU, KUBE_ASP_KIND, KUBE_APP_KIND, KUBE_FUNCTION_APP_KIND, + KUBE_FUNCTION_CONTAINER_APP_KIND, KUBE_CONTAINER_APP_KIND) from ._utils import (_normalize_sku, get_sku_name, _generic_site_operation, _get_location_from_resource_group, _validate_asp_sku) @@ -755,7 +752,6 @@ def create_webapp(cmd, resource_group_name, name, plan=None, runtime=None, custo _validate_asp_sku(app_service_environment=None, custom_location=custom_location, sku=plan_info.sku.name) is_linux = plan_info.reserved - node_default_version = NODE_EXACT_VERSION_DEFAULT location = plan_info.location if isinstance(plan_info.sku, SkuDescription) and plan_info.sku.name.upper() not in ['F1', 'FREE', 'SHARED', 'D1', @@ -799,7 +795,7 @@ def create_webapp(cmd, resource_group_name, name, plan=None, runtime=None, custo site_config.app_settings.append(NameValuePair(name='DOCKER_REGISTRY_SERVER_PASSWORD', value=docker_registry_server_password)) - helper = _StackRuntimeHelper(cmd, client, linux=(is_linux or is_kube)) + helper = _StackRuntimeHelper(cmd, linux=bool(is_linux or is_kube), windows=not bool(is_linux or is_kube)) if runtime: runtime = helper.remove_delimiters(runtime) @@ -814,11 +810,11 @@ def create_webapp(cmd, resource_group_name, name, plan=None, runtime=None, custo if runtime: site_config.linux_fx_version = runtime - match = helper.resolve(runtime) + match = helper.resolve(runtime, linux=True) if not match: raise CLIError("Linux Runtime '{}' is not supported." "Please invoke 'list-runtimes' to cross check".format(runtime)) - match['setter'](cmd=cmd, stack=match, site_config=site_config) + helper.get_site_config_setter(match, linux=True)(cmd=cmd, stack=match, site_config=site_config) elif deployment_container_image_name: site_config.linux_fx_version = _format_fx_version(deployment_container_image_name) if name_validation.name_available: @@ -845,17 +841,18 @@ def create_webapp(cmd, resource_group_name, name, plan=None, runtime=None, custo raise CLIError("usage error: --startup-file or --deployment-container-image-name or " "--multicontainer-config-type and --multicontainer-config-file is " "only appliable on linux webapp") - match = helper.resolve(runtime) + match = helper.resolve(runtime, linux=False) if not match: raise CLIError("Windows runtime '{}' is not supported. " "Please invoke 'az webapp list-runtimes' to cross check".format(runtime)) - match['setter'](cmd=cmd, stack=match, site_config=site_config) + helper.get_site_config_setter(match, linux=is_linux)(cmd=cmd, stack=match, site_config=site_config) # portal uses the current_stack propety in metadata to display stack for windows apps current_stack = get_current_stack_from_runtime(runtime) else: # windows webapp without runtime specified if name_validation.name_available: # If creating new webapp + node_default_version = helper.get_default_version("node", is_linux, get_windows_config_version=True) site_config.app_settings.append(NameValuePair(name="WEBSITE_NODE_DEFAULT_VERSION", value=node_default_version)) @@ -941,6 +938,7 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non logger.warning("No functions version specified so defaulting to 3. In the future, specifying a version will " "be required. To create a 3.x function you would pass in the flag `--functions-version 3`") functions_version = '3' + if deployment_source_url and deployment_local_git: raise MutuallyExclusiveArgumentError('usage error: --deployment-source-url | --deployment-local-git') if not plan and not consumption_plan_location and not custom_location: @@ -956,11 +954,12 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non disable_app_insights = (disable_app_insights == "true") custom_location = _get_custom_location_id(cmd, custom_location, resource_group_name) + site_config = SiteConfig(app_settings=[]) client = web_client_factory(cmd.cli_ctx) functionapp_def = Site(location=None, site_config=site_config, tags=tags) - KEYS = FUNCTIONS_STACKS_API_KEYS() + plan_info = None if runtime is not None: runtime = runtime.lower() @@ -973,7 +972,7 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non functionapp_def.location = consumption_plan_location functionapp_def.kind = 'functionapp' # if os_type is None, the os type is windows - is_linux = os_type and os_type.lower() == 'linux' + is_linux = bool(os_type and os_type.lower() == LINUX_OS_NAME) else: # apps with SKU based plan _should_create_new_plan = _should_create_new_appservice_plan_for_k8se(cmd, @@ -1001,20 +1000,12 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non plan_info = client.app_service_plans.get(resource_group_name, plan) if not plan_info: raise ResourceNotFoundError("The plan '{}' doesn't exist".format(plan)) + location = plan_info.location - is_linux = plan_info.reserved + is_linux = bool(plan_info.reserved) functionapp_def.server_farm_id = plan functionapp_def.location = location - is_kube = _is_function_kube(custom_location, plan_info, SkuDescription) - - if is_kube: - if min_worker_count is not None: - site_config.number_of_workers = min_worker_count - - if max_worker_count is not None: - site_config.app_settings.append(NameValuePair(name='K8SE_APP_MAX_INSTANCE_COUNT', value=max_worker_count)) - if functions_version == '2' and functionapp_def.location in FUNCTIONS_NO_V2_REGIONS: raise ValidationError("2.x functions are not supported in this region. To create a 3.x function, " "pass in the flag '--functions-version 3'") @@ -1023,60 +1014,24 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non raise ArgumentUsageError( "usage error: --runtime RUNTIME required for linux functions apps without custom image.") - runtime_stacks_json = _load_runtime_stacks_json_functionapp(is_linux) - if runtime is None and runtime_version is not None: raise ArgumentUsageError('Must specify --runtime to use --runtime-version') - # get the matching runtime stack object - runtime_json = _get_matching_runtime_json_functionapp(runtime_stacks_json, runtime if runtime else 'dotnet') - if not runtime_json: - # no matching runtime for os - os_string = "linux" if is_linux else "windows" - supported_runtimes = list(map(lambda x: x[KEYS.NAME], runtime_stacks_json)) - raise ValidationError("usage error: Currently supported runtimes (--runtime) in {} function apps are: {}." - .format(os_string, ', '.join(supported_runtimes))) - - runtime_version_json = _get_matching_runtime_version_json_functionapp(runtime_json, - functions_version, - runtime_version, - is_linux) - - if not runtime_version_json: - supported_runtime_versions = list(map(lambda x: x[KEYS.DISPLAY_VERSION], - _get_supported_runtime_versions_functionapp(runtime_json, - functions_version))) - if runtime_version: - if runtime == 'dotnet': - raise ArgumentUsageError('--runtime-version is not supported for --runtime dotnet. Dotnet version is ' - 'determined by --functions-version. Dotnet version {} ' - 'is not supported by Functions version {}.' - .format(runtime_version, functions_version)) - raise ArgumentUsageError('--runtime-version {} is not supported for the selected --runtime {} and ' - '--functions-version {}. Supported versions are: {}.' - .format(runtime_version, - runtime, - functions_version, - ', '.join(supported_runtime_versions))) - - # if runtime_version was not specified, then that runtime is not supported for that functions version - raise ArgumentUsageError('no supported --runtime-version found for the selected --runtime {} and ' - '--functions-version {}' - .format(runtime, functions_version)) - - if runtime == 'dotnet': - logger.warning('--runtime-version is not supported for --runtime dotnet. Dotnet version is determined by ' - '--functions-version. Dotnet version will be %s for this function app.', - runtime_version_json[KEYS.DISPLAY_VERSION]) - - if runtime_version_json[KEYS.IS_DEPRECATED]: - logger.warning('%s version %s has been deprecated. In the future, this version will be unavailable. ' - 'Please update your command to use a more recent version. For a list of supported ' - '--runtime-versions, run \"az functionapp create -h\"', - runtime_json[KEYS.PROPERTIES][KEYS.DISPLAY], runtime_version_json[KEYS.DISPLAY_VERSION]) - - site_config_json = runtime_version_json[KEYS.SITE_CONFIG_DICT] - app_settings_json = runtime_version_json[KEYS.APP_SETTINGS_DICT] + is_kube = _is_function_kube(custom_location, plan_info, SkuDescription) + + runtime_helper = _FunctionAppStackRuntimeHelper(cmd, linux=is_linux, windows=(not is_linux)) + matched_runtime = runtime_helper.resolve("dotnet" if not runtime else runtime, + runtime_version, functions_version, is_linux) + + if is_kube: + if min_worker_count is not None: + site_config.number_of_workers = min_worker_count + + if max_worker_count is not None: + site_config.app_settings.append(NameValuePair(name='K8SE_APP_MAX_INSTANCE_COUNT', value=max_worker_count)) + + site_config_dict = matched_runtime.site_config_dict + app_settings_dict = matched_runtime.app_settings_dict con_string = _validate_and_get_connection_string(cmd.cli_ctx, resource_group_name, storage_account) @@ -1130,11 +1085,11 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non site_config.linux_fx_version = _format_fx_version(deployment_container_image_name) # clear all runtime specific configs and settings - site_config_json = {KEYS.USE_32_BIT_WORKER_PROC: False} - app_settings_json = {} + site_config_dict.use32_bit_worker_process = False + app_settings_dict = {} # ensure that app insights is created if not disabled - runtime_version_json[KEYS.APPLICATION_INSIGHTS] = True + matched_runtime.app_insights = True else: site_config.app_settings.append(NameValuePair(name='WEBSITES_ENABLE_APP_SERVICE_STORAGE', value='true')) @@ -1142,7 +1097,7 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non functionapp_def.kind = 'functionapp' # set site configs - for prop, value in site_config_json.items(): + for prop, value in site_config_dict.as_dict().items(): snake_case_prop = _convert_camel_to_snake_case(prop) setattr(site_config, snake_case_prop, value) @@ -1151,7 +1106,7 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non site_config.linux_fx_version = '' # adding app settings - for app_setting, value in app_settings_json.items(): + for app_setting, value in app_settings_dict.items(): site_config.app_settings.append(NameValuePair(name=app_setting, value=value)) site_config.app_settings.append(NameValuePair(name='FUNCTIONS_EXTENSION_VERSION', @@ -1177,10 +1132,10 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non instrumentation_key = get_app_insights_key(cmd.cli_ctx, resource_group_name, app_insights) site_config.app_settings.append(NameValuePair(name='APPINSIGHTS_INSTRUMENTATIONKEY', value=instrumentation_key)) - elif disable_app_insights or not runtime_version_json[KEYS.APPLICATION_INSIGHTS]: + elif disable_app_insights or not matched_runtime.app_insights: # set up dashboard if no app insights site_config.app_settings.append(NameValuePair(name='AzureWebJobsDashboard', value=con_string)) - elif not disable_app_insights and runtime_version_json[KEYS.APPLICATION_INSIGHTS]: + elif not disable_app_insights and matched_runtime.app_insights: create_app_insights = True poller = client.web_apps.begin_create_or_update(resource_group_name, name, functionapp_def) @@ -1401,38 +1356,12 @@ def _resolve_kube_environment_id(cli_ctx, kube_environment, resource_group_name) name=kube_environment) -def _get_linux_fx_functionapp(functions_version, runtime, runtime_version): - if runtime_version is None: - runtime_version = FUNCTIONS_VERSION_TO_DEFAULT_RUNTIME_VERSION[functions_version][runtime] - if runtime == 'dotnet': - runtime_version = DOTNET_RUNTIME_VERSION_TO_DOTNET_LINUX_FX_VERSION[runtime_version] - else: - runtime = runtime.upper() - return '{}|{}'.format(runtime, runtime_version) - - def _get_linux_fx_kube_functionapp(runtime, runtime_version): if runtime.upper() == "DOTNET": runtime = "DOTNETCORE" return '{}|{}'.format(runtime.upper(), runtime_version) -def _get_website_node_version_functionapp(functions_version, runtime, runtime_version): - if runtime is None or runtime != 'node': - return FUNCTIONS_VERSION_TO_DEFAULT_NODE_VERSION[functions_version] - if runtime_version is not None: - return '~{}'.format(runtime_version) - return FUNCTIONS_VERSION_TO_DEFAULT_NODE_VERSION[functions_version] - - -def _get_java_version_functionapp(functions_version, runtime_version): - if runtime_version is None: - runtime_version = FUNCTIONS_VERSION_TO_DEFAULT_RUNTIME_VERSION[functions_version]['java'] - if runtime_version == '8': - return '1.8' - return runtime_version - - def _set_remote_or_local_git(cmd, webapp, resource_group_name, name, deployment_source_url=None, deployment_source_branch='master', deployment_local_git=None): if deployment_source_url: @@ -1450,17 +1379,6 @@ def _set_remote_or_local_git(cmd, webapp, resource_group_name, name, deployment_ setattr(webapp, 'deploymentLocalGitUrl', local_git_info['url']) -def _add_fx_version(cmd, resource_group_name, name, custom_image_name, slot=None): - fx_version = _format_fx_version(custom_image_name) - web_app = get_webapp(cmd, resource_group_name, name, slot) - if not web_app: - raise CLIError("'{}' app doesn't exist in resource group {}".format(name, resource_group_name)) - linux_fx = fx_version if (web_app.reserved or not web_app.is_xenon) else None - windows_fx = fx_version if web_app.is_xenon else None - return update_site_configs(cmd, resource_group_name, name, - linux_fx_version=linux_fx, windows_fx_version=windows_fx, slot=slot) - - @retryable_method(3, 5) def _get_app_settings_from_scm(cmd, resource_group_name, name, slot=None): scm_url = _get_scm_url(cmd, resource_group_name, name, slot) @@ -1596,8 +1514,8 @@ def enable_zip_deploy_functionapp(cmd, resource_group_name, name, src, build_rem client = web_client_factory(cmd.cli_ctx) app = client.web_apps.get(resource_group_name, name) if app is None: - raise CLIError('The function app \'{}\' was not found in resource group \'{}\'. ' - 'Please make sure these values are correct.'.format(name, resource_group_name)) + raise ResourceNotFoundError('The function app \'{}\' was not found in resource group \'{}\'. ' + 'Please make sure these values are correct.'.format(name, resource_group_name)) parse_plan_id = parse_resource_id(app.server_farm_id) plan_info = None retry_delay = 10 # seconds @@ -1611,7 +1529,7 @@ def enable_zip_deploy_functionapp(cmd, resource_group_name, name, src, build_rem time.sleep(retry_delay) if build_remote and not app.reserved: - raise CLIError('Remote build is only available on Linux function apps') + raise ValidationError('Remote build is only available on Linux function apps') is_consumption = is_plan_consumption(cmd, plan_info) if (not build_remote) and is_consumption and app.reserved: @@ -1835,69 +1753,3 @@ def unbind_ssl_cert(cmd, resource_group_name, name, certificate_thumbprint, slot SslState = cmd.get_models('SslState') return _update_ssl_binding(cmd, resource_group_name, name, certificate_thumbprint, SslState.disabled, slot) - - -def _load_runtime_stacks_json_functionapp(is_linux): - KEYS = FUNCTIONS_STACKS_API_KEYS() - if is_linux: - return get_file_json(FUNCTIONS_STACKS_API_JSON_PATHS['linux'])[KEYS.VALUE] - return get_file_json(FUNCTIONS_STACKS_API_JSON_PATHS['windows'])[KEYS.VALUE] - - -def _get_matching_runtime_json_functionapp(stacks_json, runtime): - KEYS = FUNCTIONS_STACKS_API_KEYS() - matching_runtime_json = list(filter(lambda x: x[KEYS.NAME] == runtime, stacks_json)) - if matching_runtime_json: - return matching_runtime_json[0] - return None - - -def _get_matching_runtime_version_json_functionapp(runtime_json, functions_version, runtime_version, is_linux): - KEYS = FUNCTIONS_STACKS_API_KEYS() - extension_version = _get_extension_version_functionapp(functions_version) - if runtime_version: - for runtime_version_json in runtime_json[KEYS.PROPERTIES][KEYS.MAJOR_VERSIONS]: - if (runtime_version_json[KEYS.DISPLAY_VERSION] == runtime_version and - extension_version in runtime_version_json[KEYS.SUPPORTED_EXTENSION_VERSIONS]): - return runtime_version_json - return None - - # find the matching default runtime version - supported_versions_list = _get_supported_runtime_versions_functionapp(runtime_json, functions_version) - default_version_json = {} - default_version = 0.0 - for current_runtime_version_json in supported_versions_list: - if current_runtime_version_json[KEYS.IS_DEFAULT]: - current_version = _get_runtime_version_functionapp(current_runtime_version_json[KEYS.RUNTIME_VERSION], - is_linux) - if not default_version_json or default_version < current_version: - default_version_json = current_runtime_version_json - default_version = current_version - return default_version_json - - -def _get_supported_runtime_versions_functionapp(runtime_json, functions_version): - KEYS = FUNCTIONS_STACKS_API_KEYS() - extension_version = _get_extension_version_functionapp(functions_version) - supported_versions_list = [] - - for runtime_version_json in runtime_json[KEYS.PROPERTIES][KEYS.MAJOR_VERSIONS]: - if extension_version in runtime_version_json[KEYS.SUPPORTED_EXTENSION_VERSIONS]: - supported_versions_list.append(runtime_version_json) - return supported_versions_list - - -def _get_runtime_version_functionapp(version_string, is_linux): - import re - windows_match = re.fullmatch(FUNCTIONS_WINDOWS_RUNTIME_VERSION_REGEX, version_string) - if windows_match: - return float(windows_match.group(1)) - - linux_match = re.fullmatch(FUNCTIONS_LINUX_RUNTIME_VERSION_REGEX, version_string) - if linux_match: - return float(linux_match.group(1)) - - try: - return float(version_string) - except ValueError: - return 0 diff --git a/src/appservice-kube/setup.py b/src/appservice-kube/setup.py index 71bd2f2124b..9ef132e72cc 100644 --- a/src/appservice-kube/setup.py +++ b/src/appservice-kube/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.1.3' +VERSION = '0.1.4' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers