From 0cce4e2e930aa76c4a2db5790d3cbad710576716 Mon Sep 17 00:00:00 2001 From: Sriram Madapusi Vasudevan Date: Thu, 15 Nov 2018 13:44:26 -0800 Subject: [PATCH 1/4] fix: validate runtime - Based on the language supplied and lambda runtimes, validate the host runtime to make sure they are compatible. --- aws_lambda_builders/builder.py | 15 ++++++ aws_lambda_builders/exceptions.py | 5 ++ aws_lambda_builders/runtime.py | 49 +++++++++++++++++++ .../workflows/python_pip/runtime_validator.py | 16 ++++++ tests/functional/test_cli.py | 1 - .../workflows/python_pip/test_python_pip.py | 5 ++ tests/unit/test_builder.py | 14 +++--- tests/unit/test_runtime.py | 40 +++++++++++++++ .../unit/workflows/python_pip/test_actions.py | 1 - .../python_pip/test_runtime_validator.py | 12 +++++ 10 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 aws_lambda_builders/runtime.py create mode 100644 aws_lambda_builders/workflows/python_pip/runtime_validator.py create mode 100644 tests/unit/test_runtime.py create mode 100644 tests/unit/workflows/python_pip/test_runtime_validator.py diff --git a/aws_lambda_builders/builder.py b/aws_lambda_builders/builder.py index 96a6437a2..2207f6639 100644 --- a/aws_lambda_builders/builder.py +++ b/aws_lambda_builders/builder.py @@ -6,6 +6,7 @@ import logging from aws_lambda_builders.registry import get_workflow, DEFAULT_REGISTRY +from aws_lambda_builders.runtime import Runtime from aws_lambda_builders.workflow import Capability LOG = logging.getLogger(__name__) @@ -89,6 +90,9 @@ def build(self, source_dir, artifacts_dir, scratch_dir, manifest_path, :param options: Optional dictionary of options ot pass to build action. **Not supported**. """ + if runtime: + self._validate_runtime(runtime) + workflow = self.selected_workflow_cls(source_dir, artifacts_dir, @@ -100,5 +104,16 @@ def build(self, source_dir, artifacts_dir, scratch_dir, manifest_path, return workflow.run() + def _validate_runtime(self, runtime): + """ + validate runtime and local runtime version to make sure they match + + :type runtime: str + :param runtime: + String matching a lambda runtime eg: python3.6 + """ + Runtime.validate_runtime(required_language=self.capability.language, + required_runtime=runtime) + def _clear_workflows(self): DEFAULT_REGISTRY.clear() diff --git a/aws_lambda_builders/exceptions.py b/aws_lambda_builders/exceptions.py index ca473039d..ba21fea3f 100644 --- a/aws_lambda_builders/exceptions.py +++ b/aws_lambda_builders/exceptions.py @@ -15,6 +15,11 @@ class UnsupportedManifestError(LambdaBuilderError): MESSAGE = "A builder for the given capabilities '{capabilities}' was not found" +class MisMatchRuntimeError(LambdaBuilderError): + MESSAGE = "A runtime version mismatch was found for the given language " \ + "'{language}', required runtime '{required_runtime}'" + + class WorkflowNotFoundError(LambdaBuilderError): """ Raised when a workflow matching the given capabilities was not found diff --git a/aws_lambda_builders/runtime.py b/aws_lambda_builders/runtime.py new file mode 100644 index 000000000..75ddb412d --- /dev/null +++ b/aws_lambda_builders/runtime.py @@ -0,0 +1,49 @@ +""" +Supported Runtimes and their validations. +""" + +import os +import subprocess +from enum import Enum + +from aws_lambda_builders.exceptions import MisMatchRuntimeError +from aws_lambda_builders.workflows.python_pip.runtime_validator import validate_python_cmd + +_RUNTIME_VERSION_RESOLVER = { + "python": validate_python_cmd +} + + +class Runtime(Enum): + python27 = "python2.7" + python36 = "python3.6" + + @classmethod + def has_value(cls, value): + """ + Checks if the enum has this value + :param string value: Value to check + :return bool: True, if enum has the value + """ + return any(value == item.value for item in cls) + + @classmethod + def validate_runtime(cls, required_language, required_runtime): + """ + Checks if the language supplied matches the required lambda runtime + :param string required_language: language to check eg: python + :param string required_runtime: runtime to check eg: python3.6 + :raises ValueError: Unsupported Lambda Runtime + :raises MisMatchRuntimeError: Version mismatch of the lanugage vs the required runtime + """ + if not Runtime.has_value(required_runtime): + raise ValueError("Unsupported Lambda runtime {}".format(required_runtime)) + + cmd = _RUNTIME_VERSION_RESOLVER[required_language](required_language, required_runtime) + p = subprocess.Popen(cmd, + cwd=os.getcwd(), + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p.communicate() + if p.returncode != 0: + raise MisMatchRuntimeError(language=required_language, + required_runtime=required_runtime) diff --git a/aws_lambda_builders/workflows/python_pip/runtime_validator.py b/aws_lambda_builders/workflows/python_pip/runtime_validator.py new file mode 100644 index 000000000..57d2a98f0 --- /dev/null +++ b/aws_lambda_builders/workflows/python_pip/runtime_validator.py @@ -0,0 +1,16 @@ +""" +Validation of Python Runtime Version +""" + + +def validate_python_cmd(required_language, required_runtime_version): + major, minor = required_runtime_version.replace(required_language, "").split('.') + cmd = [ + "python", + "-c", + "import sys; " + "assert sys.version_info.major == {major} " + "and sys.version_info.minor == {minor}".format( + major=major, + minor=minor)] + return cmd diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py index 9038b667f..6fcbcfff6 100644 --- a/tests/functional/test_cli.py +++ b/tests/functional/test_cli.py @@ -53,7 +53,6 @@ def test_run_hello_workflow(self): "artifacts_dir": self.artifacts_dir, "scratch_dir": "/ignored", "manifest_path": "/ignored", - "runtime": "ignored", "optimizations": {}, "options": {}, } diff --git a/tests/integration/workflows/python_pip/test_python_pip.py b/tests/integration/workflows/python_pip/test_python_pip.py index c9807db05..13e4139f3 100644 --- a/tests/integration/workflows/python_pip/test_python_pip.py +++ b/tests/integration/workflows/python_pip/test_python_pip.py @@ -41,6 +41,11 @@ def test_must_build_python_project(self): output_files = set(os.listdir(self.artifacts_dir)) self.assertEquals(expected_files, output_files) + def test_runtime_validate_python_project(self): + with self.assertRaises(ValueError): + self.builder.build(self.source_dir, self.artifacts_dir, None, self.manifest_path_valid, + runtime="python2.8") + def test_must_fail_to_resolve_dependencies(self): with self.assertRaises(WorkflowFailedError) as ctx: diff --git a/tests/unit/test_builder.py b/tests/unit/test_builder.py index cd735236d..965f7bcce 100644 --- a/tests/unit/test_builder.py +++ b/tests/unit/test_builder.py @@ -102,17 +102,17 @@ def setUp(self): @patch('aws_lambda_builders.builder.importlib') @patch('aws_lambda_builders.builder.get_workflow') def test_with_mocks(self, get_workflow_mock, importlib_mock): - workflow_cls = Mock() workflow_instance = workflow_cls.return_value = Mock() get_workflow_mock.return_value = workflow_cls - builder = LambdaBuilder(self.lang, self.lang_framework, self.app_framework, supported_workflows=[]) + with patch.object(LambdaBuilder, "_validate_runtime"): + builder = LambdaBuilder(self.lang, self.lang_framework, self.app_framework, supported_workflows=[]) - builder.build("source_dir", "artifacts_dir", "scratch_dir", "manifest_path", - runtime="runtime", optimizations="optimizations", options="options") + builder.build("source_dir", "artifacts_dir", "scratch_dir", "manifest_path", + runtime="runtime", optimizations="optimizations", options="options") - workflow_cls.assert_called_with("source_dir", "artifacts_dir", "scratch_dir", "manifest_path", - runtime="runtime", optimizations="optimizations", options="options") - workflow_instance.run.assert_called_once() + workflow_cls.assert_called_with("source_dir", "artifacts_dir", "scratch_dir", "manifest_path", + runtime="runtime", optimizations="optimizations", options="options") + workflow_instance.run.assert_called_once() diff --git a/tests/unit/test_runtime.py b/tests/unit/test_runtime.py new file mode 100644 index 000000000..928ac8108 --- /dev/null +++ b/tests/unit/test_runtime.py @@ -0,0 +1,40 @@ +from unittest import TestCase + +import mock + +from aws_lambda_builders.exceptions import MisMatchRuntimeError +from aws_lambda_builders.runtime import Runtime + + +class MockSubProcess(object): + + def __init__(self, returncode): + self.returncode = returncode + + def communicate(self): + pass + + +class TestRuntime(TestCase): + + def test_runtime_enums(self): + self.assertTrue(Runtime.has_value("python2.7")) + self.assertTrue(Runtime.has_value("python3.6")) + self.assertFalse(Runtime.has_value("test_language")) + + def test_runtime_validate_unsupported_runtime(self): + with self.assertRaises(ValueError): + Runtime.validate_runtime("test_language", "test_language2.7") + + def test_runtime_validate_supported_version_runtime(self): + with mock.patch('subprocess.Popen') as mock_subprocess: + mock_subprocess.return_value = MockSubProcess(0) + Runtime.validate_runtime("python", "python3.6") + self.assertTrue(mock_subprocess.call_count, 1) + + def test_runtime_validate_mismatch_version_runtime(self): + with mock.patch('subprocess.Popen') as mock_subprocess: + mock_subprocess.return_value = MockSubProcess(1) + with self.assertRaises(MisMatchRuntimeError): + Runtime.validate_runtime("python", "python2.7") + self.assertTrue(mock_subprocess.call_count, 1) diff --git a/tests/unit/workflows/python_pip/test_actions.py b/tests/unit/workflows/python_pip/test_actions.py index bf2d1e1f8..7450a12d1 100644 --- a/tests/unit/workflows/python_pip/test_actions.py +++ b/tests/unit/workflows/python_pip/test_actions.py @@ -28,4 +28,3 @@ def test_must_raise_exception_on_failure(self, PythonPipDependencyBuilderMock): with self.assertRaises(ActionFailedError): action.execute() - diff --git a/tests/unit/workflows/python_pip/test_runtime_validator.py b/tests/unit/workflows/python_pip/test_runtime_validator.py new file mode 100644 index 000000000..43c963198 --- /dev/null +++ b/tests/unit/workflows/python_pip/test_runtime_validator.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from aws_lambda_builders.workflows.python_pip.runtime_validator import validate_python_cmd + + +class TestPythonRuntimeValidator(TestCase): + + def test_python_command(self): + cmd = validate_python_cmd("python", "python2.7") + version_strings = ["sys.version_info.major == 2", "sys.version_info.minor == 7"] + for version_string in version_strings: + self.assertTrue(any([part for part in cmd if version_string in part])) From ad30656ce01f544d89c5800774e96fc4df60a3b7 Mon Sep 17 00:00:00 2001 From: Sriram Madapusi Vasudevan Date: Thu, 15 Nov 2018 15:51:27 -0800 Subject: [PATCH 2/4] fix: functional tests - move validation out of language specific modules --- aws_lambda_builders/runtime.py | 15 ++++++++++++++- .../workflows/python_pip/runtime_validator.py | 16 ---------------- tests/functional/test_cli.py | 2 ++ tests/unit/test_runtime.py | 7 +++++++ .../python_pip/test_runtime_validator.py | 12 ------------ 5 files changed, 23 insertions(+), 29 deletions(-) delete mode 100644 aws_lambda_builders/workflows/python_pip/runtime_validator.py delete mode 100644 tests/unit/workflows/python_pip/test_runtime_validator.py diff --git a/aws_lambda_builders/runtime.py b/aws_lambda_builders/runtime.py index 75ddb412d..95a30e9c2 100644 --- a/aws_lambda_builders/runtime.py +++ b/aws_lambda_builders/runtime.py @@ -7,7 +7,20 @@ from enum import Enum from aws_lambda_builders.exceptions import MisMatchRuntimeError -from aws_lambda_builders.workflows.python_pip.runtime_validator import validate_python_cmd + + +def validate_python_cmd(required_language, required_runtime_version): + major, minor = required_runtime_version.replace(required_language, "").split('.') + cmd = [ + "python", + "-c", + "import sys; " + "assert sys.version_info.major == {major} " + "and sys.version_info.minor == {minor}".format( + major=major, + minor=minor)] + return cmd + _RUNTIME_VERSION_RESOLVER = { "python": validate_python_cmd diff --git a/aws_lambda_builders/workflows/python_pip/runtime_validator.py b/aws_lambda_builders/workflows/python_pip/runtime_validator.py deleted file mode 100644 index 57d2a98f0..000000000 --- a/aws_lambda_builders/workflows/python_pip/runtime_validator.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Validation of Python Runtime Version -""" - - -def validate_python_cmd(required_language, required_runtime_version): - major, minor = required_runtime_version.replace(required_language, "").split('.') - cmd = [ - "python", - "-c", - "import sys; " - "assert sys.version_info.major == {major} " - "and sys.version_info.minor == {minor}".format( - major=major, - minor=minor)] - return cmd diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py index 6fcbcfff6..6807687d4 100644 --- a/tests/functional/test_cli.py +++ b/tests/functional/test_cli.py @@ -53,11 +53,13 @@ def test_run_hello_workflow(self): "artifacts_dir": self.artifacts_dir, "scratch_dir": "/ignored", "manifest_path": "/ignored", + "runtime": None, "optimizations": {}, "options": {}, } }).encode('utf-8') + env = copy.deepcopy(os.environ) env["PYTHONPATH"] = self.python_path diff --git a/tests/unit/test_runtime.py b/tests/unit/test_runtime.py index 928ac8108..fdc829d84 100644 --- a/tests/unit/test_runtime.py +++ b/tests/unit/test_runtime.py @@ -3,6 +3,7 @@ import mock from aws_lambda_builders.exceptions import MisMatchRuntimeError +from aws_lambda_builders.runtime import validate_python_cmd from aws_lambda_builders.runtime import Runtime @@ -38,3 +39,9 @@ def test_runtime_validate_mismatch_version_runtime(self): with self.assertRaises(MisMatchRuntimeError): Runtime.validate_runtime("python", "python2.7") self.assertTrue(mock_subprocess.call_count, 1) + + def test_python_command(self): + cmd = validate_python_cmd("python", "python2.7") + version_strings = ["sys.version_info.major == 2", "sys.version_info.minor == 7"] + for version_string in version_strings: + self.assertTrue(any([part for part in cmd if version_string in part])) diff --git a/tests/unit/workflows/python_pip/test_runtime_validator.py b/tests/unit/workflows/python_pip/test_runtime_validator.py deleted file mode 100644 index 43c963198..000000000 --- a/tests/unit/workflows/python_pip/test_runtime_validator.py +++ /dev/null @@ -1,12 +0,0 @@ -from unittest import TestCase - -from aws_lambda_builders.workflows.python_pip.runtime_validator import validate_python_cmd - - -class TestPythonRuntimeValidator(TestCase): - - def test_python_command(self): - cmd = validate_python_cmd("python", "python2.7") - version_strings = ["sys.version_info.major == 2", "sys.version_info.minor == 7"] - for version_string in version_strings: - self.assertTrue(any([part for part in cmd if version_string in part])) From 14103e05505093698e81318a9c98c11b0cc582c0 Mon Sep 17 00:00:00 2001 From: Sriram Madapusi Vasudevan Date: Fri, 16 Nov 2018 11:03:17 -0800 Subject: [PATCH 3/4] fix: fail silently on unsupported language runtime validation --- aws_lambda_builders/runtime.py | 6 +++++- tests/functional/test_cli.py | 2 +- .../workflows/python_pip/test_python_pip.py | 11 ++++++++--- tests/unit/test_runtime.py | 7 +++++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/aws_lambda_builders/runtime.py b/aws_lambda_builders/runtime.py index 95a30e9c2..69f29fb58 100644 --- a/aws_lambda_builders/runtime.py +++ b/aws_lambda_builders/runtime.py @@ -49,10 +49,14 @@ def validate_runtime(cls, required_language, required_runtime): :raises ValueError: Unsupported Lambda Runtime :raises MisMatchRuntimeError: Version mismatch of the lanugage vs the required runtime """ + try: + cmd = _RUNTIME_VERSION_RESOLVER[required_language](required_language, required_runtime) + except KeyError: + # The language is not currently supported, so there is no runtime validation. + return if not Runtime.has_value(required_runtime): raise ValueError("Unsupported Lambda runtime {}".format(required_runtime)) - cmd = _RUNTIME_VERSION_RESOLVER[required_language](required_language, required_runtime) p = subprocess.Popen(cmd, cwd=os.getcwd(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py index 6807687d4..cbafb878a 100644 --- a/tests/functional/test_cli.py +++ b/tests/functional/test_cli.py @@ -53,7 +53,7 @@ def test_run_hello_workflow(self): "artifacts_dir": self.artifacts_dir, "scratch_dir": "/ignored", "manifest_path": "/ignored", - "runtime": None, + "runtime": "ignored", "optimizations": {}, "options": {}, } diff --git a/tests/integration/workflows/python_pip/test_python_pip.py b/tests/integration/workflows/python_pip/test_python_pip.py index 13e4139f3..e50e67b67 100644 --- a/tests/integration/workflows/python_pip/test_python_pip.py +++ b/tests/integration/workflows/python_pip/test_python_pip.py @@ -1,6 +1,7 @@ import os import shutil +import sys import tempfile from unittest import TestCase @@ -28,6 +29,10 @@ def setUp(self): self.builder = LambdaBuilder(language="python", dependency_manager="pip", application_framework=None) + self.runtime = "{language}{major}.{minor}".format( + language=self.builder.capability.language, + major=sys.version_info.major, + minor=sys.version_info.minor) def tearDown(self): shutil.rmtree(self.artifacts_dir) @@ -35,7 +40,7 @@ def tearDown(self): def test_must_build_python_project(self): self.builder.build(self.source_dir, self.artifacts_dir, None, self.manifest_path_valid, - runtime="python2.7") + runtime=self.runtime) expected_files = self.test_data_files.union({"numpy", "numpy-1.15.4.data", "numpy-1.15.4.dist-info"}) output_files = set(os.listdir(self.artifacts_dir)) @@ -50,7 +55,7 @@ def test_must_fail_to_resolve_dependencies(self): with self.assertRaises(WorkflowFailedError) as ctx: self.builder.build(self.source_dir, self.artifacts_dir, None, self.manifest_path_invalid, - runtime="python2.7") + runtime=self.runtime) self.assertIn("Invalid requirement: 'adfasf=1.2.3'", str(ctx.exception)) @@ -58,6 +63,6 @@ def test_must_fail_if_requirements_not_found(self): with self.assertRaises(WorkflowFailedError) as ctx: self.builder.build(self.source_dir, self.artifacts_dir, None, os.path.join("non", "existent", "manifest"), - runtime="python2.7") + runtime=self.runtime) self.assertIn("Requirements file not found", str(ctx.exception)) diff --git a/tests/unit/test_runtime.py b/tests/unit/test_runtime.py index fdc829d84..fa56848eb 100644 --- a/tests/unit/test_runtime.py +++ b/tests/unit/test_runtime.py @@ -23,9 +23,12 @@ def test_runtime_enums(self): self.assertTrue(Runtime.has_value("python3.6")) self.assertFalse(Runtime.has_value("test_language")) - def test_runtime_validate_unsupported_runtime(self): + def test_runtime_validate_unsupported_language_fail_open(self): + Runtime.validate_runtime("test_language", "test_language2.7") + + def test_runtime_validate_unsupported_runtime_version(self): with self.assertRaises(ValueError): - Runtime.validate_runtime("test_language", "test_language2.7") + Runtime.validate_runtime("python", "python2.8") def test_runtime_validate_supported_version_runtime(self): with mock.patch('subprocess.Popen') as mock_subprocess: From 3ed1f7011e14f2c6b5e89963c5c4b612bd3224c2 Mon Sep 17 00:00:00 2001 From: Sriram Madapusi Vasudevan Date: Fri, 16 Nov 2018 16:31:49 -0800 Subject: [PATCH 4/4] fix: remove enum - add warning logging messages on runtime and language validation. --- aws_lambda_builders/builder.py | 6 +- aws_lambda_builders/runtime.py | 66 ----------------- aws_lambda_builders/validate.py | 71 +++++++++++++++++++ .../workflows/python_pip/test_python_pip.py | 10 +-- tests/unit/test_runtime.py | 23 +++--- 5 files changed, 91 insertions(+), 85 deletions(-) delete mode 100644 aws_lambda_builders/runtime.py create mode 100644 aws_lambda_builders/validate.py diff --git a/aws_lambda_builders/builder.py b/aws_lambda_builders/builder.py index 2207f6639..57eda2f9a 100644 --- a/aws_lambda_builders/builder.py +++ b/aws_lambda_builders/builder.py @@ -6,7 +6,7 @@ import logging from aws_lambda_builders.registry import get_workflow, DEFAULT_REGISTRY -from aws_lambda_builders.runtime import Runtime +from aws_lambda_builders.validate import RuntimeValidator from aws_lambda_builders.workflow import Capability LOG = logging.getLogger(__name__) @@ -112,8 +112,8 @@ def _validate_runtime(self, runtime): :param runtime: String matching a lambda runtime eg: python3.6 """ - Runtime.validate_runtime(required_language=self.capability.language, - required_runtime=runtime) + RuntimeValidator.validate_runtime(required_language=self.capability.language, + required_runtime=runtime) def _clear_workflows(self): DEFAULT_REGISTRY.clear() diff --git a/aws_lambda_builders/runtime.py b/aws_lambda_builders/runtime.py deleted file mode 100644 index 69f29fb58..000000000 --- a/aws_lambda_builders/runtime.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Supported Runtimes and their validations. -""" - -import os -import subprocess -from enum import Enum - -from aws_lambda_builders.exceptions import MisMatchRuntimeError - - -def validate_python_cmd(required_language, required_runtime_version): - major, minor = required_runtime_version.replace(required_language, "").split('.') - cmd = [ - "python", - "-c", - "import sys; " - "assert sys.version_info.major == {major} " - "and sys.version_info.minor == {minor}".format( - major=major, - minor=minor)] - return cmd - - -_RUNTIME_VERSION_RESOLVER = { - "python": validate_python_cmd -} - - -class Runtime(Enum): - python27 = "python2.7" - python36 = "python3.6" - - @classmethod - def has_value(cls, value): - """ - Checks if the enum has this value - :param string value: Value to check - :return bool: True, if enum has the value - """ - return any(value == item.value for item in cls) - - @classmethod - def validate_runtime(cls, required_language, required_runtime): - """ - Checks if the language supplied matches the required lambda runtime - :param string required_language: language to check eg: python - :param string required_runtime: runtime to check eg: python3.6 - :raises ValueError: Unsupported Lambda Runtime - :raises MisMatchRuntimeError: Version mismatch of the lanugage vs the required runtime - """ - try: - cmd = _RUNTIME_VERSION_RESOLVER[required_language](required_language, required_runtime) - except KeyError: - # The language is not currently supported, so there is no runtime validation. - return - if not Runtime.has_value(required_runtime): - raise ValueError("Unsupported Lambda runtime {}".format(required_runtime)) - - p = subprocess.Popen(cmd, - cwd=os.getcwd(), - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - p.communicate() - if p.returncode != 0: - raise MisMatchRuntimeError(language=required_language, - required_runtime=required_runtime) diff --git a/aws_lambda_builders/validate.py b/aws_lambda_builders/validate.py new file mode 100644 index 000000000..7f4cbeb9b --- /dev/null +++ b/aws_lambda_builders/validate.py @@ -0,0 +1,71 @@ +""" +Supported Runtimes and their validations. +""" + +import logging +import os +import subprocess + +from aws_lambda_builders.exceptions import MisMatchRuntimeError + +LOG = logging.getLogger(__name__) + + +def validate_python_cmd(required_language, required_runtime_version): + major, minor = required_runtime_version.replace(required_language, "").split('.') + cmd = [ + "python", + "-c", + "import sys; " + "assert sys.version_info.major == {major} " + "and sys.version_info.minor == {minor}".format( + major=major, + minor=minor)] + return cmd + + +_RUNTIME_VERSION_RESOLVER = { + "python": validate_python_cmd +} + + +class RuntimeValidator(object): + SUPPORTED_RUNTIMES = [ + "python2.7", + "python3.6" + ] + + @classmethod + def has_runtime(cls, runtime): + """ + Checks if the runtime is supported. + :param string runtime: Runtime to check + :return bool: True, if the runtime is supported. + """ + return runtime in cls.SUPPORTED_RUNTIMES + + @classmethod + def validate_runtime(cls, required_language, required_runtime): + """ + Checks if the language supplied matches the required lambda runtime + :param string required_language: language to check eg: python + :param string required_runtime: runtime to check eg: python3.6 + :raises MisMatchRuntimeError: Version mismatch of the language vs the required runtime + """ + if required_language in _RUNTIME_VERSION_RESOLVER: + if not RuntimeValidator.has_runtime(required_runtime): + LOG.warning("'%s' runtime is not " + "a supported runtime", required_runtime) + return + cmd = _RUNTIME_VERSION_RESOLVER[required_language](required_language, required_runtime) + + p = subprocess.Popen(cmd, + cwd=os.getcwd(), + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p.communicate() + if p.returncode != 0: + raise MisMatchRuntimeError(language=required_language, + required_runtime=required_runtime) + else: + LOG.warning("'%s' runtime has not " + "been validated!", required_language) diff --git a/tests/integration/workflows/python_pip/test_python_pip.py b/tests/integration/workflows/python_pip/test_python_pip.py index e50e67b67..70ce09bf5 100644 --- a/tests/integration/workflows/python_pip/test_python_pip.py +++ b/tests/integration/workflows/python_pip/test_python_pip.py @@ -46,10 +46,12 @@ def test_must_build_python_project(self): output_files = set(os.listdir(self.artifacts_dir)) self.assertEquals(expected_files, output_files) - def test_runtime_validate_python_project(self): - with self.assertRaises(ValueError): - self.builder.build(self.source_dir, self.artifacts_dir, None, self.manifest_path_valid, - runtime="python2.8") + def test_runtime_validate_python_project_fail_open_unsupported_runtime(self): + self.builder.build(self.source_dir, self.artifacts_dir, None, self.manifest_path_valid, + runtime="python2.8") + expected_files = self.test_data_files.union({"numpy", "numpy-1.15.4.data", "numpy-1.15.4.dist-info"}) + output_files = set(os.listdir(self.artifacts_dir)) + self.assertEquals(expected_files, output_files) def test_must_fail_to_resolve_dependencies(self): diff --git a/tests/unit/test_runtime.py b/tests/unit/test_runtime.py index fa56848eb..25799806d 100644 --- a/tests/unit/test_runtime.py +++ b/tests/unit/test_runtime.py @@ -3,8 +3,8 @@ import mock from aws_lambda_builders.exceptions import MisMatchRuntimeError -from aws_lambda_builders.runtime import validate_python_cmd -from aws_lambda_builders.runtime import Runtime +from aws_lambda_builders.validate import validate_python_cmd +from aws_lambda_builders.validate import RuntimeValidator class MockSubProcess(object): @@ -18,29 +18,28 @@ def communicate(self): class TestRuntime(TestCase): - def test_runtime_enums(self): - self.assertTrue(Runtime.has_value("python2.7")) - self.assertTrue(Runtime.has_value("python3.6")) - self.assertFalse(Runtime.has_value("test_language")) + def test_supported_runtimes(self): + self.assertTrue(RuntimeValidator.has_runtime("python2.7")) + self.assertTrue(RuntimeValidator.has_runtime("python3.6")) + self.assertFalse(RuntimeValidator.has_runtime("test_language")) def test_runtime_validate_unsupported_language_fail_open(self): - Runtime.validate_runtime("test_language", "test_language2.7") + RuntimeValidator.validate_runtime("test_language", "test_language2.7") - def test_runtime_validate_unsupported_runtime_version(self): - with self.assertRaises(ValueError): - Runtime.validate_runtime("python", "python2.8") + def test_runtime_validate_unsupported_runtime_version_fail_open(self): + RuntimeValidator.validate_runtime("python", "python2.8") def test_runtime_validate_supported_version_runtime(self): with mock.patch('subprocess.Popen') as mock_subprocess: mock_subprocess.return_value = MockSubProcess(0) - Runtime.validate_runtime("python", "python3.6") + RuntimeValidator.validate_runtime("python", "python3.6") self.assertTrue(mock_subprocess.call_count, 1) def test_runtime_validate_mismatch_version_runtime(self): with mock.patch('subprocess.Popen') as mock_subprocess: mock_subprocess.return_value = MockSubProcess(1) with self.assertRaises(MisMatchRuntimeError): - Runtime.validate_runtime("python", "python2.7") + RuntimeValidator.validate_runtime("python", "python2.7") self.assertTrue(mock_subprocess.call_count, 1) def test_python_command(self):