Skip to content

Commit

Permalink
Adding support for .NETCORE (#61)
Browse files Browse the repository at this point in the history
* Adding support for .NETCORE

Adding support for dotnetcore with windows as default

Fixing parsing node version

Fixing -l codepath that regressed plus updating index.json

Fixing build errors & upating index.json

* build fixes and index.json update
  • Loading branch information
panchagnula authored Feb 14, 2018
1 parent a7362df commit 27233a8
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 72 deletions.
8 changes: 4 additions & 4 deletions src/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
}
}
],
Expand Down
13 changes: 13 additions & 0 deletions src/webapp/azext_webapp/_constants.py
Original file line number Diff line number Diff line change
@@ -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"
124 changes: 105 additions & 19 deletions src/webapp/azext_webapp/create_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, **_):
Expand All @@ -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]
Expand All @@ -29,29 +36,31 @@ 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:]
zf.write(absname, arcname)
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):
Expand All @@ -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

Expand All @@ -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
104 changes: 56 additions & 48 deletions src/webapp/azext_webapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,53 +40,59 @@ 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)
available_locs = []
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)

Expand All @@ -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:
Expand All @@ -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
2 changes: 1 addition & 1 deletion src/webapp/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 27233a8

Please sign in to comment.