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 webapp extension for quickstart command #42

Merged
merged 10 commits into from
Feb 2, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
32 changes: 32 additions & 0 deletions src/webapps/azext_webapps/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core import AzCommandsLoader
import azext_webapps._help # pylint: disable=unused-import


class WebappsCommandLoader(AzCommandsLoader):
Copy link
Member

Choose a reason for hiding this comment

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

Would recommend WebappsExtCommandLoader instead.
The azure-cli-appservice module has the name AppserviceCommandsLoader and just want to make sure that the class names don't conflict in the future. By adding Ext, this should be clear enough.


def __init__(self, cli_ctx=None):
from azure.cli.core.commands import CliCommandType
webapps_custom = CliCommandType(
operations_tmpl='azext_webapps.custom#{}')
super(WebappsCommandLoader, self).__init__(cli_ctx=cli_ctx,
custom_command_type=webapps_custom)
Copy link
Member

Choose a reason for hiding this comment

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


def load_command_table(self, _):
with self.command_group('webapp') as g:
g.custom_command('quickstart', 'create_deploy_webapp')
return self.command_table

def load_arguments(self, _):
with self.argument_context('webapp quickstart') as c:
c.argument('name', options_list=['--name', '-n'], help='name of the new webapp')
c.argument('dryrun',
help="shows summary of the create operation instead of actually creating and deploying the app",
default=False)
Copy link
Member

Choose a reason for hiding this comment

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

Add action='store_true'



COMMAND_LOADER_CLS = WebappsCommandLoader
16 changes: 16 additions & 0 deletions src/webapps/azext_webapps/_help.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from knack.help_files import helps


helps['webapp quickstart'] = """
type: command
short-summary: Create and deploy a node web app
examples:
- name: Create a web app with the default configuration.
text: >
az webapp quickstart -n MyUniqueAppName
"""
3 changes: 3 additions & 0 deletions src/webapps/azext_webapps/azext_metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"azext.minCliCoreVersion": "2.0.24"
}
85 changes: 85 additions & 0 deletions src/webapps/azext_webapps/create_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import os
import zipfile
from azure.cli.core.commands.client_factory import get_mgmt_service_client
from azure.mgmt.resource.resources.models import ResourceGroup


def _resource_client_factory(cli_ctx, **_):
from azure.cli.core.profiles import ResourceType
return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES)


def web_client_factory(cli_ctx, **_):
from azure.mgmt.web import WebSiteManagementClient
return get_mgmt_service_client(cli_ctx, WebSiteManagementClient)


def zip_contents_from_dir(dirPath):
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 + "\\" + 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 perfom the build operation
if 'node_modules' in subdirs:
subdirs.remove('node_modules')
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 returs the supported versions
Copy link
Member

Choose a reason for hiding this comment

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

nit typo: returs

return version_val


