diff --git a/.github/workflows/ci_docker_con_workflow.yml b/.github/workflows/ci_docker_con_workflow.yml new file mode 100644 index 000000000..b4ea128ff --- /dev/null +++ b/.github/workflows/ci_docker_con_workflow.yml @@ -0,0 +1,90 @@ +# This workflow will run all tests in endtoend/tests in a docker container using the latest consumption image + +name: CI Docker Consumption tests + +on: + workflow_dispatch: + schedule: + # Monday to Thursday 1 AM PDT build + # * is a special character in YAML so you have to quote this string + - cron: "0 8 * * 1,2,3,4" + +jobs: + build: + name: "Python Docker CI Run" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ 3.7, 3.8, 3.9, "3.10" ] + env: + CONSUMPTION_DOCKER_TEST: "true" + + steps: + - name: Checkout code. + uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U -e .[dev] + python setup.py build + python setup.py webhost --branch-name=dev + python setup.py extension + - name: Running 3.7 Tests + if: matrix.python-version == 3.7 + env: + AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString37 }} + AzureWebJobsCosmosDBConnectionString: ${{ secrets.LinuxCosmosDBConnectionString37 }} + AzureWebJobsEventHubConnectionString: ${{ secrets.LinuxEventHubConnectionString37 }} + AzureWebJobsServiceBusConnectionString: ${{ secrets.LinuxServiceBusConnectionString37 }} + AzureWebJobsSqlConnectionString: ${{ secrets.LinuxSqlConnectionString37 }} + AzureWebJobsEventGridTopicUri: ${{ secrets.LinuxEventGridTopicUriString37 }} + AzureWebJobsEventGridConnectionKey: ${{ secrets.LinuxEventGridConnectionKeyString37 }} + run: | + python -m pytest -n auto --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend + - name: Running 3.8 Tests + if: matrix.python-version == 3.8 + env: + AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString38 }} + AzureWebJobsCosmosDBConnectionString: ${{ secrets.LinuxCosmosDBConnectionString38 }} + AzureWebJobsEventHubConnectionString: ${{ secrets.LinuxEventHubConnectionString38 }} + AzureWebJobsServiceBusConnectionString: ${{ secrets.LinuxServiceBusConnectionString38 }} + AzureWebJobsSqlConnectionString: ${{ secrets.LinuxSqlConnectionString38 }} + AzureWebJobsEventGridTopicUri: ${{ secrets.LinuxEventGridTopicUriString38 }} + AzureWebJobsEventGridConnectionKey: ${{ secrets.LinuxEventGridConnectionKeyString38 }} + run: | + python -m pytest -n auto --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend + - name: Running 3.9 Tests + if: matrix.python-version == 3.9 + env: + AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString39 }} + AzureWebJobsCosmosDBConnectionString: ${{ secrets.LinuxCosmosDBConnectionString39 }} + AzureWebJobsEventHubConnectionString: ${{ secrets.LinuxEventHubConnectionString39 }} + AzureWebJobsServiceBusConnectionString: ${{ secrets.LinuxServiceBusConnectionString39 }} + AzureWebJobsSqlConnectionString: ${{ secrets.LinuxSqlConnectionString39 }} + AzureWebJobsEventGridTopicUri: ${{ secrets.LinuxEventGridTopicUriString39 }} + AzureWebJobsEventGridConnectionKey: ${{ secrets.LinuxEventGridConnectionKeyString39 }} + run: | + python -m pytest -n auto --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend + - name: Running 3.10 Tests + if: matrix.python-version == 3.10 + env: + AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString310 }} + AzureWebJobsCosmosDBConnectionString: ${{ secrets.LinuxCosmosDBConnectionString310 }} + AzureWebJobsEventHubConnectionString: ${{ secrets.LinuxEventHubConnectionString310 }} + AzureWebJobsServiceBusConnectionString: ${{ secrets.LinuxServiceBusConnectionString310 }} + AzureWebJobsSqlConnectionString: ${{ secrets.LinuxSqlConnectionString310 }} + AzureWebJobsEventGridTopicUri: ${{ secrets.LinuxEventGridTopicUriString310 }} + AzureWebJobsEventGridConnectionKey: ${{ secrets.LinuxEventGridConnectionKeyString310 }} + run: | + python -m pytest -n auto --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend + - name: Codecov + uses: codecov/codecov-action@v1.0.13 + with: + file: ./coverage.xml # optional + flags: unittests # optional + name: codecov # optional + fail_ci_if_error: false # optional (default = false) diff --git a/.github/workflows/ci_docker_ded_workflow.yml b/.github/workflows/ci_docker_ded_workflow.yml new file mode 100644 index 000000000..5bcdb6aa7 --- /dev/null +++ b/.github/workflows/ci_docker_ded_workflow.yml @@ -0,0 +1,91 @@ +# This workflow will run all tests in endtoend/tests in a docker container using the latest dedicated image + + +name: CI Docker Dedicated tests + +on: + workflow_dispatch: + schedule: + # Monday to Thursday 1 AM PDT build + # * is a special character in YAML so you have to quote this string + - cron: "0 8 * * 1,2,3,4" + +jobs: + build: + name: "Python Docker CI Run" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ 3.7, 3.8, 3.9, "3.10" ] + env: + DEDICATED_DOCKER_TEST: "true" + + steps: + - name: Checkout code. + uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U -e .[dev] + python setup.py build + python setup.py webhost --branch-name=dev + python setup.py extension + - name: Running 3.7 Tests + if: matrix.python-version == 3.7 + env: + AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString37 }} + AzureWebJobsCosmosDBConnectionString: ${{ secrets.LinuxCosmosDBConnectionString37 }} + AzureWebJobsEventHubConnectionString: ${{ secrets.LinuxEventHubConnectionString37 }} + AzureWebJobsServiceBusConnectionString: ${{ secrets.LinuxServiceBusConnectionString37 }} + AzureWebJobsSqlConnectionString: ${{ secrets.LinuxSqlConnectionString37 }} + AzureWebJobsEventGridTopicUri: ${{ secrets.LinuxEventGridTopicUriString37 }} + AzureWebJobsEventGridConnectionKey: ${{ secrets.LinuxEventGridConnectionKeyString37 }} + run: | + python -m pytest -n auto --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend + - name: Running 3.8 Tests + if: matrix.python-version == 3.8 + env: + AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString38 }} + AzureWebJobsCosmosDBConnectionString: ${{ secrets.LinuxCosmosDBConnectionString38 }} + AzureWebJobsEventHubConnectionString: ${{ secrets.LinuxEventHubConnectionString38 }} + AzureWebJobsServiceBusConnectionString: ${{ secrets.LinuxServiceBusConnectionString38 }} + AzureWebJobsSqlConnectionString: ${{ secrets.LinuxSqlConnectionString38 }} + AzureWebJobsEventGridTopicUri: ${{ secrets.LinuxEventGridTopicUriString38 }} + AzureWebJobsEventGridConnectionKey: ${{ secrets.LinuxEventGridConnectionKeyString38 }} + run: | + python -m pytest -n auto --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend + - name: Running 3.9 Tests + if: matrix.python-version == 3.9 + env: + AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString39 }} + AzureWebJobsCosmosDBConnectionString: ${{ secrets.LinuxCosmosDBConnectionString39 }} + AzureWebJobsEventHubConnectionString: ${{ secrets.LinuxEventHubConnectionString39 }} + AzureWebJobsServiceBusConnectionString: ${{ secrets.LinuxServiceBusConnectionString39 }} + AzureWebJobsSqlConnectionString: ${{ secrets.LinuxSqlConnectionString39 }} + AzureWebJobsEventGridTopicUri: ${{ secrets.LinuxEventGridTopicUriString39 }} + AzureWebJobsEventGridConnectionKey: ${{ secrets.LinuxEventGridConnectionKeyString39 }} + run: | + python -m pytest -n auto --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend + - name: Running 3.10 Tests + if: matrix.python-version == 3.10 + env: + AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString310 }} + AzureWebJobsCosmosDBConnectionString: ${{ secrets.LinuxCosmosDBConnectionString310 }} + AzureWebJobsEventHubConnectionString: ${{ secrets.LinuxEventHubConnectionString310 }} + AzureWebJobsServiceBusConnectionString: ${{ secrets.LinuxServiceBusConnectionString310 }} + AzureWebJobsSqlConnectionString: ${{ secrets.LinuxSqlConnectionString310 }} + AzureWebJobsEventGridTopicUri: ${{ secrets.LinuxEventGridTopicUriString310 }} + AzureWebJobsEventGridConnectionKey: ${{ secrets.LinuxEventGridConnectionKeyString310 }} + run: | + python -m pytest -n auto --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch --cov-append tests/endtoend + - name: Codecov + uses: codecov/codecov-action@v1.0.13 + with: + file: ./coverage.xml # optional + flags: unittests # optional + name: codecov # optional + fail_ci_if_error: false # optional (default = false) diff --git a/azure_functions_worker/constants.py b/azure_functions_worker/constants.py index 9c2be2709..86a9dd59b 100644 --- a/azure_functions_worker/constants.py +++ b/azure_functions_worker/constants.py @@ -12,9 +12,6 @@ SHARED_MEMORY_DATA_TRANSFER = "SharedMemoryDataTransfer" FUNCTION_DATA_CACHE = "FunctionDataCache" -# Debug Flags -PYAZURE_WEBHOST_DEBUG = "PYAZURE_WEBHOST_DEBUG" - # Platform Environment Variables AZURE_WEBJOBS_SCRIPT_ROOT = "AzureWebJobsScriptRoot" CONTAINER_NAME = "CONTAINER_NAME" diff --git a/tests/endtoend/dependency_isolation_functions/report_dependencies/function.json b/tests/endtoend/dependency_isolation_functions/report_dependencies/function.json index ed123d425..c76954425 100644 --- a/tests/endtoend/dependency_isolation_functions/report_dependencies/function.json +++ b/tests/endtoend/dependency_isolation_functions/report_dependencies/function.json @@ -2,7 +2,7 @@ "scriptFile": "__init__.py", "bindings": [ { - "authLevel": "function", + "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req", diff --git a/tests/endtoend/http_functions/default_template/function.json b/tests/endtoend/http_functions/default_template/function.json index b8dc650e9..8c4cbe307 100644 --- a/tests/endtoend/http_functions/default_template/function.json +++ b/tests/endtoend/http_functions/default_template/function.json @@ -2,7 +2,7 @@ "scriptFile": "__init__.py", "bindings": [ { - "authLevel": "function", + "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req", diff --git a/tests/endtoend/sql_functions/sql_input/function.json b/tests/endtoend/sql_functions/sql_input/function.json index 1c5e9d552..87b67886e 100644 --- a/tests/endtoend/sql_functions/sql_input/function.json +++ b/tests/endtoend/sql_functions/sql_input/function.json @@ -2,7 +2,7 @@ "scriptFile": "__init__.py", "bindings": [ { - "authLevel": "function", + "authLevel": "anonymous", "name": "req", "type": "httpTrigger", "direction": "in", diff --git a/tests/endtoend/sql_functions/sql_output/function.json b/tests/endtoend/sql_functions/sql_output/function.json index e58c18cfd..44ede8421 100644 --- a/tests/endtoend/sql_functions/sql_output/function.json +++ b/tests/endtoend/sql_functions/sql_output/function.json @@ -2,7 +2,7 @@ "scriptFile": "__init__.py", "bindings": [ { - "authLevel": "function", + "authLevel": "anonymous", "name": "req", "type": "httpTrigger", "direction": "in", diff --git a/tests/utils/constants.py b/tests/utils/constants.py index c5ebc11a7..12b7047fc 100644 --- a/tests/utils/constants.py +++ b/tests/utils/constants.py @@ -5,7 +5,16 @@ # PROJECT_ROOT refers to the path to azure-functions-python-worker # TODO: Find root folder without .parent PROJECT_ROOT = pathlib.Path(__file__).parent.parent.parent +TESTS_ROOT = PROJECT_ROOT / 'tests' +WORKER_CONFIG = PROJECT_ROOT / '.testconfig' # E2E Integration Flags and Configurations PYAZURE_INTEGRATION_TEST = "PYAZURE_INTEGRATION_TEST" PYAZURE_WORKER_DIR = "PYAZURE_WORKER_DIR" + +# Debug Flags +PYAZURE_WEBHOST_DEBUG = "PYAZURE_WEBHOST_DEBUG" + +# CI test constants +CONSUMPTION_DOCKER_TEST = "false" +DEDICATED_DOCKER_TEST = "false" diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py index d36f6fae8..13d8870a7 100644 --- a/tests/utils/testutils.py +++ b/tests/utils/testutils.py @@ -42,13 +42,14 @@ from azure_functions_worker.bindings.shared_memory_data_transfer \ import SharedMemoryConstants as consts from azure_functions_worker.constants import ( - PYAZURE_WEBHOST_DEBUG, FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED, UNIX_SHARED_MEMORY_DIRECTORIES ) from azure_functions_worker.utils.common import is_envvar_true, get_app_setting from tests.utils.constants import PYAZURE_WORKER_DIR, \ - PYAZURE_INTEGRATION_TEST, PROJECT_ROOT + PYAZURE_INTEGRATION_TEST, PROJECT_ROOT, WORKER_CONFIG, \ + CONSUMPTION_DOCKER_TEST, DEDICATED_DOCKER_TEST, PYAZURE_WEBHOST_DEBUG +from tests.utils.testutils_docker import WebHostConsumption TESTS_ROOT = PROJECT_ROOT / 'tests' E2E_TESTS_FOLDER = pathlib.Path('endtoend') @@ -62,7 +63,6 @@ EXTENSIONS_PATH = PROJECT_ROOT / 'build' / 'extensions' / 'bin' FUNCS_PATH = TESTS_ROOT / UNIT_TESTS_FOLDER / 'http_functions' WORKER_PATH = PROJECT_ROOT / 'python' / 'test' -WORKER_CONFIG = PROJECT_ROOT / '.testconfig' ON_WINDOWS = platform.system() == 'Windows' LOCALHOST = "127.0.0.1" @@ -221,8 +221,13 @@ def setUpClass(cls): _setup_func_app(TESTS_ROOT / script_dir) try: - cls.webhost = start_webhost(script_dir=script_dir, - stdout=cls.host_stdout) + if is_envvar_true(CONSUMPTION_DOCKER_TEST): + cls.webhost = WebHostConsumption(script_dir).spawn_container() + elif is_envvar_true(DEDICATED_DOCKER_TEST): + cls.webhost = WebHostConsumption(script_dir).spawn_container() + else: + cls.webhost = start_webhost(script_dir=script_dir, + stdout=cls.host_stdout) except Exception: _teardown_func_app(TESTS_ROOT / script_dir) raise @@ -917,6 +922,7 @@ def start_webhost(*, script_dir=None, stdout=None): stdout = subprocess.DEVNULL port = _find_open_port() + proc = popen_webhost(stdout=stdout, stderr=subprocess.STDOUT, script_root=script_root, port=port) time.sleep(10) # Giving host some time to start fully. diff --git a/tests/utils/testutils_docker.py b/tests/utils/testutils_docker.py new file mode 100644 index 000000000..96f66f71d --- /dev/null +++ b/tests/utils/testutils_docker.py @@ -0,0 +1,180 @@ +import configparser +import os +import re +import subprocess +import sys +import unittest +import uuid +from time import sleep + +import requests + +from tests.utils.constants import PROJECT_ROOT, TESTS_ROOT, WORKER_CONFIG + +_DOCKER_PATH = "DOCKER_PATH" +_DOCKER_DEFAULT_PATH = "docker" +_HOST_VERSION = "4" +_docker_cmd = os.getenv(_DOCKER_PATH, _DOCKER_DEFAULT_PATH) +_addr = "" +_python_version = f'{sys.version_info.major}.{sys.version_info.minor}' +_uuid = str(uuid.uuid4()) +_MESH_IMAGE_URL = "https://mcr.microsoft.com/v2/azure-functions/mesh/tags/list" +_MESH_IMAGE_REPO = "mcr.microsoft.com/azure-functions/mesh" +_IMAGE_URL = "https://mcr.microsoft.com/v2/azure-functions/python/tags/list" +_IMAGE_REPO = "mcr.microsoft.com/azure-functions/python" + + +class WebHostProxy: + + def __init__(self, proc, addr): + self._proc = proc + self._addr = addr + + def request(self, meth, funcname, *args, **kwargs): + request_method = getattr(requests, meth.lower()) + params = dict(kwargs.pop('params', {})) + no_prefix = kwargs.pop('no_prefix', False) + + return request_method( + self._addr + ('/' if no_prefix else '/api/') + funcname, + *args, params=params, **kwargs) + + def close(self) -> bool: + """Kill a container by its name. Returns True on success. + """ + kill_cmd = [_docker_cmd, "rm", "-f", _uuid] + kill_process = subprocess.run(args=kill_cmd, stdout=subprocess.DEVNULL) + exit_code = kill_process.returncode + + return exit_code == 0 + + +class WebHostDockerContainerBase(unittest.TestCase): + + def find_latest_mesh_image(self, + image_repo: str, + image_url: str) -> str: + + regex = re.compile(_HOST_VERSION + r'.\d+.\d+-python' + _python_version) + + response = requests.get(image_url, allow_redirects=True) + if not response.ok: + raise RuntimeError(f'Failed to query latest image for v4' + f' Python {_python_version}.' + f' Status {response.status_code}') + + tag_list = response.json().get('tags', []) + # Removing images with a -upgrade and -slim. Upgrade images were + # temporary images used to onboard customers from a previous version. + # These images are no longer used. + tag_list = [x.strip("-upgrade") for x in tag_list] + tag_list = [x.strip("-slim") for x in tag_list] + + # Listing all the versions from the tags with suffix -python + python_versions = list(filter(regex.match, tag_list)) + + # sorting all the python versions based on the runtime version and + # getting the latest released runtime version for python. + latest_version = sorted(python_versions, key=lambda x: float( + x.split(_HOST_VERSION + '.')[-1].split("-python")[0]))[-1] + + image_tag = f'{image_repo}:{latest_version}' + return image_tag + + def create_container(self, image_repo: str, image_url: str, + script_path: str): + """Create a docker container and record its port. Create a docker + container according to the image name. Return the port of container. + """ + + worker_path = os.path.join(PROJECT_ROOT, 'azure_functions_worker') + script_path = os.path.join(TESTS_ROOT, script_path) + + image = self.find_latest_mesh_image(image_repo, image_url) + + container_worker_path = ( + f"/azure-functions-host/workers/python/{_python_version}/" + "LINUX/X64/azure_functions_worker" + ) + + function_path = "/home/site/wwwroot" + + env = {"FUNCTIONS_EXTENSION_VERSION": "4", + "FUNCTIONS_WORKER_RUNTIME": "python", + "FUNCTIONS_WORKER_RUNTIME_VERSION": _python_version, + "WEBSITE_SITE_NAME": _uuid, + "WEBSITE_HOSTNAME": f"{_uuid}.azurewebsites.com"} + + testconfig = None + storage_key: str = os.getenv("AzureWebJobsStorage") + if not storage_key and WORKER_CONFIG.exists(): + testconfig = configparser.ConfigParser() + testconfig.read(WORKER_CONFIG) + + if testconfig and 'azure' in testconfig: + storage_key = testconfig['azure'].get('storage_key') + + if not storage_key: + raise RuntimeError('Environment variable AzureWebJobsStorage ' + 'is required before running docker test') + + run_cmd = [] + run_cmd.extend([_docker_cmd, "run", "-p", "0:80", "-d"]) + run_cmd.extend(["--name", _uuid, "--privileged"]) + run_cmd.extend(["--cap-add", "SYS_ADMIN"]) + run_cmd.extend(["--device", "/dev/fuse"]) + run_cmd.extend(["-e", f"CONTAINER_NAME={_uuid}"]) + run_cmd.extend(["-e", f"AzureWebJobsStorage={storage_key}"]) + run_cmd.extend(["-v", f'{worker_path}:{container_worker_path}']) + run_cmd.extend(["-v", f'{script_path}:{function_path}']) + + run_cmd.append(image) + + run_process = subprocess.run(args=run_cmd, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + if run_process.returncode != 0: + raise RuntimeError('Failed to spawn docker container for' + f' {image} with uuid {_uuid}.' + f' stderr: {run_process.stderr}') + + # Wait for six seconds for the port to expose + sleep(6) + + # Acquire the port number of the container + port_cmd = [_docker_cmd, "port", _uuid] + port_process = subprocess.run(args=port_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + if port_process.returncode != 0: + raise RuntimeError(f'Failed to acquire port for {_uuid}.' + f' stderr: {port_process.stderr}') + port_number = port_process.stdout.decode().strip('\n').split(':')[-1] + + # Wait for six seconds for the container to be in ready state + sleep(10) + self._addr = f'http://localhost:{port_number}' + return WebHostProxy(run_process, self._addr) + + +class WebHostConsumption(WebHostDockerContainerBase): + + def __init__(self, script_path: str): + self.script_path = script_path + + def spawn_container(self): + return self.create_container(_MESH_IMAGE_REPO, + _MESH_IMAGE_URL, + self.script_path) + + +class WebHostDedicated(WebHostDockerContainerBase): + + def __init__(self, script_path: str): + self.script_path = script_path + + def spawn_container(self): + return self.create_container(_IMAGE_REPO, _IMAGE_URL, + self.script_path)