Skip to content

Commit a25f189

Browse files
authored
restructure project to use pyproject.toml; add secretsmanager proxy test (#106)
1 parent abcbaca commit a25f189

File tree

17 files changed

+570
-455
lines changed

17 files changed

+570
-455
lines changed

.github/workflows/aws-proxy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
3434
run: |
3535
set -e
36+
cd aws-proxy
3637
docker pull localstack/localstack-pro &
3738
docker pull public.ecr.aws/lambda/python:3.8 &
3839
@@ -49,7 +50,6 @@ jobs:
4950
# build and install extension
5051
localstack extensions init
5152
(
52-
cd aws-proxy
5353
make install
5454
. .venv/bin/activate
5555
pip install --upgrade --pre localstack localstack-ext

aws-proxy/Makefile

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,39 @@ VENV_RUN = . $(VENV_ACTIVATE)
55
TEST_PATH ?= tests
66
PIP_CMD ?= pip
77

8-
usage: ## Show this help
8+
usage: ## Show this help
99
@grep -Fh "##" $(MAKEFILE_LIST) | grep -Fv fgrep | sed -e 's/:.*##\s*/##/g' | awk -F'##' '{ printf "%-25s %s\n", $$1, $$2 }'
1010

11-
venv: $(VENV_ACTIVATE)
12-
13-
$(VENV_ACTIVATE): setup.py setup.cfg
11+
install: ## Install dependencies
1412
test -d .venv || $(VENV_BIN) .venv
15-
$(VENV_RUN); pip install --upgrade pip setuptools plux wheel
16-
$(VENV_RUN); pip install --upgrade black isort pyproject-flake8 flake8-black flake8-isort
1713
$(VENV_RUN); pip install -e .
14+
$(VENV_RUN); pip install -e .[test]
1815
touch $(VENV_DIR)/bin/activate
1916

20-
clean:
17+
clean: ## Clean up
2118
rm -rf .venv/
2219
rm -rf build/
2320
rm -rf .eggs/
2421
rm -rf *.egg-info/
2522

26-
lint:
27-
$(VENV_RUN); python -m pflake8 --show-source
28-
29-
format:
30-
$(VENV_RUN); python -m isort .; python -m black .
23+
format: ## Run ruff to format the whole codebase
24+
($(VENV_RUN); python -m ruff format .; python -m ruff check --output-format=full --fix .)
3125

32-
install: venv
33-
$(VENV_RUN); $(PIP_CMD) install -e ".[test]"
26+
lint: ## Run code linter to check code style
27+
($(VENV_RUN); python -m ruff check --output-format=full . && python -m ruff format --check .)
3428

35-
test: venv
29+
test: ## Run tests
3630
$(VENV_RUN); python -m pytest $(PYTEST_ARGS) $(TEST_PATH)
3731

38-
dist: venv
39-
$(VENV_RUN); python setup.py sdist bdist_wheel
32+
entrypoints: ## Generate plugin entrypoints for Python package
33+
$(VENV_RUN); python -m plux entrypoints
4034

41-
build: ## Build the extension
42-
mkdir -p build
43-
cp -r setup.py setup.cfg README.md aws_proxy build/
44-
(cd build && python setup.py sdist)
35+
build: entrypoints ## Build the extension
36+
$(VENV_RUN); python -m build --no-isolation . --outdir build
37+
@# make sure that the entrypoints are contained in the dist folder and are non-empty
38+
@test -s localstack_extension_aws_proxy.egg-info/entry_points.txt || (echo "Entrypoints were not correctly created! Aborting!" && exit 1)
4539

46-
enable: $(wildcard ./build/dist/localstack_extension_aws_proxy-*.tar.gz) ## Enable the extension in LocalStack
40+
enable: $(wildcard ./build/localstack_extension_aws_proxy-*.tar.gz) ## Enable the extension in LocalStack
4741
$(VENV_RUN); \
4842
pip uninstall --yes localstack-extension-aws-proxy; \
4943
localstack extensions -v install file://$?

aws-proxy/aws_proxy/client/auth_proxy.py

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
from localstack import config as localstack_config
1616
from localstack.aws.spec import load_service
1717
from localstack.config import external_service_url
18-
from localstack.constants import AWS_REGION_US_EAST_1, DOCKER_IMAGE_NAME_PRO, LOCALHOST_HOSTNAME
18+
from localstack.constants import (
19+
AWS_REGION_US_EAST_1,
20+
DOCKER_IMAGE_NAME_PRO,
21+
LOCALHOST_HOSTNAME,
22+
)
1923
from localstack.http import Request
2024
from localstack.pro.core.bootstrap.licensingv2 import (
2125
ENV_LOCALSTACK_API_KEY,
@@ -25,7 +29,10 @@
2529
from localstack.utils.bootstrap import setup_logging
2630
from localstack.utils.collections import select_attributes
2731
from localstack.utils.container_utils.container_client import PortMappings
28-
from localstack.utils.docker_utils import DOCKER_CLIENT, reserve_available_container_port
32+
from localstack.utils.docker_utils import (
33+
DOCKER_CLIENT,
34+
reserve_available_container_port,
35+
)
2936
from localstack.utils.files import new_tmp_file, save_file
3037
from localstack.utils.functions import run_safe
3138
from localstack.utils.net import get_docker_host_from_container, get_free_tcp_port
@@ -39,8 +46,6 @@
3946
from aws_proxy.shared.constants import HEADER_HOST_ORIGINAL
4047
from aws_proxy.shared.models import AddProxyRequest, ProxyConfig
4148

42-
from .http2_server import run_server
43-
4449
LOG = logging.getLogger(__name__)
4550
LOG.setLevel(logging.INFO)
4651
if localstack_config.DEBUG:
@@ -66,9 +71,14 @@ def __init__(self, config: ProxyConfig, port: int = None):
6671
super().__init__(port=port)
6772

6873
def do_run(self):
74+
# note: keep import here, to avoid runtime errors
75+
from .http2_server import run_server
76+
6977
self.register_in_instance()
7078
bind_host = self.config.get("bind_host") or DEFAULT_BIND_HOST
71-
proxy = run_server(port=self.port, bind_addresses=[bind_host], handler=self.proxy_request)
79+
proxy = run_server(
80+
port=self.port, bind_addresses=[bind_host], handler=self.proxy_request
81+
)
7282
proxy.join()
7383

7484
def proxy_request(self, request: Request, data: bytes) -> Response:
@@ -109,7 +119,9 @@ def proxy_request(self, request: Request, data: bytes) -> Response:
109119
# adjust request dict and fix certain edge cases in the request
110120
self._adjust_request_dict(service_name, request_dict)
111121

112-
headers_truncated = {k: truncate(to_str(v)) for k, v in dict(aws_request.headers).items()}
122+
headers_truncated = {
123+
k: truncate(to_str(v)) for k, v in dict(aws_request.headers).items()
124+
}
113125
LOG.debug(
114126
"Sending request for service %s to AWS: %s %s - %s - %s",
115127
service_name,
@@ -138,7 +150,9 @@ def proxy_request(self, request: Request, data: bytes) -> Response:
138150
return response
139151
except Exception as e:
140152
if LOG.isEnabledFor(logging.DEBUG):
141-
LOG.exception("Error when making request to AWS service %s: %s", service_name, e)
153+
LOG.exception(
154+
"Error when making request to AWS service %s: %s", service_name, e
155+
)
142156
return requests_response("", status_code=400)
143157

144158
def register_in_instance(self):
@@ -224,7 +238,10 @@ def _adjust_request_dict(self, service_name: str, request_dict: Dict):
224238
body_str = run_safe(lambda: to_str(req_body)) or ""
225239

226240
# TODO: this custom fix should not be required - investigate and remove!
227-
if "<CreateBucketConfiguration" in body_str and "LocationConstraint" not in body_str:
241+
if (
242+
"<CreateBucketConfiguration" in body_str
243+
and "LocationConstraint" not in body_str
244+
):
228245
region = request_dict["context"]["client_region"]
229246
if region == AWS_REGION_US_EAST_1:
230247
request_dict["body"] = ""
@@ -238,15 +255,19 @@ def _adjust_request_dict(self, service_name: str, request_dict: Dict):
238255
account_id = self._query_account_id_from_aws()
239256
if "QueueUrl" in req_body:
240257
queue_name = req_body["QueueUrl"].split("/")[-1]
241-
req_body["QueueUrl"] = f"https://queue.amazonaws.com/{account_id}/{queue_name}"
258+
req_body["QueueUrl"] = (
259+
f"https://queue.amazonaws.com/{account_id}/{queue_name}"
260+
)
242261
if "QueueOwnerAWSAccountId" in req_body:
243262
req_body["QueueOwnerAWSAccountId"] = account_id
244263
if service_name == "sqs" and request_dict.get("url"):
245264
req_json = run_safe(lambda: json.loads(body_str)) or {}
246265
account_id = self._query_account_id_from_aws()
247266
queue_name = req_json.get("QueueName")
248267
if account_id and queue_name:
249-
request_dict["url"] = f"https://queue.amazonaws.com/{account_id}/{queue_name}"
268+
request_dict["url"] = (
269+
f"https://queue.amazonaws.com/{account_id}/{queue_name}"
270+
)
250271
req_json["QueueOwnerAWSAccountId"] = account_id
251272
request_dict["body"] = to_bytes(json.dumps(req_json))
252273

@@ -256,7 +277,9 @@ def _fix_headers(self, request: Request, service_name: str):
256277
host = request.headers.get("Host") or ""
257278
regex = r"^(https?://)?([0-9.]+|localhost)(:[0-9]+)?"
258279
if re.match(regex, host):
259-
request.headers["Host"] = re.sub(regex, rf"\1s3.{LOCALHOST_HOSTNAME}", host)
280+
request.headers["Host"] = re.sub(
281+
regex, rf"\1s3.{LOCALHOST_HOSTNAME}", host
282+
)
260283
request.headers.pop("Content-Length", None)
261284
request.headers.pop("x-localstack-request-url", None)
262285
request.headers.pop("X-Forwarded-For", None)
@@ -311,7 +334,9 @@ def start_aws_auth_proxy_in_container(
311334
# should consider building pre-baked images for the extension in the future. Also,
312335
# the new packaged CLI binary can help us gain more stability over time...
313336

314-
logging.getLogger("localstack.utils.container_utils.docker_cmd_client").setLevel(logging.INFO)
337+
logging.getLogger("localstack.utils.container_utils.docker_cmd_client").setLevel(
338+
logging.INFO
339+
)
315340
logging.getLogger("localstack.utils.docker_utils").setLevel(logging.INFO)
316341
logging.getLogger("localstack.utils.run").setLevel(logging.INFO)
317342

@@ -328,13 +353,18 @@ def start_aws_auth_proxy_in_container(
328353
image_name = DOCKER_IMAGE_NAME_PRO
329354
# add host mapping for localstack.cloud to localhost to prevent the health check from failing
330355
additional_flags = (
331-
repl_config.PROXY_DOCKER_FLAGS + " --add-host=localhost.localstack.cloud:host-gateway"
356+
repl_config.PROXY_DOCKER_FLAGS
357+
+ " --add-host=localhost.localstack.cloud:host-gateway"
332358
)
333359
DOCKER_CLIENT.create_container(
334360
image_name,
335361
name=container_name,
336362
entrypoint="",
337-
command=["bash", "-c", f"touch {CONTAINER_LOG_FILE}; tail -f {CONTAINER_LOG_FILE}"],
363+
command=[
364+
"bash",
365+
"-c",
366+
f"touch {CONTAINER_LOG_FILE}; tail -f {CONTAINER_LOG_FILE}",
367+
],
338368
ports=ports,
339369
additional_flags=additional_flags,
340370
)
@@ -388,7 +418,10 @@ def start_aws_auth_proxy_in_container(
388418
command = f"{venv_activate}; localstack aws proxy -c {CONTAINER_CONFIG_FILE} -p {port} --host 0.0.0.0 > {CONTAINER_LOG_FILE} 2>&1"
389419
if use_docker_sdk_command:
390420
DOCKER_CLIENT.exec_in_container(
391-
container_name, command=["bash", "-c", command], env_vars=env_vars, interactive=True
421+
container_name,
422+
command=["bash", "-c", command],
423+
env_vars=env_vars,
424+
interactive=True,
392425
)
393426
else:
394427
env_vars_list = []

aws-proxy/aws_proxy/client/http2_server.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ def _do_create():
106106
def _encode_headers(headers):
107107
if RETURN_CASE_SENSITIVE_HEADERS:
108108
return [(key.encode(), value.encode()) for key, value in headers.items()]
109-
return [(key.lower().encode(), value.encode()) for key, value in headers.items()]
109+
return [
110+
(key.lower().encode(), value.encode()) for key, value in headers.items()
111+
]
110112

111113
quart_asgi._encode_headers = quart_asgi.encode_headers = _encode_headers
112114
quart_app.encode_headers = quart_utils.encode_headers = _encode_headers
@@ -116,7 +118,9 @@ def build_and_validate_headers(headers):
116118
for name, value in headers:
117119
if name[0] == b":"[0]:
118120
raise ValueError("Pseudo headers are not valid")
119-
header_name = bytes(name) if RETURN_CASE_SENSITIVE_HEADERS else bytes(name).lower()
121+
header_name = (
122+
bytes(name) if RETURN_CASE_SENSITIVE_HEADERS else bytes(name).lower()
123+
)
120124
validated_headers.append((header_name.strip(), bytes(value).strip()))
121125
return validated_headers
122126

@@ -212,7 +216,9 @@ async def index(path=None):
212216
response.headers.pop("Content-Length", None)
213217
result.headers.pop("Server", None)
214218
result.headers.pop("Date", None)
215-
headers = {k: str(v).replace("\n", r"\n") for k, v in result.headers.items()}
219+
headers = {
220+
k: str(v).replace("\n", r"\n") for k, v in result.headers.items()
221+
}
216222
response.headers.update(headers)
217223
# set multi-value headers
218224
multi_value_headers = getattr(result, "multi_value_headers", {})

aws-proxy/aws_proxy/server/aws_request_forwarder.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ class AwsProxyHandler(Handler):
3232
# maps port numbers to proxy instances
3333
PROXY_INSTANCES: Dict[int, ProxyInstance] = {}
3434

35-
def __call__(self, chain: HandlerChain, context: RequestContext, response: Response):
35+
def __call__(
36+
self, chain: HandlerChain, context: RequestContext, response: Response
37+
):
3638
proxy = self.select_proxy(context)
3739
if not proxy:
3840
return
@@ -63,7 +65,9 @@ def select_proxy(self, context: RequestContext) -> Optional[ProxyInstance]:
6365
proxy = self.PROXY_INSTANCES[port]
6466
proxy_config = proxy.get("config") or {}
6567
services = proxy_config.get("services") or {}
66-
service_name = self._get_canonical_service_name(context.service.service_name)
68+
service_name = self._get_canonical_service_name(
69+
context.service.service_name
70+
)
6771
service_config = services.get(service_name)
6872
if not service_config:
6973
continue
@@ -100,7 +104,9 @@ def _request_matches_resource(
100104
self, context: RequestContext, resource_name_pattern: str
101105
) -> bool:
102106
try:
103-
service_name = self._get_canonical_service_name(context.service.service_name)
107+
service_name = self._get_canonical_service_name(
108+
context.service.service_name
109+
)
104110
if service_name == "s3":
105111
bucket_name = context.service_request.get("Bucket") or ""
106112
s3_bucket_arn = arns.s3_bucket_arn(bucket_name)
@@ -113,7 +119,9 @@ def _request_matches_resource(
113119
queue_name,
114120
queue_url,
115121
sqs_queue_arn(
116-
queue_name, account_id=context.account_id, region_name=context.region
122+
queue_name,
123+
account_id=context.account_id,
124+
region_name=context.region,
117125
),
118126
)
119127
for candidate in candidates:
@@ -133,12 +141,16 @@ def _request_matches_resource(
133141
) from e
134142
return True
135143

136-
def forward_request(self, context: RequestContext, proxy: ProxyInstance) -> requests.Response:
144+
def forward_request(
145+
self, context: RequestContext, proxy: ProxyInstance
146+
) -> requests.Response:
137147
"""Forward the given request to the proxy instance, and return the response."""
138148
port = proxy["port"]
139149
request = context.request
140150
target_host = get_addressable_container_host(default_local_hostname=LOCALHOST)
141-
url = f"http://{target_host}:{port}{request.path}?{to_str(request.query_string)}"
151+
url = (
152+
f"http://{target_host}:{port}{request.path}?{to_str(request.query_string)}"
153+
)
142154

143155
# inject Auth header, to ensure we're passing the right region to the proxy (e.g., for Cognito InitiateAuth)
144156
self._extract_region_from_domain(context)
@@ -156,10 +168,20 @@ def forward_request(self, context: RequestContext, proxy: ProxyInstance) -> requ
156168
data = request.form
157169
elif request.data:
158170
data = request.data
159-
LOG.debug("Forward request: %s %s - %s - %s", request.method, url, dict(headers), data)
171+
LOG.debug(
172+
"Forward request: %s %s - %s - %s",
173+
request.method,
174+
url,
175+
dict(headers),
176+
data,
177+
)
160178
# construct response
161179
result = requests.request(
162-
method=request.method, url=url, data=data, headers=dict(headers), stream=True
180+
method=request.method,
181+
url=url,
182+
data=data,
183+
headers=dict(headers),
184+
stream=True,
163185
)
164186
# TODO: ugly hack for now, simply attaching an additional attribute for raw response content
165187
result.raw_content = result.raw.read()
@@ -173,7 +195,10 @@ def forward_request(self, context: RequestContext, proxy: ProxyInstance) -> requ
173195
)
174196
except requests.exceptions.ConnectionError:
175197
# remove unreachable proxy
176-
LOG.info("Removing unreachable AWS forward proxy due to connection issue: %s", url)
198+
LOG.info(
199+
"Removing unreachable AWS forward proxy due to connection issue: %s",
200+
url,
201+
)
177202
self.PROXY_INSTANCES.pop(port, None)
178203
return result
179204

@@ -186,7 +211,10 @@ def _is_read_request(self, context: RequestContext) -> bool:
186211
if operation_name.lower().startswith(("describe", "get", "list", "query")):
187212
return True
188213
# service-specific rules
189-
if context.service.service_name == "cognito-idp" and operation_name == "InitiateAuth":
214+
if (
215+
context.service.service_name == "cognito-idp"
216+
and operation_name == "InitiateAuth"
217+
):
190218
return True
191219
if context.service.service_name == "dynamodb" and operation_name in {
192220
"Scan",

0 commit comments

Comments
 (0)