diff --git a/.gitignore b/.gitignore index 566b969ba..93ad8d6c7 100644 --- a/.gitignore +++ b/.gitignore @@ -378,5 +378,6 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk +/Dockerfile -# End of https://www.gitignore.io/api/osx,node,macos,linux,python,windows,pycharm,intellij,sublimetext,visualstudiocode \ No newline at end of file +# End of https://www.gitignore.io/api/osx,node,macos,linux,python,windows,pycharm,intellij,sublimetext,visualstudiocode diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..e5b81496c --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include LICENSE +include requirements/base.txt +include requirements/dev.txt +recursive-include aws_lambda_builders/workflows * +prune tests + diff --git a/Makefile b/Makefile index 4bbd18691..d43feb880 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ init: test: # Run unit tests - # Fail if coverage falls below 95% - LAMBDA_BUILDERS_DEV=1 pytest --cov aws_lambda_builders --cov-report term-missing --cov-fail-under 95 tests/unit tests/functional + # Fail if coverage falls below 90% + LAMBDA_BUILDERS_DEV=1 pytest --cov aws_lambda_builders --cov-report term-missing --cov-fail-under 90 tests/unit tests/functional func-test: LAMBDA_BUILDERS_DEV=1 pytest tests/functional diff --git a/aws_lambda_builders/__main__.py b/aws_lambda_builders/__main__.py index bab9c1927..d931572cb 100644 --- a/aws_lambda_builders/__main__.py +++ b/aws_lambda_builders/__main__.py @@ -8,37 +8,76 @@ import sys import json +import os +import logging + from aws_lambda_builders.builder import LambdaBuilder from aws_lambda_builders.exceptions import WorkflowNotFoundError, WorkflowUnknownError, WorkflowFailedError -def _success_response(request_id): +log_level = int(os.environ.get("LAMBDA_BUILDERS_LOG_LEVEL", logging.INFO)) + +# Write output to stderr because stdout is used for command response +logging.basicConfig(stream=sys.stderr, + level=log_level, + format='[Lambda Builders] %(asctime)s - %(levelname)s - %(message)s') + +LOG = logging.getLogger(__name__) + + +def _success_response(request_id, artifacts_dir): return json.dumps({ "jsonrpc": "2.0", "id": request_id, - "result": {} + "result": { + "artifacts_dir": artifacts_dir + } }) -def main(): +def _error_response(request_id, http_status_code, message): + return json.dumps({ + "jsonrpc": "2.0", + "id": request_id, + "error": { + "code": http_status_code, + "message": message + } + }) + + +def main(): # pylint: disable=too-many-statements """ Implementation of CLI Interface. Handles only one JSON-RPC method at a time and responds with data + + Input is passed as JSON string either through stdin or as the first argument to the command. Output is always + printed to stdout. """ # For now the request is not validated - request = json.load(sys.stdin) + if len(sys.argv) > 1: + request_str = sys.argv[1] + LOG.debug("Using the request object from command line argument") + else: + LOG.debug("Reading the request object from stdin") + request_str = sys.stdin.read() + + request = json.loads(request_str) request_id = request["id"] params = request["params"] capabilities = params["capability"] - supported_workflows = params["supported_workflows"] + supported_workflows = params.get("supported_workflows") + exit_code = 0 + response = None try: builder = LambdaBuilder(language=capabilities["language"], dependency_manager=capabilities["dependency_manager"], application_framework=capabilities["application_framework"], supported_workflows=supported_workflows) + artifacts_dir = params["artifacts_dir"] builder.build(params["source_dir"], params["artifacts_dir"], params["scratch_dir"], @@ -48,17 +87,21 @@ def main(): options=params["options"]) # Return a success response - sys.stdout.write(_success_response(request_id)) - sys.stdout.flush() # Make sure it is written + response = _success_response(request_id, artifacts_dir) except (WorkflowNotFoundError, WorkflowUnknownError, WorkflowFailedError) as ex: - # TODO: Return a workflow error response - print(str(ex)) - sys.exit(1) + LOG.debug("Builder workflow failed", exc_info=ex) + exit_code = 1 + response = _error_response(request_id, 400, str(ex)) + except Exception as ex: - # TODO: Return a internal server response - print(str(ex)) - sys.exit(1) + LOG.debug("Builder crashed", exc_info=ex) + exit_code = 1 + response = _error_response(request_id, 500, str(ex)) + + sys.stdout.write(response) + sys.stdout.flush() # Make sure it is written + sys.exit(exit_code) if __name__ == '__main__': diff --git a/aws_lambda_builders/utils.py b/aws_lambda_builders/utils.py index 704456bfd..e7ebc3941 100644 --- a/aws_lambda_builders/utils.py +++ b/aws_lambda_builders/utils.py @@ -52,6 +52,7 @@ def copytree(source, destination, ignore=None): new_source = os.path.join(source, name) new_destination = os.path.join(destination, name) + if os.path.isdir(new_source): copytree(new_source, new_destination, ignore=ignore) else: diff --git a/requirements/base.txt b/requirements/base.txt index e69de29bb..09a93af1b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -0,0 +1 @@ +six~=1.11.0 diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py index 9038b667f..e897fe345 100644 --- a/tests/functional/test_cli.py +++ b/tests/functional/test_cli.py @@ -7,6 +7,7 @@ import copy from unittest import TestCase +from parameterized import parameterized class TestCliWithHelloWorkflow(TestCase): @@ -29,15 +30,20 @@ def setUp(self): self.expected_contents = "Hello World" self.command_name = "lambda-builders-dev" if os.environ.get("LAMBDA_BUILDERS_DEV") else "lambda-builders" + + # Make sure the test workflow is in PYTHONPATH to be automatically loaded self.python_path_list = os.environ.get("PYTHONPATH", '').split(os.pathsep) + [self.TEST_WORKFLOWS_FOLDER] self.python_path = os.pathsep.join(filter(bool, self.python_path_list)) - print(self.python_path) def tearDown(self): shutil.rmtree(self.source_dir) shutil.rmtree(self.artifacts_dir) - def test_run_hello_workflow(self): + @parameterized.expand([ + ("request_through_stdin"), + ("request_through_argument") + ]) + def test_run_hello_workflow(self, flavor): request_json = json.dumps({ "jsonschema": "2.0", @@ -57,14 +63,26 @@ def test_run_hello_workflow(self): "optimizations": {}, "options": {}, } - }).encode('utf-8') + }) env = copy.deepcopy(os.environ) env["PYTHONPATH"] = self.python_path - p = subprocess.Popen([self.command_name], env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE) - stdout_data = p.communicate(input=request_json)[0] - print(stdout_data) + stdout_data = None + if flavor == "request_through_stdin": + p = subprocess.Popen([self.command_name], env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + stdout_data = p.communicate(input=request_json.encode('utf-8'))[0] + elif flavor == "request_through_argument": + p = subprocess.Popen([self.command_name, request_json], env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + stdout_data = p.communicate()[0] + else: + raise ValueError("Invalid test flavor") + + # Validate the response object. It should be successful response + response = json.loads(stdout_data) + self.assertNotIn('error', response) + self.assertIn('result', response) + self.assertEquals(response['result']['artifacts_dir'], self.artifacts_dir) self.assertTrue(os.path.exists(self.expected_filename)) contents = '' 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() -