diff --git a/src/index.json b/src/index.json index a845e4c55e6..9e074520704 100644 --- a/src/index.json +++ b/src/index.json @@ -370,9 +370,9 @@ ], "webapp": [ { - "filename": "webapp-0.0.7-py2.py3-none-any.whl", - "sha256Digest": "11090b2d19d2082f86249bbf06566bd3924e4c976ac47c95e34ecd82541944c8", - "downloadUrl": "https://github.com/panchagnula/azure-cli-extensions/raw/sisirap-extensions-whl/dist/webapp-0.0.7-py2.py3-none-any.whl", + "filename": "webapp-0.0.8-py2.py3-none-any.whl", + "sha256Digest": "4800c51978f7801b613f93afc62367c6646839dc4fd99c7673f4e217ba96cc58", + "downloadUrl": "https://github.com/panchagnula/azure-cli-extensions/raw/sisirap-extensions-whl/dist/webapp-0.0.8-py2.py3-none-any.whl", "metadata": { "classifiers": [ "Development Status :: 4 - Beta", @@ -409,7 +409,7 @@ "metadata_version": "2.0", "name": "webapp", "summary": "An Azure CLI Extension to manage appservice resources", - "version": "0.0.7" + "version": "0.0.8" } } ], diff --git a/src/webapp/azext_webapp/_constants.py b/src/webapp/azext_webapp/_constants.py new file mode 100644 index 00000000000..f5b0e8c9126 --- /dev/null +++ b/src/webapp/azext_webapp/_constants.py @@ -0,0 +1,13 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +NODE_VERSION_DEFAULT = "8.1" +NETCORE_VERSION_DEFAULT = "2.0" +# TODO: Remove this once we have the api returning the versions +NODE_VERSIONS = ['4.4', '4.5', '6.2', '6.6', '6.9', '6.11', '8.0', '8.1'] +NETCORE_VERSIONS = ['1.0', '1.1', '2.0'] +NODE_RUNTIME_NAME = "node" +NETCORE_RUNTIME_NAME = "dotnetcore" +OS_DEFAULT = "Windows" diff --git a/src/webapp/azext_webapp/create_util.py b/src/webapp/azext_webapp/create_util.py index df5ed2b89c9..1c8cf5f6a77 100644 --- a/src/webapp/azext_webapp/create_util.py +++ b/src/webapp/azext_webapp/create_util.py @@ -7,6 +7,13 @@ import zipfile from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.mgmt.resource.resources.models import ResourceGroup +from ._constants import ( + NETCORE_VERSION_DEFAULT, + NETCORE_VERSIONS, + NODE_VERSION_DEFAULT, + NODE_VERSIONS, + NETCORE_RUNTIME_NAME, + NODE_RUNTIME_NAME) def _resource_client_factory(cli_ctx, **_): @@ -19,7 +26,7 @@ def web_client_factory(cli_ctx, **_): return get_mgmt_service_client(cli_ctx, WebSiteManagementClient) -def zip_contents_from_dir(dirPath): +def zip_contents_from_dir(dirPath, lang): 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] @@ -29,8 +36,13 @@ def zip_contents_from_dir(dirPath): for dirname, subdirs, files in os.walk(dirPath): # skip node_modules folder for Node apps, # since zip_deployment will perfom the build operation - if 'node_modules' in subdirs: + if lang.lower() == NODE_RUNTIME_NAME and 'node_modules' in subdirs: subdirs.remove('node_modules') + elif lang.lower() == NETCORE_RUNTIME_NAME: + if 'bin' in subdirs: + subdirs.remove('bin') + elif 'obj' in subdirs: + subdirs.remove('obj') for filename in files: absname = os.path.abspath(os.path.join(dirname, filename)) arcname = absname[len(abs_src) + 1:] @@ -38,20 +50,17 @@ def zip_contents_from_dir(dirPath): return zip_file_path -def is_node_application(path): - # for node application, package.json should exisit in the application root dir - # if this exists we pass the path of the file to read it contents & get version - package_json_file = os.path.join(path, 'package.json') - if os.path.isfile(package_json_file): - return package_json_file - return '' - - -def get_node_runtime_version_toSet(): - version_val = "8.0" - # trunc_version = float(node_version[:3]) - # TODO: call the list_runtimes once there is an API that returns the supported versions - return version_val +def get_runtime_version_details(file_path, lang_name): + 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() == NODE_RUNTIME_NAME: + version_detected = parse_node_version(file_path)[0] + version_to_create = detect_node_version_tocreate(version_detected) + return {'detected': version_detected, 'to_create': version_to_create} def create_resource_group(cmd, rg_name, location): @@ -65,13 +74,15 @@ def check_resource_group_exists(cmd, rg_name): return rcf.resource_groups.check_existence(rg_name) -def check_resource_group_supports_linux(cmd, rg_name, location): +def check_resource_group_supports_os(cmd, rg_name, location, 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)) - # filter by location & reserverd=false for item in plans: - if item.location == location and not item.reserved: + # for Linux if an app with reserved==False exists, ASP doesn't support Linux + if is_linux and item.location == location and not item.reserved: + return False + elif not is_linux and item.location == location and item.reserved: return False return True @@ -91,3 +102,78 @@ def check_app_exists(cmd, rg_name, app_name): if item.name == app_name: return True return False + + +def get_lang_from_content(src_path): + # NODE: package.json should exisit in the application root dir + # NETCORE: NETCORE.csproj should exist in the root dir + runtime_details_dict = dict.fromkeys(['language', 'file_loc', 'default_sku']) + package_json_file = os.path.join(src_path, 'package.json') + package_netcore_file = os.path.join(src_path, 'netcore.csproj') + if os.path.isfile(package_json_file): + runtime_details_dict['language'] = NODE_RUNTIME_NAME + runtime_details_dict['file_loc'] = package_json_file + runtime_details_dict['default_sku'] = 'S1' + elif os.path.isfile(package_netcore_file): + runtime_details_dict['language'] = NETCORE_RUNTIME_NAME + runtime_details_dict['file_loc'] = package_netcore_file + runtime_details_dict['default_sku'] = 'F1' + return runtime_details_dict + + +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): + import json + import re + version_detected = ['0.0'] + with open(file_path) as data_file: + data = [] + for d in find_key_in_json(json.load(data_file), 'node'): + non_decimal = re.compile(r'[^\d.]+') + # remove the string ~ or > that sometimes exists in version value + c = non_decimal.sub('', d) + # reduce the version to '6.0' from '6.0.0' + data.append(c[:3]) + version_detected = sorted(data, key=float, reverse=True) + return version_detected + + +def detect_netcore_version_tocreate(detected_ver): + if detected_ver in NETCORE_VERSIONS: + return detected_ver + return NETCORE_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 = float(detected_ver.split('.')[0]) + if major_ver < 4: + return NODE_VERSION_DEFAULT + elif major_ver >= 4 and major_ver < 6: + return '4.5' + elif major_ver >= 6 and major_ver < 8: + return '6.9' + return NODE_VERSION_DEFAULT + + +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 diff --git a/src/webapp/azext_webapp/custom.py b/src/webapp/azext_webapp/custom.py index 08a2c74afb1..b20db39aee0 100644 --- a/src/webapp/azext_webapp/custom.py +++ b/src/webapp/azext_webapp/custom.py @@ -18,16 +18,18 @@ from .create_util import ( zip_contents_from_dir, - is_node_application, - get_node_runtime_version_toSet, + get_runtime_version_details, create_resource_group, check_resource_group_exists, - check_resource_group_supports_linux, + check_resource_group_supports_os, check_if_asp_exists, check_app_exists, + get_lang_from_content, web_client_factory ) +from ._constants import (NODE_RUNTIME_NAME, OS_DEFAULT) + logger = get_logger(__name__) # pylint:disable=no-member,too-many-lines,too-many-locals,too-many-statements @@ -38,10 +40,31 @@ def create_deploy_webapp(cmd, name, location=None, dryrun=False): import json client = web_client_factory(cmd.cli_ctx) - sku = "S1" - os_val = "Linux" - language = "node" - full_sku = _get_sku_name(sku) + # the code to deploy is expected to be the current directory the command is running from + src_dir = os.getcwd() + + # if dir is empty, show a message in dry run + do_deployment = False if os.listdir(src_dir) == [] else True + + # determine the details for app to be created from src contents + lang_details = get_lang_from_content(src_dir) + # we support E2E create and deploy for Node & dotnetcore, any other stack, set defaults for os & runtime + # and skip deployment + if lang_details['language'] is None: + do_deployment = False + sku = 'F1' + os_val = OS_DEFAULT + detected_version = '-' + runtime_version = '-' + else: + sku = lang_details.get("default_sku") + language = lang_details.get("language") + os_val = "Linux" if language.lower() == NODE_RUNTIME_NAME else OS_DEFAULT + # detect the version + data = get_runtime_version_details(lang_details.get('file_loc'), language) + version_used_create = data.get('to_create') + detected_version = data.get('detected') + runtime_version = "{}|{}".format(language, version_used_create) if location is None: locs = client.list_geo_regions(sku, True) @@ -49,42 +72,27 @@ def create_deploy_webapp(cmd, name, location=None, dryrun=False): for loc in locs: available_locs.append(loc.geo_region_name) location = available_locs[0] - # Remove spaces from the location string, incase the GeoRegion string is used loc_name = location.replace(" ", "") + full_sku = _get_sku_name(sku) + + is_linux = True if os_val == 'Linux' else False asp = "appsvc_asp_{}_{}".format(os_val, loc_name) rg_name = "appsvc_rg_{}_{}".format(os_val, loc_name) - # the code to deploy is expected to be the current directory the command is running from - src_dir = os.getcwd() - - # if dir is empty, show a message in dry run - do_deployment = False if os.listdir(src_dir) == [] else True - package_json_path = is_node_application(src_dir) - str_no_contents_warn = "" if not do_deployment: str_no_contents_warn = "[Empty directory, no deployment will be triggered]" - if package_json_path == '': - node_version = "[No package.json file found in root directory, not a Node app?]" - version_used_create = "8.0" - else: - with open(package_json_path) as data_file: - data = json.load(data_file) - node_version = data['version'] - version_used_create = get_node_runtime_version_toSet() - # Resource group: check if default RG is set default_rg = cmd.cli_ctx.config.get('defaults', 'group', fallback=None) - if default_rg and check_resource_group_supports_linux(cmd, default_rg, location): + if default_rg and check_resource_group_supports_os(cmd, default_rg, location, is_linux): rg_name = default_rg rg_mssg = "[Using default Resource group]" else: rg_mssg = "" - runtime_version = "{}|{}".format(language, version_used_create) src_path = "{} {}".format(src_dir.replace("\\", "\\\\"), str_no_contents_warn) rg_str = "{} {}".format(rg_name, rg_mssg) @@ -100,7 +108,7 @@ def create_deploy_webapp(cmd, name, location=None, dryrun=False): "version_to_create": "%s" } """ % (name, asp, rg_str, full_sku, os_val, location, src_path, - node_version, runtime_version) + detected_version, runtime_version) create_json = json.dumps(json.loads(dry_run_str), indent=4, sort_keys=True) if dryrun: @@ -120,46 +128,46 @@ def create_deploy_webapp(cmd, name, location=None, dryrun=False): # create asp if not check_if_asp_exists(cmd, rg_name, asp): logger.warning("Creating App service plan '%s' ...", asp) - sku_def = SkuDescription(tier=full_sku, name=sku, capacity=1) + sku_def = SkuDescription(tier=full_sku, name=sku, capacity=(1 if is_linux else None)) plan_def = AppServicePlan(loc_name, app_service_plan_name=asp, - sku=sku_def, reserved=True) + sku=sku_def, reserved=(is_linux or None)) client.app_service_plans.create_or_update(rg_name, asp, plan_def) logger.warning("App service plan creation complete") else: logger.warning("App service plan '%s' already exists.", asp) - # create the Linux app + # create the app if not check_app_exists(cmd, rg_name, name): logger.warning("Creating app '%s' ....", name) - create_webapp(cmd, rg_name, name, asp, runtime_version) + create_webapp(cmd, rg_name, name, asp, runtime_version if is_linux else None) logger.warning("Webapp creation complete") else: logger.warning("App '%s' already exists", name) - # setting to build after deployment - logger.warning("Updating app settings to enable build after deployment") - update_app_settings(cmd, rg_name, name, ["SCM_DO_BUILD_DURING_DEPLOYMENT=true"]) - # work around until the timeout limits issue for linux is investigated & fixed - # wakeup kudu, by making an SCM call - - import requests - # work around until the timeout limits issue for linux is investigated & fixed - user_name, password = _get_site_credential(cmd.cli_ctx, rg_name, name) - scm_url = _get_scm_url(cmd, rg_name, name) - import urllib3 - authorization = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(user_name, password)) - requests.get(scm_url + '/api/settings', headers=authorization) - - if package_json_path != '': + if do_deployment: + # setting to build after deployment + logger.warning("Updating app settings to enable build after deployment") + update_app_settings(cmd, rg_name, name, ["SCM_DO_BUILD_DURING_DEPLOYMENT=true"]) + # work around until the timeout limits issue for linux is investigated & fixed + # wakeup kudu, by making an SCM call + + import requests + # work around until the timeout limits issue for linux is investigated & fixed + user_name, password = _get_site_credential(cmd.cli_ctx, rg_name, name) + scm_url = _get_scm_url(cmd, rg_name, name) + import urllib3 + authorization = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(user_name, password)) + requests.get(scm_url + '/api/settings', headers=authorization) + logger.warning("Creating zip with contents of dir %s ...", src_dir) # zip contents & deploy - zip_file_path = zip_contents_from_dir(src_dir) + zip_file_path = zip_contents_from_dir(src_dir, language) logger.warning("Deploying and building contents to app." "This operation can take some time to finish...") enable_zip_deploy(cmd, rg_name, name, zip_file_path) else: - logger.warning("No package.json found, skipping zip and deploy process") + logger.warning("No 'NODE' or 'DOTNETCORE' package detected, skipping zip and deploy process") logger.warning("All done. %s", create_json) return None diff --git a/src/webapp/setup.py b/src/webapp/setup.py index bedccd9e4da..ab027cbe258 100644 --- a/src/webapp/setup.py +++ b/src/webapp/setup.py @@ -8,7 +8,7 @@ from codecs import open from setuptools import setup, find_packages -VERSION = "0.0.7" +VERSION = "0.0.8" CLASSIFIERS = [ 'Development Status :: 4 - Beta',