def create_resource_group(cmd, rg_name, location):
rcf = _resource_client_factory(cmd.cli_ctx)
rg_params = ResourceGroup(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_linux(cmd, rg_name, location):
# 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:
return False
return True


def check_if_asp_exists(cmd, rg_name, asp_name):
# get all appservice plans from RG
client = web_client_factory(cmd.cli_ctx)
for item in list(client.app_service_plans.list_by_resource_group(rg_name)):
if item.name == asp_name:
return True
return False
144 changes: 144 additions & 0 deletions src/webapps/azext_webapps/custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from __future__ import print_function
from knack.log import get_logger

from azure.mgmt.web.models import (AppServicePlan, SkuDescription)

from azure.cli.command_modules.appservice.custom import (
enable_zip_deploy,
create_webapp,
update_app_settings,
_get_sku_name)

from .create_util import (
zip_contents_from_dir,
is_node_application,
get_node_runtime_version_toSet,
create_resource_group,
check_resource_group_exists,
check_resource_group_supports_linux,
check_if_asp_exists,
web_client_factory
)

logger = get_logger(__name__)

# pylint:disable=no-member,too-many-lines,too-many-locals,too-many-statements


def create_deploy_webapp(cmd, name, location=None, dryrun=False):
import os
import json

client = web_client_factory(cmd.cli_ctx)
sku = "S1"
os_val = "Linux"
language = "node"
full_sku = _get_sku_name(sku)

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(" ", "")

asp = "appsvc_asp_{}_{}".format(os_val, loc_name)
rg = "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()

# ResourceGroup: check if default RG is set
default_rg = cmd.cli_ctx.config.get('defaults', 'group')
if (default_rg and check_resource_group_supports_linux(cmd, default_rg, location)):
rg = default_rg
rg_mssg = "[Using default ResourceGroup]"
else:
rg_mssg = ""

runtime_version = "{}|{}".format(language, version_used_create)
src_path = "{} {}".format(src_dir.replace("\\", "\\\\"), str_no_contents_warn)
rg_str = "{} {}".format(rg, rg_mssg)

dry_run_str = r""" {
"name" : "%s",
"appServicePlan" : "%s",
"resourceGroup" : "%s",
"sku": "%s",
"os": "%s",
"location" : "%s",
"src_path" : "%s",
"version_detected": "%s",
"version_to_create": "%s"
}
""" % (name, asp, rg_str, full_sku, os_val, location, src_path,
node_version, runtime_version)

create_json = json.dumps(json.loads(dry_run_str), indent=4, sort_keys=True)
if dryrun:
logger.warning("""
Web app will be created with the below configuration,
re-run command without the --dryrun flag to create & deploy a new app
""")
return logger.warning(create_json)
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need to return the logger warning?

logger.warning(create_json)
return None


# create RG if the RG doesn't already exist
if not check_resource_group_exists(cmd, rg):
logger.warning("Creating ResourceGroup '%s' ...", rg)
create_resource_group(cmd, rg, location)
logger.warning("ResourceGroup creation complete")
else:
logger.warning("Resource Group '%s' already exists.", rg)
Copy link
Member

Choose a reason for hiding this comment

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

Would suggest Resource group instead of ResourceGroup and Resource Group in the log messages.


# create asp
if not check_if_asp_exists(cmd, rg, asp):
logger.warning("Creating App service plan '%s' ...", asp)
sku_def = SkuDescription(tier=full_sku, name=sku, capacity=1)
plan_def = AppServicePlan(loc_name, app_service_plan_name=asp,
sku=sku_def, reserved=True)
client.app_service_plans.create_or_update(rg, asp, plan_def)
logger.warning("AppServicePlan creation complete")
else:
logger.warning("AppServicePlan '%s' already exists.", asp)
Copy link
Member

Choose a reason for hiding this comment

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

Same here App service plan seems better.


# create the Linux app
logger.warning("Creating app '%s' ....", name)
create_webapp(cmd, rg, name, asp, runtime_version)
logger.warning("Webapp creation complete")

# setting to build after deployment
logger.warning("Updating app settings to enable build after deployment")
update_app_settings(cmd, rg, name, ["SCM_DO_BUILD_DURING_DEPLOYMENT=true"])

# zip contents & deploy
logger.warning("Creating zip with contents of dir %s ...", src_dir)
zip_file_path = zip_contents_from_dir(src_dir)

logger.warning("Deploying and building contents to app. This operation can take some time to finish...")
enable_zip_deploy(cmd, rg, name, zip_file_path)
return logger.warning("All done. %s", create_json)
Copy link
Member

Choose a reason for hiding this comment

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

You don't need to return with logger.warning.
Just leave it and by default Python will return None or explicitly add return None at the end.

2 changes: 2 additions & 0 deletions src/webapps/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[bdist_wheel]
universal=1
41 changes: 41 additions & 0 deletions src/webapps/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env python

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from codecs import open
from setuptools import setup, find_packages

VERSION = "0.0.1"

CLASSIFIERS = [
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'License :: OSI Approved :: MIT License',
]

DEPENDENCIES = []

setup(
name='webapps',
version=VERSION,
description='An Azure CLI Extension to manage appservice resources',
long_description='An Azure CLI Extension to manage appservice resources',
license='MIT',
author='Sisira Panchagnula',
author_email='sisirap@microsoft.com',
url='https://github.com/Azure/azure-cli-extensions',
classifiers=CLASSIFIERS,
packages=find_packages(exclude=["tests"]),
install_requires=DEPENDENCIES
)