Skip to content

Commit

Permalink
remove deprecated ProxyListener for starting local aws-replicator pro…
Browse files Browse the repository at this point in the history
…xy server (#38)
  • Loading branch information
whummer authored Nov 10, 2023
1 parent 43aee35 commit 9c397ad
Show file tree
Hide file tree
Showing 9 changed files with 76 additions and 44 deletions.
18 changes: 10 additions & 8 deletions .github/workflows/aws-replicator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ jobs:
# TODO remove
mkdir ~/.localstack; echo '{"token":"test"}' > ~/.localstack/auth.json
branchName=${GITHUB_HEAD_REF##*/}
if [ "$branchName" = "" ]; then branchName=main; fi
echo "Installing from branch name $branchName"
localstack extensions init
localstack extensions install "git+https://github.com/localstack/localstack-extensions.git@"$branchName"#egg=localstack-extension-aws-replicator&subdirectory=aws-replicator"
# install dependencies
sudo apt-get update
sudo apt-get install -y libsasl2-dev
(cd aws-replicator; pip install -e .)
# build and install extension
localstack extensions init
(
cd aws-replicator
make install && make build && make enable
)
# install awslocal/tflocal command lines
pip install awscli-local[ver1]
Expand All @@ -63,7 +63,6 @@ jobs:
- name: Run linter
run: |
cd aws-replicator
make install
(. .venv/bin/activate; pip install --upgrade --pre localstack localstack-ext)
make lint
Expand All @@ -81,7 +80,10 @@ jobs:
AWS_DEFAULT_REGION: us-east-1
AWS_ACCESS_KEY_ID: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }}
LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }}
run: |
# TODO tmp fix for https://github.com/localstack/localstack/issues/8267:
pip install --upgrade 'botocore<1.31.81'
cd aws-replicator/example
make test
Expand Down
12 changes: 11 additions & 1 deletion aws-replicator/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,20 @@ test: venv
dist: venv
$(VENV_RUN); python setup.py sdist bdist_wheel

build: ## Build the extension
mkdir -p build
cp -r setup.py setup.cfg README.md aws_replicator build/
(cd build && python setup.py sdist)

enable: $(wildcard ./build/dist/localstack-extension-aws-replicator-*.tar.gz) ## Enable the extension in LocalStack
$(VENV_RUN); \
pip uninstall --yes localstack-extension-aws-replicator; \
localstack extensions -v install file://./$?

