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

Adding support for .NETCORE #61

Merged
merged 2 commits into from
Feb 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: exisit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:) thanks @derekbekoe . i will have this fixed in my next PR

# 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')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hang on. This only works if the project is NAME netcore.csproj. It will be *.csproj, not a specific name! This can't ever work, I'm sorry.

When you type "dotnet new razor" you will get a new project file named after the current folder. @panchagnula

Copy link

@shanselman shanselman Feb 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try:

import glob
os.path.join(src_path, glob.glob("*.csproj")[0])

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