From ac8bcc038b745cbe8cfb88311f7fb3b9a499ab1a Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Tue, 16 Sep 2025 22:53:21 +0200 Subject: [PATCH 1/7] rename extension from aws-replicator to aws-proxy --- .github/workflows/aws-replicator.yml | 16 +++++----- CODEOWNERS | 2 +- README.md | 18 +++++------ {aws-replicator => aws-proxy}/.gitignore | 0 aws-proxy/MANIFEST.in | 3 ++ {aws-replicator => aws-proxy}/Makefile | 6 ++-- {aws-replicator => aws-proxy}/README.md | 10 +++---- aws-proxy/aws_proxy/__init__.py | 1 + .../aws_proxy}/client/__init__.py | 0 .../aws_proxy}/client/auth_proxy.py | 14 ++++----- .../aws_proxy}/client/cli.py | 28 ++++-------------- .../aws_proxy}/client/http2_server.py | 0 .../aws_proxy}/client/replicate.py | 6 ++-- .../aws_proxy}/client/resource_types.json | 0 .../aws_proxy}/client/service_states.py | 8 +++-- .../aws_proxy}/client/utils.py | 4 +-- .../aws_proxy}/config.py | 8 ++--- .../aws_proxy}/server/__init__.py | 0 .../server/aws_request_forwarder.py | 4 +-- .../aws_proxy}/server/extension.py | 0 .../aws_proxy}/server/request_handler.py | 12 ++++---- .../aws_proxy}/server/resource_replicator.py | 0 .../aws_proxy}/server/ui/__init__.py | 0 .../aws_proxy}/server/ui/app.js | 0 .../aws_proxy}/server/ui/favicon.png | Bin .../aws_proxy}/server/ui/index.html | 0 .../aws_proxy}/shared/__init__.py | 0 .../aws_proxy}/shared/constants.py | 0 .../aws_proxy}/shared/models.py | 0 .../aws_proxy}/shared/utils.py | 0 .../etc/aws-replicate-overview.png | Bin .../etc/proxy-settings.png | Bin .../example/Makefile | 0 .../example/README.md | 0 .../example/lambda.py | 0 {aws-replicator => aws-proxy}/example/main.tf | 0 .../example/proxy_config.yml | 0 {aws-replicator => aws-proxy}/logo.png | Bin {aws-replicator => aws-proxy}/pyproject.toml | 2 +- {aws-replicator => aws-proxy}/setup.cfg | 10 +++---- {aws-replicator => aws-proxy}/setup.py | 0 .../tests/__init__.py | 0 .../tests/conftest.py | 0 .../tests/test_config.py | 4 +-- .../tests/test_proxy_requests.py | 4 +-- aws-replicator/MANIFEST.in | 3 -- aws-replicator/aws_replicator/__init__.py | 1 - 47 files changed, 75 insertions(+), 89 deletions(-) rename {aws-replicator => aws-proxy}/.gitignore (100%) create mode 100644 aws-proxy/MANIFEST.in rename {aws-replicator => aws-proxy}/Makefile (85%) rename {aws-replicator => aws-proxy}/README.md (95%) create mode 100644 aws-proxy/aws_proxy/__init__.py rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/client/__init__.py (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/client/auth_proxy.py (97%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/client/cli.py (76%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/client/http2_server.py (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/client/replicate.py (98%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/client/resource_types.json (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/client/service_states.py (96%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/client/utils.py (86%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/config.py (60%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/server/__init__.py (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/server/aws_request_forwarder.py (98%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/server/extension.py (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/server/request_handler.py (93%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/server/resource_replicator.py (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/server/ui/__init__.py (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/server/ui/app.js (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/server/ui/favicon.png (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/server/ui/index.html (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/shared/__init__.py (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/shared/constants.py (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/shared/models.py (100%) rename {aws-replicator/aws_replicator => aws-proxy/aws_proxy}/shared/utils.py (100%) rename {aws-replicator => aws-proxy}/etc/aws-replicate-overview.png (100%) rename {aws-replicator => aws-proxy}/etc/proxy-settings.png (100%) rename {aws-replicator => aws-proxy}/example/Makefile (100%) rename {aws-replicator => aws-proxy}/example/README.md (100%) rename {aws-replicator => aws-proxy}/example/lambda.py (100%) rename {aws-replicator => aws-proxy}/example/main.tf (100%) rename {aws-replicator => aws-proxy}/example/proxy_config.yml (100%) rename {aws-replicator => aws-proxy}/logo.png (100%) rename {aws-replicator => aws-proxy}/pyproject.toml (77%) rename {aws-replicator => aws-proxy}/setup.cfg (84%) rename {aws-replicator => aws-proxy}/setup.py (100%) rename {aws-replicator => aws-proxy}/tests/__init__.py (100%) rename {aws-replicator => aws-proxy}/tests/conftest.py (100%) rename {aws-replicator => aws-proxy}/tests/test_config.py (77%) rename {aws-replicator => aws-proxy}/tests/test_proxy_requests.py (99%) delete mode 100644 aws-replicator/MANIFEST.in delete mode 100644 aws-replicator/aws_replicator/__init__.py diff --git a/.github/workflows/aws-replicator.yml b/.github/workflows/aws-replicator.yml index 8e6e398..f412dcc 100644 --- a/.github/workflows/aws-replicator.yml +++ b/.github/workflows/aws-replicator.yml @@ -1,15 +1,15 @@ -name: LocalStack AWS Replicator Extension Tests +name: LocalStack AWS Proxy Extension Tests on: push: paths: - - aws-replicator/** + - aws-proxy/** branches: - main pull_request: paths: - - .github/workflows/aws-replicator.yml - - aws-replicator/** + - .github/workflows/aws-proxy.yml + - aws-proxy/** workflow_dispatch: jobs: @@ -49,7 +49,7 @@ jobs: # build and install extension localstack extensions init ( - cd aws-replicator + cd aws-proxy make install . .venv/bin/activate pip install --upgrade --pre localstack localstack-ext @@ -68,7 +68,7 @@ jobs: - name: Run linter run: | - cd aws-replicator + cd aws-proxy (. .venv/bin/activate; pip install --upgrade --pre localstack localstack-ext) make lint @@ -78,7 +78,7 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }} run: | - cd aws-replicator + cd aws-proxy make test - name: Deploy and test sample app @@ -88,7 +88,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }} LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} run: | - cd aws-replicator/example + cd aws-proxy/example make test - name: Print LocalStack logs diff --git a/CODEOWNERS b/CODEOWNERS index 537cb8e..5b72205 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -12,7 +12,7 @@ ### Extensions ### ###################### -/aws-replicator/ @whummer +/aws-proxy/ @whummer @nik-localstack /diagnosis-viewer/ @thrau @silv-io /hello-world/ @lukqw @thrau /http-bin/ @thrau @dominikschubert diff --git a/README.md b/README.md index acc8756..15a71f8 100644 --- a/README.md +++ b/README.md @@ -65,16 +65,16 @@ $ localstack extensions install "git+https://github.com/localstack/localstack-ex Here is the current list of extensions developed by the LocalStack team and their support status. You can install the respective extension by calling `localstack install `. -| Extension | Install name | Version | Support status | -| --------- | ------------ | ------- | -------------- | -| [AWS replicator](https://github.com/localstack/localstack-extensions/tree/main/aws-replicator) | localstack-extension-aws-replicator | 0.1.7 | Experimental | +| Extension | Install name | Version | Support status | +|----------------------------------------------------------------------------------------------------| ------------ | ------- | -------------- | +| [AWS Proxy](https://github.com/localstack/localstack-extensions/tree/main/aws-proxy) | localstack-extension-aws-proxy | 0.1.7 | Experimental | | [Diagnosis Viewer](https://github.com/localstack/localstack-extensions/tree/main/diagnosis-viewer) | localstack-extension-diagnosis-viewer | 0.1.0 | Stable | -| [Hello World](https://github.com/localstack/localstack-extensions/tree/main/hello-world) | localstack-extension-hello-world | 0.1.0 | Stable | -| [httpbin](https://github.com/localstack/localstack-extensions/tree/main/httpbin) | localstack-extension-httpbin | 0.1.0 | Stable | -| [MailHog](https://github.com/localstack/localstack-extensions/tree/main/mailhog) | localstack-extension-mailhog | 0.1.0 | Stable | -| [Miniflare](https://github.com/localstack/localstack-extensions/tree/main/miniflare) | localstack-extension-miniflare | 0.1.0 | Experimental | -| [Stripe](https://github.com/localstack/localstack-extensions/tree/main/stripe) | localstack-extension-stripe | 0.2.0 | Stable | -| [Terraform Init](https://github.com/localstack/localstack-extensions/tree/main/terraform-init) | localstack-extension-terraform-init | 0.2.0 | Experimental | +| [Hello World](https://github.com/localstack/localstack-extensions/tree/main/hello-world) | localstack-extension-hello-world | 0.1.0 | Stable | +| [httpbin](https://github.com/localstack/localstack-extensions/tree/main/httpbin) | localstack-extension-httpbin | 0.1.0 | Stable | +| [MailHog](https://github.com/localstack/localstack-extensions/tree/main/mailhog) | localstack-extension-mailhog | 0.1.0 | Stable | +| [Miniflare](https://github.com/localstack/localstack-extensions/tree/main/miniflare) | localstack-extension-miniflare | 0.1.0 | Experimental | +| [Stripe](https://github.com/localstack/localstack-extensions/tree/main/stripe) | localstack-extension-stripe | 0.2.0 | Stable | +| [Terraform Init](https://github.com/localstack/localstack-extensions/tree/main/terraform-init) | localstack-extension-terraform-init | 0.2.0 | Experimental | ## Developing Extensions diff --git a/aws-replicator/.gitignore b/aws-proxy/.gitignore similarity index 100% rename from aws-replicator/.gitignore rename to aws-proxy/.gitignore diff --git a/aws-proxy/MANIFEST.in b/aws-proxy/MANIFEST.in new file mode 100644 index 0000000..8eb6be3 --- /dev/null +++ b/aws-proxy/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include aws_proxy *.html +recursive-include aws_proxy *.js +recursive-include aws_proxy *.png diff --git a/aws-replicator/Makefile b/aws-proxy/Makefile similarity index 85% rename from aws-replicator/Makefile rename to aws-proxy/Makefile index 4544cc2..f832448 100644 --- a/aws-replicator/Makefile +++ b/aws-proxy/Makefile @@ -40,12 +40,12 @@ dist: venv build: ## Build the extension mkdir -p build - cp -r setup.py setup.cfg README.md aws_replicator build/ + cp -r setup.py setup.cfg README.md aws_proxy build/ (cd build && python setup.py sdist) -enable: $(wildcard ./build/dist/localstack_extension_aws_replicator-*.tar.gz) ## Enable the extension in LocalStack +enable: $(wildcard ./build/dist/localstack_extension_aws_proxy-*.tar.gz) ## Enable the extension in LocalStack $(VENV_RUN); \ - pip uninstall --yes localstack-extension-aws-replicator; \ + pip uninstall --yes localstack-extension-aws-proxy; \ localstack extensions -v install file://$? publish: clean-dist venv dist diff --git a/aws-replicator/README.md b/aws-proxy/README.md similarity index 95% rename from aws-replicator/README.md rename to aws-proxy/README.md index 0d36ff0..d743dc2 100644 --- a/aws-replicator/README.md +++ b/aws-proxy/README.md @@ -1,13 +1,13 @@ AWS Cloud Proxy Extension (experimental) ======================================== -[![Install LocalStack Extension](https://localstack.cloud/gh/extension-badge.svg)](https://app.localstack.cloud/extensions/remote?url=git+https://github.com/localstack/localstack-extensions/#egg=localstack-extension-aws-replicator&subdirectory=aws-replicator) +[![Install LocalStack Extension](https://localstack.cloud/gh/extension-badge.svg)](https://app.localstack.cloud/extensions/remote?url=git+https://github.com/localstack/localstack-extensions/#egg=localstack-extension-aws-proxy&subdirectory=aws-proxy) A LocalStack extension to proxy and integrate AWS resources into your local machine. This enables one flavor of "hybrid" or "remocal" setups where you can easily bridge the gap between LocalStack (local resources) and remote AWS (resources in the real cloud). ⚠️ Please note that this extension is experimental and still under active development. -⚠️ Note: Given that the scope of this extension has recently changed (see [below](#resource-replicator-cli-deprecated)), it may get renamed from `aws-replicator` to `cloud-proxy` in an upcoming release. +⚠️ Note: Given that the scope of this extension has recently changed (see [below](#resource-proxy-cli-deprecated)) - it has been recently renamed from `aws-replicator` to `aws-proxy`. ## Prerequisites @@ -33,11 +33,11 @@ For example, in order to forward all API calls for DynamoDB/S3/Cognito to real A ``` $ localstack start -d ``` -2. Enable LocalStack AWS replicator from the Web Application Extension Library +2. Enable LocalStack AWS Proxy from the Web Application Extension Library 3. After installation restart Localstack -4. Install the AWS replicator CLI package +4. Install the AWS Proxy CLI package ``` -$ pip install localstack-extension-aws-replicator +$ pip install localstack-extension-aws-proxy ``` 5. Configure real cloud account credentials in a new terminal session to allow access ``` diff --git a/aws-proxy/aws_proxy/__init__.py b/aws-proxy/aws_proxy/__init__.py new file mode 100644 index 0000000..747d860 --- /dev/null +++ b/aws-proxy/aws_proxy/__init__.py @@ -0,0 +1 @@ +name = "aws-proxy" diff --git a/aws-replicator/aws_replicator/client/__init__.py b/aws-proxy/aws_proxy/client/__init__.py similarity index 100% rename from aws-replicator/aws_replicator/client/__init__.py rename to aws-proxy/aws_proxy/client/__init__.py diff --git a/aws-replicator/aws_replicator/client/auth_proxy.py b/aws-proxy/aws_proxy/client/auth_proxy.py similarity index 97% rename from aws-replicator/aws_replicator/client/auth_proxy.py rename to aws-proxy/aws_proxy/client/auth_proxy.py index d438417..fd17b2b 100644 --- a/aws-replicator/aws_replicator/client/auth_proxy.py +++ b/aws-proxy/aws_proxy/client/auth_proxy.py @@ -29,11 +29,11 @@ from localstack.utils.strings import short_uid, to_bytes, to_str, truncate from requests import Response -from aws_replicator import config as repl_config -from aws_replicator.client.utils import truncate_content -from aws_replicator.config import HANDLER_PATH_PROXIES -from aws_replicator.shared.constants import HEADER_HOST_ORIGINAL -from aws_replicator.shared.models import AddProxyRequest, ProxyConfig +from aws_proxy import config as repl_config +from aws_proxy.client.utils import truncate_content +from aws_proxy.config import HANDLER_PATH_PROXIES +from aws_proxy.shared.constants import HEADER_HOST_ORIGINAL +from aws_proxy.shared.models import AddProxyRequest, ProxyConfig from .http2_server import run_server @@ -55,9 +55,9 @@ LOG.setLevel(logging.DEBUG) # TODO make configurable -CLI_PIP_PACKAGE = "localstack-extension-aws-replicator" +CLI_PIP_PACKAGE = "localstack-extension-aws-proxy" # note: enable the line below temporarily for testing: -# CLI_PIP_PACKAGE = "git+https://github.com/localstack/localstack-extensions/@branch#egg=localstack-extension-aws-replicator&subdirectory=aws-replicator" +# CLI_PIP_PACKAGE = "git+https://github.com/localstack/localstack-extensions/@branch#egg=localstack-extension-aws-proxy&subdirectory=aws-proxy" CONTAINER_NAME_PREFIX = "ls-aws-proxy-" CONTAINER_CONFIG_FILE = "/tmp/ls.aws.proxy.yml" diff --git a/aws-replicator/aws_replicator/client/cli.py b/aws-proxy/aws_proxy/client/cli.py similarity index 76% rename from aws-replicator/aws_replicator/client/cli.py rename to aws-proxy/aws_proxy/client/cli.py index 6ee9748..1c3df17 100644 --- a/aws-replicator/aws_replicator/client/cli.py +++ b/aws-proxy/aws_proxy/client/cli.py @@ -7,7 +7,7 @@ from localstack.logging.setup import setup_logging from localstack.utils.files import load_file -from aws_replicator.shared.models import ProxyConfig, ProxyServiceConfig +from aws_proxy.shared.models import ProxyConfig, ProxyServiceConfig try: from localstack.pro.core.bootstrap.auth import get_platform_auth_headers @@ -20,8 +20,8 @@ from localstack.pro.core.config import is_api_key_configured as is_auth_token_configured -class AwsReplicatorPlugin(LocalstackCliPlugin): - name = "aws-replicator" +class AwsProxyPlugin(LocalstackCliPlugin): + name = "aws-proxy" def should_load(self) -> bool: return _is_logged_in() or is_auth_token_configured() @@ -31,7 +31,6 @@ def attach(self, cli: LocalstackCli) -> None: if not group.get_command(ctx=None, cmd_name="aws"): group.add_command(aws) aws.add_command(cmd_aws_proxy) - aws.add_command(cmd_aws_replicate) # TODO: remove over time as we're phasing out the `login` command @@ -47,7 +46,7 @@ def _is_logged_in() -> bool: @click.option( "-s", "--services", - help="Comma-delimited list of services to replicate (e.g., sqs,s3)", + help="Comma-delimited list of services to proxy (e.g., sqs,s3)", required=False, ) @click.option( @@ -74,7 +73,7 @@ def _is_logged_in() -> bool: required=False, ) 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_in_container + from aws_proxy.client.auth_proxy import start_aws_auth_proxy_in_container config_json: ProxyConfig = {"services": {}} if config: @@ -90,7 +89,7 @@ def cmd_aws_proxy(services: str, config: str, container: bool, port: int, host: return start_aws_auth_proxy_in_container(config_json) # note: deferring the import here, to avoid import errors in CLI context - from aws_replicator.client.auth_proxy import start_aws_auth_proxy + from aws_proxy.client.auth_proxy import start_aws_auth_proxy proxy = start_aws_auth_proxy(config_json, port=port) proxy.join() @@ -99,20 +98,5 @@ def cmd_aws_proxy(services: str, config: str, container: bool, port: int, host: sys.exit(1) -@click.command(name="replicate", help="Replicate the state of an AWS account into LocalStack") -@click.option( - "-s", - "--services", - help="Comma-delimited list of services to replicate (e.g., sqs,s3)", - required=True, -) -def cmd_aws_replicate(services: str): - from aws_replicator.client.replicate import replicate_state_into_local - - setup_logging() - services = _split_string(services) - replicate_state_into_local(services) - - def _split_string(string): return [s.strip().lower() for s in re.split(r"[\s,]+", string) if s.strip()] diff --git a/aws-replicator/aws_replicator/client/http2_server.py b/aws-proxy/aws_proxy/client/http2_server.py similarity index 100% rename from aws-replicator/aws_replicator/client/http2_server.py rename to aws-proxy/aws_proxy/client/http2_server.py diff --git a/aws-replicator/aws_replicator/client/replicate.py b/aws-proxy/aws_proxy/client/replicate.py similarity index 98% rename from aws-replicator/aws_replicator/client/replicate.py rename to aws-proxy/aws_proxy/client/replicate.py index 362ea24..61bb4fd 100644 --- a/aws-replicator/aws_replicator/client/replicate.py +++ b/aws-proxy/aws_proxy/client/replicate.py @@ -11,13 +11,13 @@ from localstack.utils.json import extract_jsonpath from localstack.utils.threads import parallelize -from aws_replicator.client.utils import post_request_to_instance -from aws_replicator.shared.models import ( +from aws_proxy.client.utils import post_request_to_instance +from aws_proxy.shared.models import ( ExtendedResourceStateReplicator, ReplicateStateRequest, ResourceReplicator, ) -from aws_replicator.shared.utils import list_all_resources +from aws_proxy.shared.utils import list_all_resources LOG = logging.getLogger(__name__) diff --git a/aws-replicator/aws_replicator/client/resource_types.json b/aws-proxy/aws_proxy/client/resource_types.json similarity index 100% rename from aws-replicator/aws_replicator/client/resource_types.json rename to aws-proxy/aws_proxy/client/resource_types.json diff --git a/aws-replicator/aws_replicator/client/service_states.py b/aws-proxy/aws_proxy/client/service_states.py similarity index 96% rename from aws-replicator/aws_replicator/client/service_states.py rename to aws-proxy/aws_proxy/client/service_states.py index 148339b..5bce81d 100644 --- a/aws-replicator/aws_replicator/client/service_states.py +++ b/aws-proxy/aws_proxy/client/service_states.py @@ -1,3 +1,5 @@ +# TODO: remove this module - no longer used or required in this project! + import logging from typing import Dict, Optional, Type @@ -9,9 +11,9 @@ from localstack.utils.objects import get_all_subclasses from localstack.utils.threads import parallelize -from aws_replicator.client.utils import post_request_to_instance -from aws_replicator.shared.models import ReplicateStateRequest -from aws_replicator.shared.utils import get_resource_type +from aws_proxy.client.utils import post_request_to_instance +from aws_proxy.shared.models import ReplicateStateRequest +from aws_proxy.shared.utils import get_resource_type LOG = logging.getLogger(__name__) diff --git a/aws-replicator/aws_replicator/client/utils.py b/aws-proxy/aws_proxy/client/utils.py similarity index 86% rename from aws-replicator/aws_replicator/client/utils.py rename to aws-proxy/aws_proxy/client/utils.py index d5d7a76..af2c43c 100644 --- a/aws-replicator/aws_replicator/client/utils.py +++ b/aws-proxy/aws_proxy/client/utils.py @@ -5,8 +5,8 @@ from localstack.utils.functions import run_safe from localstack.utils.strings import to_str, truncate -from aws_replicator.config import HANDLER_PATH_REPLICATE -from aws_replicator.shared.models import ReplicateStateRequest +from aws_proxy.config import HANDLER_PATH_REPLICATE +from aws_proxy.shared.models import ReplicateStateRequest def post_request_to_instance(request: ReplicateStateRequest = None): diff --git a/aws-replicator/aws_replicator/config.py b/aws-proxy/aws_proxy/config.py similarity index 60% rename from aws-replicator/aws_replicator/config.py rename to aws-proxy/aws_proxy/config.py index 5fbfc8c..454156d 100644 --- a/aws-replicator/aws_replicator/config.py +++ b/aws-proxy/aws_proxy/config.py @@ -4,14 +4,14 @@ from localstack.constants import INTERNAL_RESOURCE_PATH # handler path within the internal /_localstack endpoint -HANDLER_PATH_REPLICATE = f"{INTERNAL_RESOURCE_PATH}/aws/replicate" +HANDLER_PATH_PROXY = f"{INTERNAL_RESOURCE_PATH}/aws/proxy" HANDLER_PATH_PROXIES = f"{INTERNAL_RESOURCE_PATH}/aws/proxies" # whether to clean up proxy containers (set to "0" to investigate startup issues) -CLEANUP_PROXY_CONTAINERS = is_env_not_false("REPLICATOR_CLEANUP_PROXY_CONTAINERS") +CLEANUP_PROXY_CONTAINERS = is_env_not_false("PROXY_CLEANUP_CONTAINERS") # additional Docker flags to pass to the proxy containers -PROXY_DOCKER_FLAGS = (os.getenv("REPLICATOR_PROXY_DOCKER_FLAGS") or "").strip() +PROXY_DOCKER_FLAGS = (os.getenv("PROXY_DOCKER_FLAGS") or "").strip() # LS hostname to use for proxy Docker container to register itself at the main container -PROXY_LOCALSTACK_HOST = (os.getenv("REPLICATOR_LOCALSTACK_HOST") or "").strip() +PROXY_LOCALSTACK_HOST = (os.getenv("PROXY_LOCALSTACK_HOST") or "").strip() diff --git a/aws-replicator/aws_replicator/server/__init__.py b/aws-proxy/aws_proxy/server/__init__.py similarity index 100% rename from aws-replicator/aws_replicator/server/__init__.py rename to aws-proxy/aws_proxy/server/__init__.py diff --git a/aws-replicator/aws_replicator/server/aws_request_forwarder.py b/aws-proxy/aws_proxy/server/aws_request_forwarder.py similarity index 98% rename from aws-replicator/aws_replicator/server/aws_request_forwarder.py rename to aws-proxy/aws_proxy/server/aws_request_forwarder.py index a24ba04..f2cce22 100644 --- a/aws-replicator/aws_replicator/server/aws_request_forwarder.py +++ b/aws-proxy/aws_proxy/server/aws_request_forwarder.py @@ -22,8 +22,8 @@ except ImportError: from localstack.constants import TEST_AWS_ACCESS_KEY_ID -from aws_replicator.shared.constants import HEADER_HOST_ORIGINAL -from aws_replicator.shared.models import ProxyInstance, ProxyServiceConfig +from aws_proxy.shared.constants import HEADER_HOST_ORIGINAL +from aws_proxy.shared.models import ProxyInstance, ProxyServiceConfig LOG = logging.getLogger(__name__) diff --git a/aws-replicator/aws_replicator/server/extension.py b/aws-proxy/aws_proxy/server/extension.py similarity index 100% rename from aws-replicator/aws_replicator/server/extension.py rename to aws-proxy/aws_proxy/server/extension.py diff --git a/aws-replicator/aws_replicator/server/request_handler.py b/aws-proxy/aws_proxy/server/request_handler.py similarity index 93% rename from aws-replicator/aws_replicator/server/request_handler.py rename to aws-proxy/aws_proxy/server/request_handler.py index 7e90eeb..7252171 100644 --- a/aws-replicator/aws_replicator/server/request_handler.py +++ b/aws-proxy/aws_proxy/server/request_handler.py @@ -19,16 +19,16 @@ from localstack.utils.strings import to_str from localstack.utils.threads import start_worker_thread -from aws_replicator import config as repl_config -from aws_replicator.client.auth_proxy import ( +from aws_proxy import config as repl_config +from aws_proxy.client.auth_proxy import ( CONTAINER_CONFIG_FILE, CONTAINER_NAME_PREFIX, start_aws_auth_proxy_in_container, ) -from aws_replicator.config import HANDLER_PATH_PROXIES, HANDLER_PATH_REPLICATE -from aws_replicator.server import ui as web_ui -from aws_replicator.server.aws_request_forwarder import AwsProxyHandler -from aws_replicator.shared.models import AddProxyRequest, ReplicateStateRequest, ResourceReplicator +from aws_proxy.config import HANDLER_PATH_PROXIES, HANDLER_PATH_REPLICATE +from aws_proxy.server import ui as web_ui +from aws_proxy.server.aws_request_forwarder import AwsProxyHandler +from aws_proxy.shared.models import AddProxyRequest, ReplicateStateRequest, ResourceReplicator LOG = logging.getLogger(__name__) diff --git a/aws-replicator/aws_replicator/server/resource_replicator.py b/aws-proxy/aws_proxy/server/resource_replicator.py similarity index 100% rename from aws-replicator/aws_replicator/server/resource_replicator.py rename to aws-proxy/aws_proxy/server/resource_replicator.py diff --git a/aws-replicator/aws_replicator/server/ui/__init__.py b/aws-proxy/aws_proxy/server/ui/__init__.py similarity index 100% rename from aws-replicator/aws_replicator/server/ui/__init__.py rename to aws-proxy/aws_proxy/server/ui/__init__.py diff --git a/aws-replicator/aws_replicator/server/ui/app.js b/aws-proxy/aws_proxy/server/ui/app.js similarity index 100% rename from aws-replicator/aws_replicator/server/ui/app.js rename to aws-proxy/aws_proxy/server/ui/app.js diff --git a/aws-replicator/aws_replicator/server/ui/favicon.png b/aws-proxy/aws_proxy/server/ui/favicon.png similarity index 100% rename from aws-replicator/aws_replicator/server/ui/favicon.png rename to aws-proxy/aws_proxy/server/ui/favicon.png diff --git a/aws-replicator/aws_replicator/server/ui/index.html b/aws-proxy/aws_proxy/server/ui/index.html similarity index 100% rename from aws-replicator/aws_replicator/server/ui/index.html rename to aws-proxy/aws_proxy/server/ui/index.html diff --git a/aws-replicator/aws_replicator/shared/__init__.py b/aws-proxy/aws_proxy/shared/__init__.py similarity index 100% rename from aws-replicator/aws_replicator/shared/__init__.py rename to aws-proxy/aws_proxy/shared/__init__.py diff --git a/aws-replicator/aws_replicator/shared/constants.py b/aws-proxy/aws_proxy/shared/constants.py similarity index 100% rename from aws-replicator/aws_replicator/shared/constants.py rename to aws-proxy/aws_proxy/shared/constants.py diff --git a/aws-replicator/aws_replicator/shared/models.py b/aws-proxy/aws_proxy/shared/models.py similarity index 100% rename from aws-replicator/aws_replicator/shared/models.py rename to aws-proxy/aws_proxy/shared/models.py diff --git a/aws-replicator/aws_replicator/shared/utils.py b/aws-proxy/aws_proxy/shared/utils.py similarity index 100% rename from aws-replicator/aws_replicator/shared/utils.py rename to aws-proxy/aws_proxy/shared/utils.py diff --git a/aws-replicator/etc/aws-replicate-overview.png b/aws-proxy/etc/aws-replicate-overview.png similarity index 100% rename from aws-replicator/etc/aws-replicate-overview.png rename to aws-proxy/etc/aws-replicate-overview.png diff --git a/aws-replicator/etc/proxy-settings.png b/aws-proxy/etc/proxy-settings.png similarity index 100% rename from aws-replicator/etc/proxy-settings.png rename to aws-proxy/etc/proxy-settings.png diff --git a/aws-replicator/example/Makefile b/aws-proxy/example/Makefile similarity index 100% rename from aws-replicator/example/Makefile rename to aws-proxy/example/Makefile diff --git a/aws-replicator/example/README.md b/aws-proxy/example/README.md similarity index 100% rename from aws-replicator/example/README.md rename to aws-proxy/example/README.md diff --git a/aws-replicator/example/lambda.py b/aws-proxy/example/lambda.py similarity index 100% rename from aws-replicator/example/lambda.py rename to aws-proxy/example/lambda.py diff --git a/aws-replicator/example/main.tf b/aws-proxy/example/main.tf similarity index 100% rename from aws-replicator/example/main.tf rename to aws-proxy/example/main.tf diff --git a/aws-replicator/example/proxy_config.yml b/aws-proxy/example/proxy_config.yml similarity index 100% rename from aws-replicator/example/proxy_config.yml rename to aws-proxy/example/proxy_config.yml diff --git a/aws-replicator/logo.png b/aws-proxy/logo.png similarity index 100% rename from aws-replicator/logo.png rename to aws-proxy/logo.png diff --git a/aws-replicator/pyproject.toml b/aws-proxy/pyproject.toml similarity index 77% rename from aws-replicator/pyproject.toml rename to aws-proxy/pyproject.toml index 5af0877..71ede6f 100644 --- a/aws-replicator/pyproject.toml +++ b/aws-proxy/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line_length = 100 -include = '(aws_replicator|example|tests)/.*\.py$' +include = '(aws_proxy|example|tests)/.*\.py$' [tool.isort] profile = 'black' diff --git a/aws-replicator/setup.cfg b/aws-proxy/setup.cfg similarity index 84% rename from aws-replicator/setup.cfg rename to aws-proxy/setup.cfg index bcb0d18..5f285f7 100644 --- a/aws-replicator/setup.cfg +++ b/aws-proxy/setup.cfg @@ -1,11 +1,11 @@ [metadata] -name = localstack-extension-aws-replicator +name = localstack-extension-aws-proxy version = 0.1.25 summary = LocalStack AWS Proxy Extension description = Proxy AWS resources into your LocalStack instance long_description = file: README.md long_description_content_type = text/markdown; charset=UTF-8 -url = https://github.com/localstack/localstack-extensions/tree/main/aws-replicator +url = https://github.com/localstack/localstack-extensions/tree/main/aws-proxy author = LocalStack Team author_email = info@localstack.cloud @@ -44,13 +44,13 @@ test = rolo [options.package_data] -aws_replicator = +aws_proxy = **/*.html **/*.js **/*.png [options.entry_points] localstack.extensions = - aws-replicator = aws_replicator.server.extension:AwsReplicatorExtension + aws-proxy = aws_proxy.server.extension:AwsReplicatorExtension localstack.plugins.cli = - aws-replicator = aws_replicator.client.cli:AwsReplicatorPlugin + aws-proxy = aws_proxy.client.cli:AwsReplicatorPlugin diff --git a/aws-replicator/setup.py b/aws-proxy/setup.py similarity index 100% rename from aws-replicator/setup.py rename to aws-proxy/setup.py diff --git a/aws-replicator/tests/__init__.py b/aws-proxy/tests/__init__.py similarity index 100% rename from aws-replicator/tests/__init__.py rename to aws-proxy/tests/__init__.py diff --git a/aws-replicator/tests/conftest.py b/aws-proxy/tests/conftest.py similarity index 100% rename from aws-replicator/tests/conftest.py rename to aws-proxy/tests/conftest.py diff --git a/aws-replicator/tests/test_config.py b/aws-proxy/tests/test_config.py similarity index 77% rename from aws-replicator/tests/test_config.py rename to aws-proxy/tests/test_config.py index affea20..cf68209 100644 --- a/aws-replicator/tests/test_config.py +++ b/aws-proxy/tests/test_config.py @@ -1,5 +1,5 @@ -from aws_replicator.server.aws_request_forwarder import AwsProxyHandler -from aws_replicator.shared.models import ProxyServiceConfig +from aws_proxy.server.aws_request_forwarder import AwsProxyHandler +from aws_proxy.shared.models import ProxyServiceConfig def test_get_resource_names(): diff --git a/aws-replicator/tests/test_proxy_requests.py b/aws-proxy/tests/test_proxy_requests.py similarity index 99% rename from aws-replicator/tests/test_proxy_requests.py rename to aws-proxy/tests/test_proxy_requests.py index 0ce520c..03f2b49 100644 --- a/aws-replicator/tests/test_proxy_requests.py +++ b/aws-proxy/tests/test_proxy_requests.py @@ -15,8 +15,8 @@ from localstack.utils.strings import short_uid from localstack.utils.sync import retry -from aws_replicator.client.auth_proxy import start_aws_auth_proxy -from aws_replicator.shared.models import ProxyConfig +from aws_proxy.client.auth_proxy import start_aws_auth_proxy +from aws_proxy.shared.models import ProxyConfig try: from localstack.testing.config import TEST_AWS_ACCOUNT_ID diff --git a/aws-replicator/MANIFEST.in b/aws-replicator/MANIFEST.in deleted file mode 100644 index 45b1f8d..0000000 --- a/aws-replicator/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -recursive-include aws_replicator *.html -recursive-include aws_replicator *.js -recursive-include aws_replicator *.png diff --git a/aws-replicator/aws_replicator/__init__.py b/aws-replicator/aws_replicator/__init__.py deleted file mode 100644 index 71dffa0..0000000 --- a/aws-replicator/aws_replicator/__init__.py +++ /dev/null @@ -1 +0,0 @@ -name = "aws-replicator" From 8b49a05e0cce13a38df4f30f003d5c3988003c79 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Wed, 17 Sep 2025 00:29:19 +0200 Subject: [PATCH 2/7] minor refactoring --- README.md | 2 +- aws-proxy/README.md | 14 +++++++------- aws-proxy/aws_proxy/client/cli.py | 1 - aws-proxy/aws_proxy/client/utils.py | 4 ++-- aws-proxy/aws_proxy/server/extension.py | 14 +++++++------- aws-proxy/aws_proxy/server/request_handler.py | 6 +++--- aws-proxy/aws_proxy/server/resource_replicator.py | 6 +++--- aws-proxy/setup.cfg | 2 +- 8 files changed, 24 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 15a71f8..60dc6dc 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ You can install the respective extension by calling `localstack install ResourceReplicator: - from aws_replicator.server.resource_replicator import ResourceReplicatorFormer2 + from aws_proxy.server.resource_replicator import ResourceReplicatorFormer2 # TODO deprecated - fix the implementation of the replicator/copy logic! # return ResourceReplicatorInternal() diff --git a/aws-proxy/aws_proxy/server/resource_replicator.py b/aws-proxy/aws_proxy/server/resource_replicator.py index 63687cb..0d154d8 100644 --- a/aws-proxy/aws_proxy/server/resource_replicator.py +++ b/aws-proxy/aws_proxy/server/resource_replicator.py @@ -9,9 +9,9 @@ from localstack.utils.files import mkdir from localstack.utils.run import run -from aws_replicator.client.service_states import ExtendedResourceStateReplicator -from aws_replicator.shared.models import ResourceReplicator -from aws_replicator.shared.utils import get_resource_type +from aws_proxy.client.service_states import ExtendedResourceStateReplicator +from aws_proxy.shared.models import ResourceReplicator +from aws_proxy.shared.utils import get_resource_type LOG = logging.getLogger(__name__) diff --git a/aws-proxy/setup.cfg b/aws-proxy/setup.cfg index 5f285f7..e24ec36 100644 --- a/aws-proxy/setup.cfg +++ b/aws-proxy/setup.cfg @@ -51,6 +51,6 @@ aws_proxy = [options.entry_points] localstack.extensions = - aws-proxy = aws_proxy.server.extension:AwsReplicatorExtension + aws-proxy = aws_proxy.server.extension:AwsProxyExtension localstack.plugins.cli = aws-proxy = aws_proxy.client.cli:AwsReplicatorPlugin From 47c549fe1d949e2f71fa560e7b460a7208a422c2 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Wed, 17 Sep 2025 11:03:33 +0200 Subject: [PATCH 3/7] clean up old replicator code --- aws-proxy/README.md | 6 +- aws-proxy/aws_proxy/client/auth_proxy.py | 16 +- aws-proxy/aws_proxy/client/cli.py | 23 +- aws-proxy/aws_proxy/client/replicate.py | 265 ------------------ aws-proxy/aws_proxy/client/service_states.py | 161 ----------- aws-proxy/aws_proxy/server/request_handler.py | 27 +- .../aws_proxy/server/resource_replicator.py | 106 ------- 7 files changed, 12 insertions(+), 592 deletions(-) delete mode 100644 aws-proxy/aws_proxy/client/replicate.py delete mode 100644 aws-proxy/aws_proxy/client/service_states.py delete mode 100644 aws-proxy/aws_proxy/server/resource_replicator.py diff --git a/aws-proxy/README.md b/aws-proxy/README.md index 6a217dc..f93c397 100644 --- a/aws-proxy/README.md +++ b/aws-proxy/README.md @@ -7,7 +7,7 @@ This enables one flavor of "hybrid" or "remocal" setups where you can easily bri ⚠️ Please note that this extension is experimental and still under active development. -⚠️ Note: Given that the scope of this extension has recently changed (see [below](#resource-replicator-cli-deprecated)) - it has been recently renamed from `aws-replicator` to `aws-proxy`. +⚠️ Note: Given that the scope of this extension has recently changed (see [below](#resource-replicator-cli-deprecated)) - it has been renamed from `aws-replicator` to `aws-proxy`. ## Prerequisites @@ -120,13 +120,13 @@ In addition to the proxy services configuration shown above, the following confi ## Resource Replicator CLI (deprecated) Note: Previous versions of this extension also offered a "replicate" mode to copy/clone (rather than proxy) resources from an AWS account into the local instance. -This functionality has been removed from this extension, and is now being migrated to a new extension (more details following soon). +This functionality has been removed from this extension, and is now available directly in the LocalStack Pro image (see [here](https://docs.localstack.cloud/aws/tooling/aws-replicator)). If you wish to access the deprecated instructions, they can be found [here](https://github.com/localstack/localstack-extensions/blob/fe0c97e8a9d94f72c80358493e51ce6c1da535dc/aws-replicator/README.md#resource-replicator-cli). ## Change Log -* `0.1.25`: Fix dynamodb proxying for read-only mode. +* `0.1.25`: Fix dynamodb proxying for read-only mode * `0.1.24`: Fix healthcheck probe for proxy container * `0.1.23`: Fix unpinned React.js dependencies preventing webui from loading * `0.1.22`: Fix auth-related imports that prevent the AWS proxy from starting diff --git a/aws-proxy/aws_proxy/client/auth_proxy.py b/aws-proxy/aws_proxy/client/auth_proxy.py index fd17b2b..82f11fd 100644 --- a/aws-proxy/aws_proxy/client/auth_proxy.py +++ b/aws-proxy/aws_proxy/client/auth_proxy.py @@ -17,6 +17,10 @@ from localstack.config import external_service_url from localstack.constants import AWS_REGION_US_EAST_1, DOCKER_IMAGE_NAME_PRO, LOCALHOST_HOSTNAME from localstack.http import Request +from localstack.pro.core.bootstrap.licensingv2 import ( + ENV_LOCALSTACK_API_KEY, + ENV_LOCALSTACK_AUTH_TOKEN, +) from localstack.utils.aws.aws_responses import requests_response from localstack.utils.bootstrap import setup_logging from localstack.utils.collections import select_attributes @@ -37,18 +41,6 @@ from .http2_server import run_server -try: - from localstack.pro.core.bootstrap.licensingv2 import ( - ENV_LOCALSTACK_API_KEY, - ENV_LOCALSTACK_AUTH_TOKEN, - ) -except ImportError: - # TODO remove once we don't need compatibility with <3.6 anymore - from localstack_ext.bootstrap.licensingv2 import ( - ENV_LOCALSTACK_API_KEY, - ENV_LOCALSTACK_AUTH_TOKEN, - ) - LOG = logging.getLogger(__name__) LOG.setLevel(logging.INFO) if localstack_config.DEBUG: diff --git a/aws-proxy/aws_proxy/client/cli.py b/aws-proxy/aws_proxy/client/cli.py index 52f7454..94e5da8 100644 --- a/aws-proxy/aws_proxy/client/cli.py +++ b/aws-proxy/aws_proxy/client/cli.py @@ -4,26 +4,18 @@ import click import yaml from localstack.cli import LocalstackCli, LocalstackCliPlugin, console +from localstack.pro.core.cli.aws import aws +from localstack.pro.core.config import is_auth_token_configured from localstack.utils.files import load_file from aws_proxy.shared.models import ProxyConfig, ProxyServiceConfig -try: - from localstack.pro.core.bootstrap.auth import get_platform_auth_headers - from localstack.pro.core.cli.aws import aws - from localstack.pro.core.config import is_auth_token_configured -except ImportError: - # Only support anything over version 3.6 - from localstack.pro.core.bootstrap.auth import get_auth_headers as get_platform_auth_headers - from localstack.pro.core.cli.aws import aws - from localstack.pro.core.config import is_api_key_configured as is_auth_token_configured - class AwsProxyPlugin(LocalstackCliPlugin): name = "aws-proxy" def should_load(self) -> bool: - return _is_logged_in() or is_auth_token_configured() + return is_auth_token_configured() def attach(self, cli: LocalstackCli) -> None: group: click.Group = cli.group @@ -32,15 +24,6 @@ def attach(self, cli: LocalstackCli) -> None: aws.add_command(cmd_aws_proxy) -# TODO: remove over time as we're phasing out the `login` command -def _is_logged_in() -> bool: - try: - get_platform_auth_headers() - return True - except Exception: - return False - - @click.command(name="proxy", help="Start up an authentication proxy against real AWS") @click.option( "-s", diff --git a/aws-proxy/aws_proxy/client/replicate.py b/aws-proxy/aws_proxy/client/replicate.py deleted file mode 100644 index 61bb4fd..0000000 --- a/aws-proxy/aws_proxy/client/replicate.py +++ /dev/null @@ -1,265 +0,0 @@ -import json -import logging -import os -import threading -from copy import deepcopy -from typing import Dict, List - -import boto3 -from localstack.utils.collections import select_attributes -from localstack.utils.files import load_file, save_file -from localstack.utils.json import extract_jsonpath -from localstack.utils.threads import parallelize - -from aws_proxy.client.utils import post_request_to_instance -from aws_proxy.shared.models import ( - ExtendedResourceStateReplicator, - ReplicateStateRequest, - ResourceReplicator, -) -from aws_proxy.shared.utils import list_all_resources - -LOG = logging.getLogger(__name__) - -# maximum number of pages to fetch for paginated APIs -MAX_PAGES = 3 - -# additional service resources that are currently not yet supported by Cloud Control -SERVICE_RESOURCES = { - "AWS::DynamoDB::Table": { - "list_operation": "list_tables", - "results": "$.TableNames", - "fetch_details": { - "operation": "describe_table", - "parameters": {"TableName": "$"}, - "results": [ - "$.Table.AttributeDefinitions", - "$.Table.TableName", - "$.Table.KeySchema", - "$.Table.LocalSecondaryIndexes", - "$.Table.GlobalSecondaryIndexes", - { - "GlobalSecondaryIndexes": lambda params: [ - select_attributes(p, ["IndexName", "KeySchema", "Projection"]) - for p in params["Table"].get("GlobalSecondaryIndexes", []) - ] - }, - {"BillingMode": "PAY_PER_REQUEST"}, - "$.Table.StreamSpecification", - "$.Table.Tags", - "$.Table.TableClass", - ], - }, - }, - "AWS::SSM::Parameter": { - "list_operation": "describe_parameters", - "results": "$.Parameters", - "fetch_details": { - "operation": "get_parameter", - "parameters": {"Name": "$.Name"}, - "results": [ - "$.Parameter.Name", - "$.Parameter.Type", - "$.Parameter.Value", - ], - }, - }, -} - - -class AwsAccountScraper: - """Scrapes and returns the resources in the AWS account targeted by a given boto3 session""" - - def __init__(self, session: boto3.Session): - self.session = session - - def get_resource_types(self) -> List[Dict]: - """Return a list of supported resource types for scraping an AWS account""" - - res_types_file = os.path.join(os.path.dirname(__file__), "resource_types.json") - if os.path.exists(res_types_file): - # load cached resources file - all_types = json.loads(load_file(res_types_file)) - else: - cloudformation = self.session.client("cloudformation") - all_types = list_all_resources( - lambda kwargs: cloudformation.list_types( - Type="RESOURCE", - Visibility="PUBLIC", - ProvisioningType="FULLY_MUTABLE", - MaxResults=100, - **kwargs, - ), - last_token_attr_name="NextToken", - list_attr_name="TypeSummaries", - ) - all_types = [select_attributes(ts, ["TypeName"]) for ts in all_types] - # update cached resources file - save_file(res_types_file, json.dumps(all_types)) - - # add custom resource types - for res_type, details in SERVICE_RESOURCES.items(): - existing = [ts for ts in all_types if ts["TypeName"] == res_type] - if not existing: - all_types.append({"TypeName": res_type}) - - return all_types - - def get_resources(self, resource_type: str) -> List[Dict]: - result = [] - try: - result += self.get_resources_cloudcontrol(resource_type) - result += self.get_resources_custom(resource_type) - except Exception as e: - LOG.info("Unable to fetch resources of type %s: %s", resource_type, e) - return result - - def get_resources_cloudcontrol(self, resource_type: str) -> List[Dict]: - cloudcontrol = self.session.client("cloudcontrol") - try: - # fetch the list of resource identifiers - res_list = list_all_resources( - lambda kwargs: cloudcontrol.list_resources(TypeName=resource_type), - last_token_attr_name="NextToken", - list_attr_name="ResourceDescriptions", - max_pages=MAX_PAGES, - ) - - # fetch the detailed resource descriptions - - def handle(resource): - cloudcontrol = self.session.client("cloudcontrol") - res_details = cloudcontrol.get_resource( - TypeName=resource_type, Identifier=resource["Identifier"] - ) - with lock: - resources.append(res_details) - - # parallelizing the execution, as CloudControl can be very slow to respond - lock = threading.RLock() - resources = [] - parallelize(handle, res_list) - - result = [ - { - "TypeName": resource_type, - "Identifier": r["ResourceDescription"]["Identifier"], - "Properties": json.loads(r["ResourceDescription"]["Properties"]), - } - for r in resources - ] - return result - except Exception as e: - exs = str(e) - if "UnsupportedActionException" in exs: - LOG.info("Unsupported operation: %s", e) - return [] - if "must not be null" in exs or "cannot be empty" in exs: - LOG.info("Unable to list resources: %s", e) - return [] - LOG.warning("Unknown error occurred: %s", e) - return [] - - def get_resources_custom(self, resource_type: str) -> List[Dict]: - resource_type_short = resource_type.removeprefix("AWS::") - details = SERVICE_RESOURCES.get(resource_type_short) - if not details: - return [] - - from localstack.services.cloudformation.engine import template_deployer - - service_name = template_deployer.get_service_name({"Type": resource_type}) - from_client = boto3.client(service_name) - - res_list = getattr(from_client, details["list_operation"])() - - res_selector = details.get("results") - if res_selector: - res_list = extract_jsonpath(res_list, res_selector) - - res_list = res_list or [] - result = [] - for _resource in res_list: - props = {} - props_mapping = details.get("props") or {} - for prop_key, prop_val in props_mapping.items(): - if "$" in prop_val: - props[prop_key] = extract_jsonpath(_resource, prop_val) - - fetch_details = details.get("fetch_details") - if fetch_details: - params = deepcopy(fetch_details.get("parameters") or {}) - for param, param_value in params.items(): - if "$" in param_value: - params[param] = extract_jsonpath(_resource, param_value) - res_details = getattr(from_client, fetch_details["operation"])(**params) - res_fields = fetch_details.get("results") - for res_field in res_fields: - if isinstance(res_field, dict): - field_name = list(res_field.keys())[0] - field_value = list(res_field.values())[0] - if callable(field_value): - field_value = field_value(res_details) - else: - field_name = res_field.split(".")[-1] - field_value = extract_jsonpath(res_details, res_field) - if field_value != []: - props[field_name] = field_value - - res_json = {"Type": resource_type, "Properties": props} - result.append(res_json) - - return result - - -class ResourceReplicatorClient(ResourceReplicator): - def create(self, resource: Dict): - # request creation via server - request = ReplicateStateRequest(**resource) - post_request_to_instance(request) - - # add extended state attributes - model_instance = ExtendedResourceStateReplicator.get_resource_instance(resource) - if model_instance: - model_instance.add_extended_state_external() - - def create_all(self): - # request creation - post_request_to_instance() - - -def replicate_state_with_scraper_on_host( - scraper: AwsAccountScraper, creator: ResourceReplicator, services: List[str] = None -): - """Replicate the state from a source AWS account into a target account (or LocalStack)""" - - res_types = scraper.get_resource_types() - LOG.info("Found %s Cloud Control resources types", len(res_types)) - - for res_type in res_types: - type_name = res_type["TypeName"] - if services: - service_name = type_name.removeprefix("AWS::").lower().split("::")[0] - if service_name not in services: - continue - resources = scraper.get_resources(type_name) - LOG.info("Found %s resources of type %s", len(resources), type_name) - for resource in resources: - creator.create(resource) - - -def replicate_state_with_scraper_in_container( - creator: ResourceReplicator, services: List[str] = None -): - """Replicate the state from a source AWS account into a target account (or LocalStack)""" - creator.create_all() - - -def replicate_state_into_local(services: List[str]): - creator = ResourceReplicatorClient() - - # deprecated - # scraper = AwsAccountScraper(boto3.Session()) - # return replicate_state_with_scraper_on_host(scraper, creator, services=services) - - return replicate_state_with_scraper_in_container(creator, services=services) diff --git a/aws-proxy/aws_proxy/client/service_states.py b/aws-proxy/aws_proxy/client/service_states.py deleted file mode 100644 index 5bce81d..0000000 --- a/aws-proxy/aws_proxy/client/service_states.py +++ /dev/null @@ -1,161 +0,0 @@ -# TODO: remove this module - no longer used or required in this project! - -import logging -from typing import Dict, Optional, Type - -import boto3 -from botocore.client import BaseClient -from localstack.services.cloudformation.models.s3 import S3Bucket -from localstack.services.cloudformation.service_models import GenericBaseModel -from localstack.utils.aws import aws_stack -from localstack.utils.objects import get_all_subclasses -from localstack.utils.threads import parallelize - -from aws_proxy.client.utils import post_request_to_instance -from aws_proxy.shared.models import ReplicateStateRequest -from aws_proxy.shared.utils import get_resource_type - -LOG = logging.getLogger(__name__) - - -# # TODO: move to patch utils -def mixin_for(wrapped_clazz: Type): - """Decorator that adds the decorated class as a mixin to the base classes of the given class""" - - def wrapper(wrapping_clazz): - wrapped_clazz.__bases__ = (wrapping_clazz,) + wrapped_clazz.__bases__ - - return wrapper - - -# TODO: remove / adjust to use latest upstream CFn models! -class ExtendedResourceStateReplicator(GenericBaseModel): - """Extended resource models, used to replicate (inject) additional state into a resource instance""" - - def add_extended_state_external(self, remote_client: BaseClient = None): - """Called in the context of external CLI execution to fetch/replicate resource details from a remote account""" - - def add_extended_state_internal(self, state: Dict): - """Called in the context of the internal LocalStack instance to inject the state into a resource""" - - @classmethod - def get_resource_instance(cls, resource: Dict) -> Optional["ExtendedResourceStateReplicator"]: - resource_type = get_resource_type(resource) - resource_class = cls.find_resource_classes().get(resource_type) - if resource_class: - return resource_class(resource) - - @classmethod - def get_resource_class( - cls, resource_type: str - ) -> Optional[Type["ExtendedResourceStateReplicator"]]: - return cls.find_resource_classes().get(resource_type) - - @classmethod - def find_resource_classes(cls) -> Dict[str, "ExtendedResourceStateReplicator"]: - return { - inst.cloudformation_type(): inst - for inst in get_all_subclasses(ExtendedResourceStateReplicator) - } - - -# resource-specific replications - - -# @mixin_for(SQSQueue) -class StateReplicatorSQSQueue(ExtendedResourceStateReplicator): - # @classmethod - # def cloudformation_type(cls): - # return "AWS::SQS::Queue" - - def add_extended_state_external(self, state: Dict = None, remote_client: BaseClient = None): - # executing in the context of the CLI - - remote = remote_client or boto3.client("sqs") - queue_name = self.props["QueueName"] - queue_url = remote.get_queue_url(QueueName=queue_name)["QueueUrl"] - - messages = [] - while True: - response = remote.receive_message(QueueUrl=queue_url, WaitTimeSeconds=1) - msgs = response.get("Messages") - if not msgs: - break - messages.extend(msgs) - - state = {**self.props, "Messages": messages} - request = ReplicateStateRequest( - Type=self.cloudformation_type(), - Properties=state, - PhysicalResourceId=queue_url, - ) - post_request_to_instance(request) - - def add_extended_state_internal(self, state: Dict = None): - # executing in the context of the server - from localstack.aws.api.sqs import Message - from localstack.services.sqs.provider import sqs_stores - - queue_name = self.props["QueueName"] - messages = state.get("Messages") or [] - LOG.info("Inserting %s messages into queue", len(messages), queue_name) - for region, details in sqs_stores.regions().items(): - queue = details.queues.get(queue_name) - if not queue: - continue - for message in messages: - message.setdefault("MD5OfMessageAttributes", None) - queue.put(Message(**message)) - break - - -# @mixin_for(DynamoDBTable) -class StateReplicatorDynamoDBTable(ExtendedResourceStateReplicator): - # @classmethod - # def cloudformation_type(cls): - # return "AWS::DynamoDB::Table" - - def add_extended_state_external(self, remote_client: BaseClient = None): - table_name = self.props["TableName"] - LOG.debug("Copying items from source table '%s' into target", table_name) - - remote = remote_client or boto3.resource("dynamodb") - local = aws_stack.connect_to_resource("dynamodb") - remote_table = remote.Table(table_name) - local_table = local.Table(table_name) - - first_request = True - response = {} - while first_request or "LastEvaluatedKey" in response: - kwargs = {} if first_request else {"ExclusiveStartKey": response["LastEvaluatedKey"]} - first_request = False - response = remote_table.scan(**kwargs) - with local_table.batch_writer() as batch: - for item in response["Items"]: - batch.put_item(Item=item) - - -@mixin_for(S3Bucket) -class StateReplicatorS3Bucket(ExtendedResourceStateReplicator): - # @classmethod - # def cloudformation_type(cls): - # return "AWS::S3::Bucket" - - def add_extended_state_external(self, remote_client: BaseClient = None): - bucket_name = self.props["BucketName"] - LOG.debug("Copying items from source S3 bucket '%s' into target", bucket_name) - - remote = boto3.resource("s3") - local = aws_stack.connect_to_resource("s3") - remote_bucket = remote.Bucket(bucket_name) - local_bucket = local.Bucket(bucket_name) - # TODO: make configurable - max_object_size = 1000 * 1000 - - def copy_object(obj): - if obj.size > max_object_size: - LOG.debug("Skip copying large S3 object %s with %s bytes", obj.key, obj.size) - return - local_bucket.put_object(Key=obj.key, Body=obj.get()["Body"].read()) - - parallelize(copy_object, list(remote_bucket.objects.all()), size=15) diff --git a/aws-proxy/aws_proxy/server/request_handler.py b/aws-proxy/aws_proxy/server/request_handler.py index f05bddc..dd277ba 100644 --- a/aws-proxy/aws_proxy/server/request_handler.py +++ b/aws-proxy/aws_proxy/server/request_handler.py @@ -25,10 +25,10 @@ CONTAINER_NAME_PREFIX, start_aws_auth_proxy_in_container, ) -from aws_proxy.config import HANDLER_PATH_PROXIES, HANDLER_PATH_PROXY +from aws_proxy.config import HANDLER_PATH_PROXIES from aws_proxy.server import ui as web_ui from aws_proxy.server.aws_request_forwarder import AwsProxyHandler -from aws_proxy.shared.models import AddProxyRequest, ReplicateStateRequest, ResourceReplicator +from aws_proxy.shared.models import AddProxyRequest LOG = logging.getLogger(__name__) @@ -37,16 +37,6 @@ class RequestHandler: - @route(HANDLER_PATH_PROXY, methods=["POST"]) - def handle_replicate(self, request: Request, **kwargs): - replicator = _get_replicator() - payload = _get_json(request) - if payload: - req = ReplicateStateRequest(**payload) - result = replicator.create(req) - else: - result = replicator.create_all() - return result or {} @route(HANDLER_PATH_PROXIES, methods=["POST"]) def add_proxy(self, request: Request, **kwargs): @@ -104,19 +94,6 @@ def serve_static_file(self, path: str): return Response(Path(file_path).open(mode="rb"), mimetype=mime_type) -def handle_replicate_request(request: ReplicateStateRequest): - replicator = _get_replicator() - return replicator.create(request) - - -def _get_replicator() -> ResourceReplicator: - from aws_proxy.server.resource_replicator import ResourceReplicatorFormer2 - - # TODO deprecated - fix the implementation of the replicator/copy logic! - # return ResourceReplicatorInternal() - return ResourceReplicatorFormer2() - - def handle_proxies_request(request: AddProxyRequest): port = request.get("port") if not port: diff --git a/aws-proxy/aws_proxy/server/resource_replicator.py b/aws-proxy/aws_proxy/server/resource_replicator.py deleted file mode 100644 index 0d154d8..0000000 --- a/aws-proxy/aws_proxy/server/resource_replicator.py +++ /dev/null @@ -1,106 +0,0 @@ -import logging -import os -from typing import Dict, Optional, Type - -from localstack import config -from localstack.services.cloudformation.engine import template_deployer -from localstack.services.cloudformation.engine.entities import StackMetadata, StackTemplate -from localstack.services.cloudformation.provider import Stack -from localstack.utils.files import mkdir -from localstack.utils.run import run - -from aws_proxy.client.service_states import ExtendedResourceStateReplicator -from aws_proxy.shared.models import ResourceReplicator -from aws_proxy.shared.utils import get_resource_type - -LOG = logging.getLogger(__name__) - -FORMER2_NPM_PACKAGE = "https://github.com/iann0036/former2" - - -# TODO see if we still need this class / unify -class ResourceReplicatorInternal(ResourceReplicator): - """Utility that creates resources from CloudFormation/CloudControl templates.""" - - def create(self, resource: Dict): - cf_model_class = self._get_cf_model_class(resource) - if not cf_model_class: - return - - if resource.get("TypeName") and not resource.get("Type"): - resource["Type"] = resource.pop("TypeName") - - res_type = get_resource_type(resource) - res_json = {"Type": res_type, "Properties": resource["Properties"]} - LOG.debug("Deploying CloudFormation resource: %s", res_json) - - # note: quick hack for now - creating a fake Stack for each individual resource to be deployed - template = StackTemplate(StackName="s1", Resources={"myres": res_json}) - metadata = StackMetadata(StackName="s1") - stack = Stack(metadata, template=template) - resource_status = template_deployer.retrieve_resource_details( - "myres", {}, stack.resources, stack_name=stack.stack_name - ) - - if not resource_status: - # deploy resource, if it doesn't exist yet - deployer = template_deployer.TemplateDeployer(stack) - deployer.deploy_stack() - # TODO: need to ensure that the ID of the created resource also matches! - - # add extended state (e.g., actual S3 objects) - - model_instance = ExtendedResourceStateReplicator.get_resource_instance(resource) - if not model_instance: - res_type = get_resource_type(resource) - LOG.info("Unable to find CloudFormation model class for resource: %s", res_type) - return - return model_instance.add_extended_state_internal(resource["Properties"]) - - def create_all(self): - raise NotImplementedError - - def _get_cf_model_class(self, resource: Dict) -> Optional[Type]: - res_type = get_resource_type(resource) - return self._load_resource_models().get(res_type) - - def _load_resource_models(self): - if not hasattr(template_deployer, "_ls_patch_applied"): - try: - from localstack.pro.core.services.cloudformation.cloudformation_extended import ( - patch_cloudformation, - ) - except ImportError: - # TODO remove once we don't need compatibility with <3.6 anymore - from localstack_ext.services.cloudformation.cloudformation_extended import ( - patch_cloudformation, - ) - - patch_cloudformation() - template_deployer._ls_patch_applied = True - return template_deployer.RESOURCE_MODELS - - -class ResourceReplicatorFormer2(ResourceReplicator): - """Resource replicator implementation based on the former2 project (https://github.com/iann0036/former2)""" - - def create(self, resource: Dict): - raise NotImplementedError - - def create_all(self): - cfn_template = self._run_former2_cli("generate") - LOG.debug("Generated CloudFormation template: %s", cfn_template) - # TODO: deploy template into LocalStack instance! - - def _run_former2_cli(self, *cmd_args) -> str: - script_path = self._install() - return run([script_path, *cmd_args]) - - def _install(self) -> str: - install_dir = os.path.join(config.dirs.var_libs, "former2") - mkdir(install_dir) - script_path = os.path.join(install_dir, "node_modules/.bin/former2") - if not os.path.exists(script_path): - run(["npm", "init", "-y"], cwd=install_dir) - run(["npm", "i", FORMER2_NPM_PACKAGE], cwd=install_dir) - return script_path From 841b713924adddbc575b05e0e80d6450223491c3 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Wed, 17 Sep 2025 11:07:24 +0200 Subject: [PATCH 4/7] bump version --- aws-proxy/README.md | 1 + aws-proxy/setup.cfg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/aws-proxy/README.md b/aws-proxy/README.md index f93c397..d6c4fee 100644 --- a/aws-proxy/README.md +++ b/aws-proxy/README.md @@ -126,6 +126,7 @@ If you wish to access the deprecated instructions, they can be found [here](http ## Change Log +* `0.2.0`: Rename extension from `localstack-extension-aws-replicator` to `localstack-extension-aws-proxy` * `0.1.25`: Fix dynamodb proxying for read-only mode * `0.1.24`: Fix healthcheck probe for proxy container * `0.1.23`: Fix unpinned React.js dependencies preventing webui from loading diff --git a/aws-proxy/setup.cfg b/aws-proxy/setup.cfg index e24ec36..4f984ac 100644 --- a/aws-proxy/setup.cfg +++ b/aws-proxy/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = localstack-extension-aws-proxy -version = 0.1.25 +version = 0.2.0 summary = LocalStack AWS Proxy Extension description = Proxy AWS resources into your LocalStack instance long_description = file: README.md From e23244919e18fa5f7a04766453d1f09164a31a3d Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Wed, 17 Sep 2025 12:12:27 +0200 Subject: [PATCH 5/7] Update aws-proxy/setup.cfg Co-authored-by: Nikos --- aws-proxy/setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-proxy/setup.cfg b/aws-proxy/setup.cfg index 4f984ac..079b8c4 100644 --- a/aws-proxy/setup.cfg +++ b/aws-proxy/setup.cfg @@ -53,4 +53,4 @@ aws_proxy = localstack.extensions = aws-proxy = aws_proxy.server.extension:AwsProxyExtension localstack.plugins.cli = - aws-proxy = aws_proxy.client.cli:AwsReplicatorPlugin + aws-proxy = aws_proxy.client.cli:AwsProxyPlugin From e668d6c097d23696b8ec37d9f0a5ae69b13b5062 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Wed, 17 Sep 2025 12:27:53 +0200 Subject: [PATCH 6/7] more cleanups --- aws-proxy/aws_proxy/client/utils.py | 13 ----------- aws-proxy/aws_proxy/shared/models.py | 33 +--------------------------- 2 files changed, 1 insertion(+), 45 deletions(-) diff --git a/aws-proxy/aws_proxy/client/utils.py b/aws-proxy/aws_proxy/client/utils.py index 1adc6fd..cbaa557 100644 --- a/aws-proxy/aws_proxy/client/utils.py +++ b/aws-proxy/aws_proxy/client/utils.py @@ -1,21 +1,8 @@ from typing import Union -import requests -from localstack.config import get_edge_url from localstack.utils.functions import run_safe from localstack.utils.strings import to_str, truncate -from aws_proxy.config import HANDLER_PATH_PROXY -from aws_proxy.shared.models import ReplicateStateRequest - - -def post_request_to_instance(request: ReplicateStateRequest = None): - url = f"{get_edge_url()}{HANDLER_PATH_PROXY}" - response = requests.post(url, json=request or {}) - if not response.ok: - raise Exception(f"Invocation failed (code {response.status_code}): {response.content}") - return response - # TODO: add to common utils def truncate_content(content: Union[str, bytes], max_length: int = None): diff --git a/aws-proxy/aws_proxy/shared/models.py b/aws-proxy/aws_proxy/shared/models.py index 5644212..3f77627 100644 --- a/aws-proxy/aws_proxy/shared/models.py +++ b/aws-proxy/aws_proxy/shared/models.py @@ -1,40 +1,9 @@ import logging -from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional, TypedDict, Union +from typing import Dict, List, TypedDict, Union LOG = logging.getLogger(__name__) -class ReplicateStateRequest(TypedDict): - """ - Represents a request sent from the CLI to the extension request - handler to inject additional resource state properties. - Using upper-case property names, to stay in line with CloudFormation/CloudControl resource models. - """ - - # resource type name (e.g., "AWS::S3::Bucket") - Type: str - # identifier of the resource - PhysicalResourceId: Optional[str] - # resource properties - Properties: Dict[str, Any] - - -class ResourceReplicator(ABC): - """ - Interface for resource replicator, to effect the creation of a cloned resource inside LocalStack. - This interface has a client-side and a server-side implementation. - """ - - @abstractmethod - def create(self, resource: Dict): - """Create the resource specified via the given resource dict.""" - - @abstractmethod - def create_all(self): - """Scrape and replicate all resources from the source AWS account into LocalStack.""" - - class ProxyServiceConfig(TypedDict, total=False): # list of regexes identifying resources to be proxied requests to resources: Union[str, List[str]] From c76d63d2245135b5ba79c5f94b9a077a84207fb2 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Wed, 17 Sep 2025 12:36:12 +0200 Subject: [PATCH 7/7] update version in main README --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 60dc6dc..f801c6e 100644 --- a/README.md +++ b/README.md @@ -66,15 +66,15 @@ Here is the current list of extensions developed by the LocalStack team and thei You can install the respective extension by calling `localstack install `. | Extension | Install name | Version | Support status | -|----------------------------------------------------------------------------------------------------| ------------ | ------- | -------------- | -| [AWS Proxy](https://github.com/localstack/localstack-extensions/tree/main/aws-proxy) | localstack-extension-aws-proxy | 0.1.25 | Experimental | -| [Diagnosis Viewer](https://github.com/localstack/localstack-extensions/tree/main/diagnosis-viewer) | localstack-extension-diagnosis-viewer | 0.1.0 | Stable | -| [Hello World](https://github.com/localstack/localstack-extensions/tree/main/hello-world) | localstack-extension-hello-world | 0.1.0 | Stable | -| [httpbin](https://github.com/localstack/localstack-extensions/tree/main/httpbin) | localstack-extension-httpbin | 0.1.0 | Stable | -| [MailHog](https://github.com/localstack/localstack-extensions/tree/main/mailhog) | localstack-extension-mailhog | 0.1.0 | Stable | -| [Miniflare](https://github.com/localstack/localstack-extensions/tree/main/miniflare) | localstack-extension-miniflare | 0.1.0 | Experimental | -| [Stripe](https://github.com/localstack/localstack-extensions/tree/main/stripe) | localstack-extension-stripe | 0.2.0 | Stable | -| [Terraform Init](https://github.com/localstack/localstack-extensions/tree/main/terraform-init) | localstack-extension-terraform-init | 0.2.0 | Experimental | +|----------------------------------------------------------------------------------------------------| ------------ |---------| -------------- | +| [AWS Proxy](https://github.com/localstack/localstack-extensions/tree/main/aws-proxy) | localstack-extension-aws-proxy | 0.2.0 | Experimental | +| [Diagnosis Viewer](https://github.com/localstack/localstack-extensions/tree/main/diagnosis-viewer) | localstack-extension-diagnosis-viewer | 0.1.0 | Stable | +| [Hello World](https://github.com/localstack/localstack-extensions/tree/main/hello-world) | localstack-extension-hello-world | 0.1.0 | Stable | +| [httpbin](https://github.com/localstack/localstack-extensions/tree/main/httpbin) | localstack-extension-httpbin | 0.1.0 | Stable | +| [MailHog](https://github.com/localstack/localstack-extensions/tree/main/mailhog) | localstack-extension-mailhog | 0.1.0 | Stable | +| [Miniflare](https://github.com/localstack/localstack-extensions/tree/main/miniflare) | localstack-extension-miniflare | 0.1.0 | Experimental | +| [Stripe](https://github.com/localstack/localstack-extensions/tree/main/stripe) | localstack-extension-stripe | 0.2.0 | Stable | +| [Terraform Init](https://github.com/localstack/localstack-extensions/tree/main/terraform-init) | localstack-extension-terraform-init | 0.2.0 | Experimental | ## Developing Extensions