publish: clean-dist venv dist
$(VENV_RUN); pip install --upgrade twine; twine upload dist/*

clean-dist: clean
rm -rf dist/

.PHONY: clean clean-dist dist install publish test
.PHONY: build clean clean-dist dist install publish test
1 change: 1 addition & 0 deletions aws-replicator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ localstack extensions install "git+https://github.com/localstack/localstack-exte

## Change Log

* `0.1.2`: Remove deprecated ProxyListener for starting local aws-replicator proxy server
* `0.1.1`: Add simple configuration Web UI
* `0.1.0`: Initial version of extension

Expand Down
47 changes: 25 additions & 22 deletions aws-replicator/aws_replicator/client/auth_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@
from localstack.aws.spec import load_service
from localstack.config import get_edge_url
from localstack.constants import AWS_REGION_US_EAST_1, DOCKER_IMAGE_NAME_PRO
from localstack.services.generic_proxy import ProxyListener, start_proxy_server
from localstack.http import Request
from localstack.utils.aws.aws_responses import requests_response
from localstack.utils.bootstrap import setup_logging
from localstack.utils.collections import select_attributes
from localstack.utils.container_utils.container_client import PortMappings
from localstack.utils.docker_utils import DOCKER_CLIENT, reserve_available_container_port
from localstack.utils.files import new_tmp_file, save_file
from localstack.utils.functions import run_safe
from localstack.utils.net import get_free_tcp_port
from localstack.utils.server.http2_server import run_server
from localstack.utils.serving import Server
from localstack.utils.strings import short_uid, to_str, truncate
from localstack_ext.bootstrap.licensing import ENV_LOCALSTACK_API_KEY
from requests import Response

from aws_replicator.client.utils import truncate_content
from aws_replicator.config import HANDLER_PATH_PROXIES
Expand All @@ -47,6 +50,9 @@
CONTAINER_CONFIG_FILE = "/tmp/ls.aws.proxy.yml"
CONTAINER_LOG_FILE = "/tmp/ls-aws-proxy.log"

# default bind host if `bind_host` is not specified for the proxy
DEFAULT_BIND_HOST = "127.0.0.1"


class AuthProxyAWS(Server):
def __init__(self, config: ProxyConfig, port: int = None):
Expand All @@ -55,34 +61,30 @@ def __init__(self, config: ProxyConfig, port: int = None):
super().__init__(port=port)

def do_run(self):
class Handler(ProxyListener):
def forward_request(_self, method, path, data, headers):
return self.proxy_request(method, path, data, headers)

self.register_in_instance()
# TODO: change to using Gateway!
proxy = start_proxy_server(self.port, update_listener=Handler())
bind_host = self.config.get("bind_host") or DEFAULT_BIND_HOST
proxy = run_server(port=self.port, bind_addresses=[bind_host], handler=self.proxy_request)
proxy.join()

def proxy_request(self, method, path, data, headers):
parsed = self._extract_region_and_service(headers)
def proxy_request(self, request: Request, data: bytes) -> Response:
parsed = self._extract_region_and_service(request.headers)
if not parsed:
return 400
return requests_response("", status_code=400)
region_name, service_name = parsed

LOG.debug(
"Proxying request to %s (%s): %s %s",
service_name,
region_name,
method,
path,
request.method,
request.path,
)

path, _, query_string = path.partition("?")
path, _, query_string = request.path.partition("?")
request = HttpRequest(
body=data,
method=method,
headers=headers,
method=request.method,
headers=request.headers,
path=path,
query_string=query_string,
)
Expand All @@ -104,7 +106,7 @@ def proxy_request(self, method, path, data, headers):
LOG.debug(
"Sending request for service %s to AWS: %s %s - %s - %s",
service_name,
method,
request.method,
aws_request.url,
truncate_content(request_dict.get("body"), max_length=500),
headers_truncated,
Expand All @@ -113,11 +115,12 @@ def proxy_request(self, method, path, data, headers):
# send request to upstream AWS
result = client._endpoint.make_request(operation_model, request_dict)

# create response object
response = requests.Response()
response.status_code = result[0].status_code
response._content = result[0].content
response.headers = dict(result[0].headers)
# create response object - TODO: to be replaced with localstack.http.Response over time
response = requests_response(
result[0].content,
status_code=result[0].status_code,
headers=dict(result[0].headers),
)

LOG.debug(
"Received response for service %s from AWS: %s - %s",
Expand All @@ -129,7 +132,7 @@ def proxy_request(self, method, path, data, headers):
except Exception as e:
if LOG.isEnabledFor(logging.DEBUG):
LOG.exception("Error when making request to AWS service %s: %s", service_name, e)
return 400
return requests_response("", status_code=400)

def register_in_instance(self):
port = getattr(self, "port", None)
Expand Down
9 changes: 7 additions & 2 deletions aws-replicator/aws_replicator/client/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ def attach(self, cli: LocalstackCli) -> None:
help="Path to config file for detailed proxy configurations",
required=False,
)
@click.option(
"--host",
help="Network bind host to expose the proxy process on (default: 127.0.0.1)",
required=False,
)
@click.option(
"--container",
help="Run the proxy in a container and not on the host",
Expand All @@ -51,13 +56,13 @@ def attach(self, cli: LocalstackCli) -> None:
help="Custom port to run the proxy on (by default a random port is used)",
required=False,
)
def cmd_aws_proxy(services: str, config: str, container: bool, port: int):
def cmd_aws_proxy(services: str, config: str, container: bool, port: int, host: str):
from aws_replicator.client.auth_proxy import (
start_aws_auth_proxy,
start_aws_auth_proxy_in_container,
)

config_json: ProxyConfig = {"services": {}}
config_json: ProxyConfig = {"services": {}, "bind_host": host}
if config:
config_json = yaml.load(load_file(config), Loader=yaml.SafeLoader)
if services:
Expand Down
2 changes: 2 additions & 0 deletions aws-replicator/aws_replicator/shared/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class ProxyServiceConfig(TypedDict, total=False):
class ProxyConfig(TypedDict, total=False):
# maps service name to service proxy configs
services: Dict[str, ProxyServiceConfig]
# bind host for the proxy (defaults to 127.0.0.1)
bind_host: str


class ProxyInstance(TypedDict):
Expand Down
4 changes: 2 additions & 2 deletions aws-replicator/example/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ test: ## Run the end-to-end test with a simple sample app
aws sqs create-queue --queue-name test-queue1; \
queueUrl=$$(aws sqs get-queue-url --queue-name test-queue1 | jq -r .QueueUrl); \
echo "Starting AWS replicator proxy"; \
(DEBUG=1 localstack aws proxy -s s3,sqs & ); \
(DEBUG=1 localstack aws proxy -s s3,sqs --host 0.0.0.0 & ); \
echo "Deploying Terraform template locally"; \
tflocal init; \
tflocal apply -auto-approve; \
echo "Puting a message to the queue in real AWS"; \
echo "Putting a message to the queue in real AWS"; \
aws sqs send-message --queue-url $$queueUrl --message-body '{"test":"foobar 123"}'; \
echo "Waiting a bit for Lambda to be triggered by SQS message ..."; \
sleep 7 # ; \
Expand Down
5 changes: 3 additions & 2 deletions aws-replicator/setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = localstack-extension-aws-replicator
version = 0.1.1
version = 0.1.2
summary = LocalStack Extension: AWS replicator
description = Replicate AWS resources into your LocalStack instance
long_description = file: README.md
Expand All @@ -16,7 +16,8 @@ install_requires =
# TODO: currently requires a version pin, see note in auth_proxy.py
boto3>=1.26.151
# TODO: currently requires a version pin, see note in auth_proxy.py
botocore>=1.29.151
# TODO: upper version pin due to https://github.com/localstack/localstack/issues/8267
botocore>=1.29.151,<1.31.81
flask
localstack
localstack-client
Expand Down
22 changes: 15 additions & 7 deletions aws-replicator/tests/test_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
from aws_replicator.client.auth_proxy import start_aws_auth_proxy
from aws_replicator.shared.models import ProxyConfig

# binding proxy to 0.0.0.0 to enable testing in CI
PROXY_BIND_HOST = "0.0.0.0"


@pytest.fixture
def start_aws_proxy():
Expand All @@ -34,7 +37,7 @@ def _start(config: dict = None):
@pytest.mark.parametrize("metadata_gzip", [True, False])
def test_s3_requests(start_aws_proxy, s3_create_bucket, metadata_gzip):
# start proxy
config = ProxyConfig(services={"s3": {"resources": ".*"}})
config = ProxyConfig(services={"s3": {"resources": ".*"}}, bind_host=PROXY_BIND_HOST)
start_aws_proxy(config)

# create clients
Expand All @@ -43,14 +46,14 @@ def test_s3_requests(start_aws_proxy, s3_create_bucket, metadata_gzip):

# list buckets to assert that proxy is up and running
buckets_proxied = s3_client.list_buckets()["Buckets"]
bucket_aws = s3_client_aws.list_buckets()["Buckets"]
assert buckets_proxied == bucket_aws
buckets_aws = s3_client_aws.list_buckets()["Buckets"]
assert buckets_proxied == buckets_aws

# create bucket
bucket = s3_create_bucket()
buckets_proxied = s3_client.list_buckets()["Buckets"]
bucket_aws = s3_client_aws.list_buckets()["Buckets"]
assert buckets_proxied and buckets_proxied == bucket_aws
buckets_aws = s3_client_aws.list_buckets()["Buckets"]
assert buckets_proxied and buckets_proxied == buckets_aws

# put object
key = "test-key-with-urlencoded-chars-:+"
Expand All @@ -77,6 +80,9 @@ def test_s3_requests(start_aws_proxy, s3_create_bucket, metadata_gzip):
# list objects v2
result_aws = s3_client_aws.list_objects_v2(Bucket=bucket, **kwargs)
result_proxied = s3_client.list_objects_v2(Bucket=bucket, **kwargs)
# TODO: for some reason, the proxied result may contain 'Owner', whereas result_aws does not
for res in result_proxied["Contents"]:
res.pop("Owner", None)
assert result_proxied["Contents"] == result_aws["Contents"]

# delete object
Expand Down Expand Up @@ -108,7 +114,9 @@ def test_sqs_requests(start_aws_proxy, cleanups):
queue_name_local = "test-queue-local"

# start proxy - only forwarding requests for queue name `test-queue-aws`
config = ProxyConfig(services={"sqs": {"resources": f".*:{queue_name_aws}"}})
config = ProxyConfig(
services={"sqs": {"resources": f".*:{queue_name_aws}"}}, bind_host=PROXY_BIND_HOST
)
start_aws_proxy(config)

# create clients
Expand All @@ -119,7 +127,7 @@ def test_sqs_requests(start_aws_proxy, cleanups):
# create queue in AWS
sqs_client_aws.create_queue(QueueName=queue_name_aws)
queue_url_aws = sqs_client_aws.get_queue_url(QueueName=queue_name_aws)["QueueUrl"]
queue_arn_aws = sqs_client.get_queue_attributes(
queue_arn_aws = sqs_client_aws.get_queue_attributes(
QueueUrl=queue_url_aws, AttributeNames=["QueueArn"]
)["Attributes"]["QueueArn"]
cleanups.append(lambda: sqs_client_aws.delete_queue(QueueUrl=queue_url_aws))
Expand Down

0 comments on commit 9c397ad

Please sign in to comment.