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

[functionapp] Add support for v3 function apps and node 12. #11987

Merged
merged 3 commits into from Feb 13, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/azure-cli/HISTORY.rst
Expand Up @@ -33,6 +33,10 @@ Release History

* Azure Stack: surface commands under the profile of 2019-03-01-hybrid
* functionapp: Add ability to create Java function apps in Linux
* functionapp: Added --functions-version property to 'az functionapp create'
* functionapp: Added support for node 12 for v3 function apps
* functionapp: Added support for python 3.8 for v3 function apps
* functionapp: Changed python default version to 3.7 for v2 and v3 function apps

**ARM**

Expand Down
71 changes: 52 additions & 19 deletions src/azure-cli/azure/cli/command_modules/appservice/_constants.py
Expand Up @@ -4,7 +4,6 @@
# --------------------------------------------------------------------------------------------

NODE_VERSION_DEFAULT = "10.14"
NODE_VERSION_DEFAULT_FUNCTIONAPP = "~10"
NETCORE_VERSION_DEFAULT = "2.2"
DOTNET_VERSION_DEFAULT = "4.7"
PYTHON_VERSION_DEFAULT = "3.7"
Expand All @@ -19,26 +18,60 @@
NETCORE_VERSIONS = ['1.0', '1.1', '2.1', '2.2']
DOTNET_VERSIONS = ['3.5', '4.7']
LINUX_SKU_DEFAULT = "P1V2"
RUNTIME_TO_DEFAULT_VERSION = {
'node': '8',
'dotnet': '2',
'python': '3.6',
'java': '8'
FUNCTIONS_VERSIONS_FUNCTIONAPP = ['2', '3']
# functions version : default node version
NODE_VERSION_DEFAULT_FUNCTIONAPP = {
'2': '~10',
'3': '~12'
}

RUNTIME_TO_IMAGE_FUNCTIONAPP = {
'node': {
'8': 'mcr.microsoft.com/azure-functions/node:2.0-node8-appservice',
'10': 'mcr.microsoft.com/azure-functions/node:2.0-node10-appservice'
},
'python': {
'3.6': 'mcr.microsoft.com/azure-functions/python:2.0-python3.6-appservice',
'3.7': 'mcr.microsoft.com/azure-functions/python:2.0-python3.7-appservice'
# functions version -> runtime : default runtime version
RUNTIME_TO_DEFAULT_VERSION_FUNCTIONAPP = {
'2': {
'node': '8',
'dotnet': '2',
'python': '3.7',
'java': '8'
},
'dotnet': {
'2': 'mcr.microsoft.com/azure-functions/dotnet:2.0-appservice'
'3': {
'node': '12',
'dotnet': '3',
'python': '3.7',
'java': '8'
}
}
# functions version -> runtime -> runtime version : container image
RUNTIME_TO_IMAGE_FUNCTIONAPP = {
'2': {
'node': {
'8': 'mcr.microsoft.com/azure-functions/node:2.0-node8-appservice',
'10': 'mcr.microsoft.com/azure-functions/node:2.0-node10-appservice'
},
'python': {
'3.6': 'mcr.microsoft.com/azure-functions/python:2.0-python3.6-appservice',
'3.7': 'mcr.microsoft.com/azure-functions/python:2.0-python3.7-appservice'
},
'dotnet': {
'2': 'mcr.microsoft.com/azure-functions/dotnet:2.0-appservice'
},
'java': {
'8': 'mcr.microsoft.com/azure-functions/java:2.0-java8-appservice'
}
},
'java': {
'8': 'mcr.microsoft.com/azure-functions/java:2.0-java8-appservice'
'3': {
'node': {
'10': 'mcr.microsoft.com/azure-functions/node:3.0-node10-appservice',
'12': 'mcr.microsoft.com/azure-functions/node:3.0-node12-appservice'
},
'python': {
'3.6': 'mcr.microsoft.com/azure-functions/python:3.0-python3.6-appservice',
'3.7': 'mcr.microsoft.com/azure-functions/python:3.0-python3.7-appservice',
'3.8': 'mcr.microsoft.com/azure-functions/python:3.0-python3.8-appservice'
},
'dotnet': {
'3': 'mcr.microsoft.com/azure-functions/dotnet:3.0-appservice'
},
'java': {
'8': 'mcr.microsoft.com/azure-functions/java:3.0-java8-appservice'
}
}
}
15 changes: 12 additions & 3 deletions src/azure-cli/azure/cli/command_modules/appservice/_params.py
Expand Up @@ -13,7 +13,7 @@
from azure.mgmt.web.models import DatabaseType, ConnectionStringType, BuiltInAuthenticationProvider, AzureStorageType

from ._completers import get_hostname_completion_list
from ._constants import RUNTIME_TO_IMAGE_FUNCTIONAPP
from ._constants import FUNCTIONS_VERSIONS_FUNCTIONAPP, RUNTIME_TO_IMAGE_FUNCTIONAPP
from ._validators import (validate_timeout_value, validate_site_create, validate_asp_create,
validate_add_vnet, validate_front_end_scale_factor, validate_ase_create)

Expand Down Expand Up @@ -50,9 +50,17 @@ def load_arguments(self, _):
isolated_sku_arg_type = CLIArgumentType(help='The Isolated pricing tiers, e.g., I1 (Isolated Small), I2 (Isolated Medium), I3 (Isolated Large)',
arg_type=get_enum_type(['I1', 'I2', 'I3']))

# combine all runtime versions for all functions versions
functionapp_runtime_to_version = {}
for functions_version in RUNTIME_TO_IMAGE_FUNCTIONAPP.values():
for runtime, val in functions_version.items():
functionapp_runtime_to_version[runtime] = functionapp_runtime_to_version.get(runtime, set()).union(val.keys())

functionapp_runtime_to_version_texts = []
for runtime, val in RUNTIME_TO_IMAGE_FUNCTIONAPP.items():
functionapp_runtime_to_version_texts.append(runtime + ' -> [' + ', '.join(val.keys()) + ']')
for runtime, runtime_versions in functionapp_runtime_to_version.items():
runtime_versions_list = list(runtime_versions)
runtime_versions_list.sort(key=float)
Comment on lines +61 to +62
Copy link
Contributor

Choose a reason for hiding this comment

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

The set will perform auto sort, we don't need to do this explicitly.
The next line ', '.join will iterate through the set. So we may not need these two lines.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah so this was a bit frustrating because if I just join the set, it'll print out node versions as "8, 12, 10" for some reason. I can't sort the set so if I turn it into a list and sort it, it'll print "10, 12, 8" because 1 comes before 8. Setting the key to float will sort them as if they're floats and behave how we would want.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, I see, yea, that makes sense. We can keep them here.

functionapp_runtime_to_version_texts.append(runtime + ' -> [' + ', '.join(runtime_versions_list) + ']')

# use this hidden arg to give a command the right instance, that functionapp commands
# work on function app and webapp ones work on web app
Expand Down Expand Up @@ -460,6 +468,7 @@ 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_FUNCTIONAPP))
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))
Copy link
Member

Choose a reason for hiding this comment

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

Can we put all available versions here and delete --version? What's the difference between version 2 and 3 when choosing same runtime and runtime_version?

Copy link
Member Author

Choose a reason for hiding this comment

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

runtime_version is the version of runtime language you're using (node, python, etc...) whereas version is the version of the functions host you want to use (You can make a Functions v2 App or v3 App). The two aren't related other than each functions host version only supports a subset of runtime versions. We recently GA'd functions host v3 (more info here) so we need to allow customers a choice for their host version.

Copy link
Member

Choose a reason for hiding this comment

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

To chime in...it took me a minute to figure this out when I was first ramping up. Maybe we scrub docs to ensure that we clearly describe the differences.

Copy link
Member

Choose a reason for hiding this comment

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

I see. Thanks for the detailed explanation.

Expand Down
55 changes: 34 additions & 21 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Expand Up @@ -57,7 +57,7 @@
should_create_new_rg, set_location, does_app_already_exist, get_profile_username,
get_plan_to_use, get_lang_from_content, get_rg_to_use, get_sku_to_use,
detect_os_form_src)
from ._constants import (RUNTIME_TO_DEFAULT_VERSION, NODE_VERSION_DEFAULT_FUNCTIONAPP,
from ._constants import (RUNTIME_TO_DEFAULT_VERSION_FUNCTIONAPP, NODE_VERSION_DEFAULT_FUNCTIONAPP,
RUNTIME_TO_IMAGE_FUNCTIONAPP, NODE_VERSION_DEFAULT)

logger = get_logger(__name__)
Expand Down Expand Up @@ -2301,12 +2301,17 @@ def validate_range_of_int_flag(flag_name, value, min_val, max_val):


def create_function(cmd, resource_group_name, name, storage_account, plan=None,
os_type=None, runtime=None, runtime_version=None, consumption_plan_location=None,
app_insights=None, app_insights_key=None, disable_app_insights=None, deployment_source_url=None,
os_type=None, functions_version=None, runtime=None, runtime_version=None,
consumption_plan_location=None, app_insights=None, app_insights_key=None,
disable_app_insights=None, deployment_source_url=None,
deployment_source_branch='master', deployment_local_git=None,
docker_registry_server_password=None, docker_registry_server_user=None,
deployment_container_image_name=None, tags=None):
# pylint: disable=too-many-statements, too-many-branches
if functions_version is None:
logger.warning("No functions version specified so defaulting to 2. In the future, specifying a version will "
"be required. To create a 2.x function you would pass in the flag `--functions_version 2`")
functions_version = '2'
if deployment_source_url and deployment_local_git:
raise CLIError('usage error: --deployment-source-url <url> | --deployment-local-git')
if bool(plan) == bool(consumption_plan_location):
Expand Down Expand Up @@ -2360,22 +2365,19 @@ def create_function(cmd, resource_group_name, name, storage_account, plan=None,
if runtime_version is not None:
if runtime is None:
raise CLIError('Must specify --runtime to use --runtime-version')
allowed_versions = RUNTIME_TO_IMAGE_FUNCTIONAPP[runtime].keys()
allowed_versions = RUNTIME_TO_IMAGE_FUNCTIONAPP[functions_version][runtime].keys()
if runtime_version not in allowed_versions:
raise CLIError('--runtime-version {} is not supported for the selected --runtime {}. '
'Supported versions are: {}'
.format(runtime_version, runtime, ', '.join(allowed_versions)))
raise CLIError('--runtime-version {} is not supported for the selected --runtime {} and '
'--functions_version {}. Supported versions are: {}'
.format(runtime_version, runtime, functions_version, ', '.join(allowed_versions)))

con_string = _validate_and_get_connection_string(cmd.cli_ctx, resource_group_name, storage_account)

if is_linux:
functionapp_def.kind = 'functionapp,linux'
functionapp_def.reserved = True
is_consumption = consumption_plan_location is not None
if is_consumption:
site_config.app_settings.append(NameValuePair(name='FUNCTIONS_EXTENSION_VERSION', value='~2'))
else:
site_config.app_settings.append(NameValuePair(name='FUNCTIONS_EXTENSION_VERSION', value='~2'))
if not is_consumption:
site_config.app_settings.append(NameValuePair(name='MACHINEKEY_DecryptionKey',
value=str(hexlify(urandom(32)).decode()).upper()))
if deployment_container_image_name:
Expand All @@ -2389,18 +2391,23 @@ def create_function(cmd, resource_group_name, name, storage_account, plan=None,
else:
site_config.app_settings.append(NameValuePair(name='WEBSITES_ENABLE_APP_SERVICE_STORAGE',
value='true'))
if runtime not in RUNTIME_TO_IMAGE_FUNCTIONAPP.keys():
if runtime not in RUNTIME_TO_IMAGE_FUNCTIONAPP[functions_version].keys():
raise CLIError("An appropriate linux image for runtime:'{}' was not found".format(runtime))
if deployment_container_image_name is None:
site_config.linux_fx_version = _get_linux_fx_functionapp(is_consumption, runtime, runtime_version)
site_config.linux_fx_version = _get_linux_fx_functionapp(is_consumption,
functions_version,
runtime,
runtime_version)
else:
functionapp_def.kind = 'functionapp'
site_config.app_settings.append(NameValuePair(name='FUNCTIONS_EXTENSION_VERSION', value='~2'))
# adding appsetting to site to make it a function
site_config.app_settings.append(NameValuePair(name='FUNCTIONS_EXTENSION_VERSION',
value=_get_extension_version_functionapp(functions_version)))
site_config.app_settings.append(NameValuePair(name='AzureWebJobsStorage', value=con_string))
site_config.app_settings.append(NameValuePair(name='AzureWebJobsDashboard', value=con_string))
site_config.app_settings.append(NameValuePair(name='WEBSITE_NODE_DEFAULT_VERSION',
value=_get_website_node_version_functionapp(runtime,
value=_get_website_node_version_functionapp(functions_version,
runtime,
runtime_version)))

# If plan is not consumption or elastic premium, we need to set always on
Expand Down Expand Up @@ -2452,21 +2459,27 @@ def create_function(cmd, resource_group_name, name, storage_account, plan=None,
return functionapp


def _get_linux_fx_functionapp(is_consumption, runtime, runtime_version):
def _get_extension_version_functionapp(functions_version):
if functions_version is not None:
return '~{}'.format(functions_version)
return '~2'


def _get_linux_fx_functionapp(is_consumption, functions_version, runtime, runtime_version):
if runtime_version is None:
runtime_version = RUNTIME_TO_DEFAULT_VERSION[runtime]
runtime_version = RUNTIME_TO_DEFAULT_VERSION_FUNCTIONAPP[functions_version][runtime]
if is_consumption:
return '{}|{}'.format(runtime.upper(), runtime_version)
# App service or Elastic Premium
return _format_fx_version(RUNTIME_TO_IMAGE_FUNCTIONAPP[runtime][runtime_version])
return _format_fx_version(RUNTIME_TO_IMAGE_FUNCTIONAPP[functions_version][runtime][runtime_version])


def _get_website_node_version_functionapp(runtime, runtime_version):
def _get_website_node_version_functionapp(functions_version, runtime, runtime_version):
if runtime is None or runtime != 'node':
return NODE_VERSION_DEFAULT_FUNCTIONAPP
return NODE_VERSION_DEFAULT_FUNCTIONAPP[functions_version]
if runtime_version is not None:
return '~{}'.format(runtime_version)
return NODE_VERSION_DEFAULT_FUNCTIONAPP
return NODE_VERSION_DEFAULT_FUNCTIONAPP[functions_version]


def try_create_application_insights(cmd, functionapp):
Expand Down