Skip to content

Commit 681d65b

Browse files
authored
feat: allow path resolver to look at additional paths (#78)
* feat: allow path resolver to look at additional paths - search for executables on additional paths that are not on `$PATH` - additional paths take precedence before searching for executables on `$PATH` - rev protocol version to 0.2 with introduction of additional search paths. * fix: version checks for RPC calls - address comments - add functional tests * fix: add Debug statements - extra func test for back compat * fix: explanation for version check compatibility * fix: appveyor build
1 parent 29f254b commit 681d65b

File tree

12 files changed

+189
-29
lines changed

12 files changed

+189
-29
lines changed

.appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ install:
2323
- "%PYTHON%\\python.exe -m pip install -e ."
2424
- "set PATH=C:\\Ruby25-x64\\bin;%PATH%"
2525
- "gem --version"
26-
- "gem install bundler -v 1.17.3 --no-ri --no-rdoc"
26+
- "gem install bundler -v 1.17.3"
2727
- "bundler --version"
2828
- "echo %PATH%"
2929

aws_lambda_builders/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
AWS Lambda Builder Library
33
"""
44
__version__ = '0.0.5'
5-
RPC_PROTOCOL_VERSION = "0.1"
5+
RPC_PROTOCOL_VERSION = "0.2"

aws_lambda_builders/__main__.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
import json
1111
import os
1212
import logging
13+
import re
1314

1415
from aws_lambda_builders.builder import LambdaBuilder
1516
from aws_lambda_builders.exceptions import WorkflowNotFoundError, WorkflowUnknownError, WorkflowFailedError
16-
17+
from aws_lambda_builders import RPC_PROTOCOL_VERSION as lambda_builders_protocol_version
1718

1819
log_level = int(os.environ.get("LAMBDA_BUILDERS_LOG_LEVEL", logging.INFO))
1920

@@ -24,6 +25,8 @@
2425

2526
LOG = logging.getLogger(__name__)
2627

28+
VERSION_REGEX = re.compile("^([0-9])+.([0-9]+)$")
29+
2730

2831
def _success_response(request_id, artifacts_dir):
2932
return json.dumps({
@@ -46,6 +49,31 @@ def _error_response(request_id, http_status_code, message):
4649
})
4750

4851

52+
def _parse_version(version_string):
53+
54+
if VERSION_REGEX.match(version_string):
55+
return float(version_string)
56+
else:
57+
ex = "Protocol Version does not match : {}".format(VERSION_REGEX.pattern)
58+
LOG.debug(ex)
59+
raise ValueError(ex)
60+
61+
62+
def version_compatibility_check(version):
63+
# The following check is between current protocol version vs version of the protocol
64+
# with which aws-lambda-builders is called.
65+
# Example:
66+
# 0.2 < 0.2 comparison will fail, don't throw a value Error saying incompatible version.
67+
# 0.2 < 0.3 comparison will pass, throwing a ValueError
68+
# 0.2 < 0.1 comparison will fail, don't throw a value Error saying incompatible version
69+
70+
if _parse_version(lambda_builders_protocol_version) < version:
71+
ex = "Incompatible Protocol Version : {}, " \
72+
"Current Protocol Version: {}".format(version, lambda_builders_protocol_version)
73+
LOG.error(ex)
74+
raise ValueError(ex)
75+
76+
4977
def _write_response(response, exit_code):
5078
sys.stdout.write(response)
5179
sys.stdout.flush() # Make sure it is written
@@ -77,11 +105,20 @@ def main(): # pylint: disable=too-many-statements
77105
response = _error_response(request_id, -32601, "Method unavailable")
78106
return _write_response(response, 1)
79107

108+
try:
109+
protocol_version = _parse_version(params.get("__protocol_version"))
110+
version_compatibility_check(protocol_version)
111+
112+
except ValueError:
113+
response = _error_response(request_id, 505, "Unsupported Protocol Version")
114+
return _write_response(response, 1)
115+
80116
capabilities = params["capability"]
81117
supported_workflows = params.get("supported_workflows")
82118

83119
exit_code = 0
84120
response = None
121+
85122
try:
86123
builder = LambdaBuilder(language=capabilities["language"],
87124
dependency_manager=capabilities["dependency_manager"],
@@ -93,6 +130,7 @@ def main(): # pylint: disable=too-many-statements
93130
params["artifacts_dir"],
94131
params["scratch_dir"],
95132
params["manifest_path"],
133+
executable_search_paths=params['executable_search_paths'],
96134
runtime=params["runtime"],
97135
optimizations=params["optimizations"],
98136
options=params["options"])

aws_lambda_builders/builder.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(self, language, dependency_manager, application_framework, supporte
5656
LOG.debug("Found workflow '%s' to support capabilities '%s'", self.selected_workflow_cls.NAME, self.capability)
5757

5858
def build(self, source_dir, artifacts_dir, scratch_dir, manifest_path,
59-
runtime=None, optimizations=None, options=None):
59+
runtime=None, optimizations=None, options=None, executable_search_paths=None):
6060
"""
6161
Actually build the code by running workflows
6262
@@ -89,6 +89,10 @@ def build(self, source_dir, artifacts_dir, scratch_dir, manifest_path,
8989
:type options: dict
9090
:param options:
9191
Optional dictionary of options ot pass to build action. **Not supported**.
92+
93+
:type executable_search_paths: list
94+
:param executable_search_paths:
95+
Additional list of paths to search for executables required by the workflow.
9296
"""
9397

9498
if not os.path.exists(scratch_dir):
@@ -100,7 +104,8 @@ def build(self, source_dir, artifacts_dir, scratch_dir, manifest_path,
100104
manifest_path,
101105
runtime=runtime,
102106
optimizations=optimizations,
103-
options=options)
107+
options=options,
108+
executable_search_paths=executable_search_paths)
104109

105110
return workflow.run()
106111

aws_lambda_builders/path_resolver.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@
77

88
class PathResolver(object):
99

10-
def __init__(self, binary, runtime):
10+
def __init__(self, binary, runtime, executable_search_paths=None):
1111
self.binary = binary
1212
self.runtime = runtime
1313
self.executables = [self.runtime, self.binary]
14+
self.executable_search_paths = executable_search_paths
1415

1516
def _which(self):
1617
exec_paths = []
1718
for executable in [executable for executable in self.executables if executable is not None]:
18-
paths = which(executable)
19+
paths = which(executable, executable_search_paths=self.executable_search_paths)
1920
exec_paths.extend(paths)
2021

2122
if not exec_paths:

aws_lambda_builders/utils.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,27 @@ def copytree(source, destination, ignore=None):
6868
# Copyright 2019 by the Python Software Foundation
6969

7070

71-
def which(cmd, mode=os.F_OK | os.X_OK, path=None): # pragma: no cover
72-
"""Given a command, mode, and a PATH string, return the paths which
73-
conforms to the given mode on the PATH, or None if there is no such
74-
file.
75-
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
76-
of os.environ.get("PATH"), or can be overridden with a custom search
77-
path.
71+
def which(cmd, mode=os.F_OK | os.X_OK, executable_search_paths=None): # pragma: no cover
72+
"""Given a command, mode, and executable search paths list, return the paths which
73+
conforms to the given mode on the PATH with the prepended additional search paths,
74+
or None if there is no such file.
75+
`mode` defaults to os.F_OK | os.X_OK. the default search `path` defaults
76+
to the result of os.environ.get("PATH")
7877
Note: This function was backported from the Python 3 source code.
78+
79+
:type cmd: str
80+
:param cmd:
81+
Executable to be looked up in PATH.
82+
83+
:type mode: str
84+
:param mode:
85+
Modes of access for the executable.
86+
87+
:type executable_search_paths: list
88+
:param executable_search_paths:
89+
List of paths to look for `cmd` in preference order.
7990
"""
91+
8092
# Check that a given file can be accessed with the correct mode.
8193
# Additionally check that `file` is not a directory, as on Windows
8294
# directories pass the os.access check.
@@ -93,13 +105,16 @@ def _access_check(fn, mode):
93105

94106
return None
95107

96-
if path is None:
97-
path = os.environ.get("PATH", os.defpath)
108+
path = os.environ.get("PATH", os.defpath)
109+
98110
if not path:
99111
return None
100112

101113
path = path.split(os.pathsep)
102114

115+
if executable_search_paths:
116+
path = executable_search_paths + path
117+
103118
if sys.platform == "win32":
104119
# The current directory takes precedence on Windows.
105120
if os.curdir not in path:

aws_lambda_builders/workflow.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def __init__(self,
117117
scratch_dir,
118118
manifest_path,
119119
runtime=None,
120+
executable_search_paths=None,
120121
optimizations=None,
121122
options=None):
122123
"""
@@ -152,6 +153,10 @@ def __init__(self,
152153
:type options: dict
153154
:param options:
154155
Optional dictionary of options ot pass to build action. **Not supported**.
156+
157+
:type executable_search_paths: list
158+
:param executable_search_paths:
159+
Optional, Additional list of paths to search for executables required by the workflow.
155160
"""
156161

157162
self.source_dir = source_dir
@@ -161,6 +166,7 @@ def __init__(self,
161166
self.runtime = runtime
162167
self.optimizations = optimizations
163168
self.options = options
169+
self.executable_search_paths = executable_search_paths
164170

165171
# Actions are registered by the subclasses as they seem fit
166172
self.actions = []
@@ -181,7 +187,8 @@ def get_resolvers(self):
181187
"""
182188
Non specialized path resolver that just returns the list of executable for the runtime on the path.
183189
"""
184-
return [PathResolver(runtime=self.runtime, binary=self.CAPABILITY.language)]
190+
return [PathResolver(runtime=self.runtime, binary=self.CAPABILITY.language,
191+
executable_search_paths=self.executable_search_paths)]
185192

186193
def get_validators(self):
187194
"""

tests/functional/test_builder.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
import shutil
55
import tempfile
66

7+
try:
8+
import pathlib
9+
except ImportError:
10+
import pathlib2 as pathlib
11+
712
from unittest import TestCase
813
from aws_lambda_builders.builder import LambdaBuilder
914

@@ -40,16 +45,17 @@ def tearDown(self):
4045
# Remove the workflows folder from PYTHONPATH
4146
sys.path.remove(self.TEST_WORKFLOWS_FOLDER)
4247

43-
def test_run_hello_workflow(self):
48+
def test_run_hello_workflow_with_exec_paths(self):
4449

4550
self.hello_builder.build(self.source_dir,
4651
self.artifacts_dir,
4752
self.scratch_dir,
48-
"/ignored")
53+
"/ignored",
54+
executable_search_paths=[str(pathlib.Path(sys.executable).parent)])
4955

5056
self.assertTrue(os.path.exists(self.expected_filename))
5157
contents = ''
5258
with open(self.expected_filename, 'r') as fp:
5359
contents = fp.read()
5460

55-
self.assertEquals(contents, self.expected_contents)
61+
self.assertEquals(contents, self.expected_contents)

tests/functional/test_cli.py

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@
55
import tempfile
66
import subprocess
77
import copy
8+
import sys
89

910
from unittest import TestCase
1011
from parameterized import parameterized
1112

13+
try:
14+
import pathlib
15+
except ImportError:
16+
import pathlib2 as pathlib
17+
18+
19+
from aws_lambda_builders import RPC_PROTOCOL_VERSION as lambda_builders_protocol_version
20+
1221

1322
class TestCliWithHelloWorkflow(TestCase):
1423

@@ -39,19 +48,21 @@ def setUp(self):
3948
def tearDown(self):
4049
shutil.rmtree(self.source_dir)
4150
shutil.rmtree(self.artifacts_dir)
42-
shutil.rmtree(self.scratch_dir)
4351

4452
@parameterized.expand([
45-
("request_through_stdin"),
46-
("request_through_argument")
53+
("request_through_stdin", lambda_builders_protocol_version),
54+
("request_through_argument", lambda_builders_protocol_version),
55+
("request_through_stdin", "0.1"),
56+
("request_through_argument", "0.1"),
4757
])
48-
def test_run_hello_workflow(self, flavor):
58+
def test_run_hello_workflow_with_backcompat(self, flavor, protocol_version):
4959

5060
request_json = json.dumps({
5161
"jsonschema": "2.0",
5262
"id": 1234,
5363
"method": "LambdaBuilder.build",
5464
"params": {
65+
"__protocol_version": protocol_version,
5566
"capability": {
5667
"language": self.language,
5768
"dependency_manager": self.dependency_manager,
@@ -65,6 +76,7 @@ def test_run_hello_workflow(self, flavor):
6576
"runtime": "ignored",
6677
"optimizations": {},
6778
"options": {},
79+
"executable_search_paths": [str(pathlib.Path(sys.executable).parent)]
6880
}
6981
})
7082

@@ -94,4 +106,52 @@ def test_run_hello_workflow(self, flavor):
94106
contents = fp.read()
95107

96108
self.assertEquals(contents, self.expected_contents)
109+
shutil.rmtree(self.scratch_dir)
110+
111+
@parameterized.expand([
112+
("request_through_stdin"),
113+
("request_through_argument")
114+
])
115+
def test_run_hello_workflow_incompatible(self, flavor):
116+
117+
request_json = json.dumps({
118+
"jsonschema": "2.0",
119+
"id": 1234,
120+
"method": "LambdaBuilder.build",
121+
"params": {
122+
"__protocol_version": "2.0",
123+
"capability": {
124+
"language": self.language,
125+
"dependency_manager": self.dependency_manager,
126+
"application_framework": self.application_framework
127+
},
128+
"supported_workflows": [self.HELLO_WORKFLOW_MODULE],
129+
"source_dir": self.source_dir,
130+
"artifacts_dir": self.artifacts_dir,
131+
"scratch_dir": self.scratch_dir,
132+
"manifest_path": "/ignored",
133+
"runtime": "ignored",
134+
"optimizations": {},
135+
"options": {},
136+
"executable_search_paths": [str(pathlib.Path(sys.executable).parent)]
137+
}
138+
})
139+
140+
141+
env = copy.deepcopy(os.environ)
142+
env["PYTHONPATH"] = self.python_path
143+
144+
stdout_data = None
145+
if flavor == "request_through_stdin":
146+
p = subprocess.Popen([self.command_name], env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
147+
stdout_data = p.communicate(input=request_json.encode('utf-8'))[0]
148+
elif flavor == "request_through_argument":
149+
p = subprocess.Popen([self.command_name, request_json], env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
150+
stdout_data = p.communicate()[0]
151+
else:
152+
raise ValueError("Invalid test flavor")
97153

154+
# Validate the response object. It should be error response
155+
response = json.loads(stdout_data)
156+
self.assertIn('error', response)
157+
self.assertEquals(response['error']['code'], 505)

tests/functional/test_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def test_must_respect_excludes_list(self):
4040
self.assertEquals(set(os.listdir(os.path.join(self.dest, "a"))), {"c"})
4141
self.assertEquals(set(os.listdir(os.path.join(self.dest, "a"))), {"c"})
4242

43+
4344
def file(*args):
4445
path = os.path.join(*args)
4546
basedir = os.path.dirname(path)

0 commit comments

Comments
 (0)