8000 Unify hostnames in returned URLs by simonrw · Pull Request #7774 · localstack/localstack · GitHub
[go: up one dir, main page]

Skip to content

Unify hostnames in returned URLs #7774

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5637712
Start tests for url returning
simonrw Feb 23, 2023
0325017
Add patch_hostnames fixture
simonrw Feb 27, 2023
480bb2f
Add test for s3 location
simonrw Feb 27, 2023
61c4994
Add helpers for configuring the hostname
simonrw Mar 1, 2023
3ede981
Use host utils in opensearch
simonrw Mar 1, 2023
dd812b5
Update S3 to use new host config
simonrw Mar 2, 2023
fe4d980
Cover more cases with SQS tests
simonrw Mar 3, 2023
74abbfb
Partially update SQS to use the new config
simonrw Mar 3, 2023
1a1df4e
Add `assert_host_customisation` fixture
simonrw Mar 3, 2023
91e483f
Update lambda function urls to use new localstack_host function
simonrw Mar 7, 2023
2b5d625
Correct typo with localstack-hostname value
simonrw Mar 8, 2023
c94c3f1
Set localstack_hostname to the hostname of the host
simonrw Mar 8, 2023
edad51b
Handle S3 201 response hostname returning
simonrw Mar 8, 2023
a912dc9
S3 vpath: handle HOSTNAME_EXTERNAL if supplied
simonrw Mar 9, 2023
bae2c59
Update s3 vhosts to use new localstack_host config
simonrw Mar 9, 2023
888059a
Run ASF tests correctly
simonrw Mar 10, 2023
fef6b5c
Run ASF lambda tests correctly
simonrw Mar 10, 2 8000 023
53dab3a
Add explanation comment for socket.gethostname
simonrw Mar 10, 2023
9845857
Update `s3_listener._update_location`
simonrw Mar 10, 2023
600d22f
Make suggested changes to sqs tests
simonrw Mar 10, 2023
75176fe
Remove outdated comment
simonrw Mar 13, 2023
fa16a0d
Merge branch 'master' into network-unification
simonrw Mar 15, 2023
263bacb
Merge branch 'master' into network-unification
simonrw Mar 16, 2023
a75d3e1
Merge branch 'master' into network-unification
simonrw Mar 17, 2023
2c5899f
Fix pro integration test with s3
simonrw Mar 17, 2023
46ea0e9
Fix issue with legacy s3 provider
simonrw Mar 17, 2023
ba95b63
Increase timeout of tests
simonrw Mar 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ jobs:
name: Test ASF Lambda provider
environment:
PROVIDER_OVERRIDE_LAMBDA: "asf"
TEST_PATH: "tests/integration/awslambda/test_lambda.py tests/integration/awslambda/test_lambda_api.py tests/integration/awslambda/test_lambda_common.py tests/integration/awslambda/test_lambda_integration_sqs.py tests/integration/cloudformation/resources/test_lambda.py tests/integration/awslambda/test_lambda_integration_dynamodbstreams.py tests/integration/awslambda/test_lambda_integration_kinesis.py tests/integration/awslambda/test_lambda_developer_tools.py"
TEST_PATH: "tests/integration/awslambda/test_lambda.py tests/integration/awslambda/test_lambda_api.py tests/integration/awslambda/test_lambda_common.py tests/integration/awslambda/test_lambda_integration_sqs.py tests/integration/cloudformation/resources/test_lambda.py tests/integration/awslambda/test_lambda_integration_dynamodbstreams.py tests/integration/awslambda/test_lambda_integration_kinesis.py tests/integration/awslambda/test_lambda_developer_tools.py tests/integration/test_network_configuration.py::TestLambda"
PYTEST_ARGS: "--reruns 3 --junitxml=target/reports/lambda_asf.xml -o junit_suite_name='lambda_asf'"
COVERAGE_ARGS: "-p"
command: make test-coverage
Expand All @@ -172,7 +172,7 @@ jobs:
name: Test ASF S3 provider
environment:
PROVIDER_OVERRIDE_S3: "asf"
TEST_PATH: "tests/integration/s3/"
TEST_PATH: "tests/integration/s3/ tests/integration/test_network_configuration.py::TestS3"
PYTEST_ARGS: "--reruns 3 --junitxml=target/reports/s3_asf.xml -o junit_suite_name='s3_asf'"
COVERAGE_ARGS: "-p"
command: make test-coverage
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pro-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ concurrency:
jobs:
run-integration-tests:
runs-on: ubuntu-latest
timeout-minutes: 110
timeout-minutes: 120
defaults:
run:
working-directory: localstack-ext
Expand Down
8 changes: 6 additions & 2 deletions localstack/services/awslambda/lambda_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

from localstack import config, constants
from localstack.aws.accounts import get_aws_account_id
from localstack.constants import APPLICATION_JSON, LOCALHOST_HOSTNAME
from localstack.constants import APPLICATION_JSON
from localstack.http import Request
from localstack.http import Response as HttpResponse
from localstack.services.awslambda import lambda_executors
Expand Down Expand Up @@ -81,6 +81,7 @@
now_utc,
timestamp,
)
from localstack.utils.urls import localstack_host

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -1511,7 +1512,10 @@ def create_url_config(function):

custom_id = md5(str(random()))
region_name = aws_stack.get_region()
url = f"http://{custom_id}.lambda-url.{region_name}.{LOCALHOST_HOSTNAME}:{config.EDGE_PORT_HTTP or config.EDGE_PORT}/"
host_definition = localstack_host(
use_localhost_cloud=True, custom_port=config.EDGE_PORT_HTTP or config.EDGE_PORT
)
url = f"http://{custom_id}.lambda-url.{region_name}.{host_definition.host_and_port()}/"
# TODO: HTTPS support

data = json.loads(to_str(request.data))
Expand Down
8 changes: 6 additions & 2 deletions localstack/services/awslambda/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@
UpdateFunctionUrlConfigResponse,
Version,
)
from localstack.constants import LOCALHOST_HOSTNAME
from localstack.services.awslambda import api_utils
from localstack.services.awslambda import hooks as lambda_hooks
from localstack.services.awslambda.api_utils import STATEMENT_ID_REGEX
Expand Down Expand Up @@ -193,6 +192,7 @@
from localstack.utils.files import load_file
from localstack.utils.strings import get_random_hex, long_uid, short_uid, to_bytes, to_str
from localstack.utils.sync import poll_condition
from localstack.utils.urls import localstack_host

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -1632,12 +1632,16 @@ def create_function_url_config(

# create function URL config
url_id = api_utils.generate_random_url_id()

host_definition = localstack_host(
use_localhost_cloud=True, custom_port=config.EDGE_PORT_HTTP or config.EDGE_PORT
)
fn.function_url_configs[normalized_qualifier] = FunctionUrlConfig(
function_arn=function_arn,
function_name=function_name,
cors=cors,
url_id=url_id,
url=f"http://{url_id}.lambda-url.{context.region}.{LOCALHOST_HOSTNAME}:{config.EDGE_PORT_HTTP or config.EDGE_PORT}/", # TODO: https support
url=f"http://{url_id}.lambda-url.{context.region}.{host_definition.host_and_port()}/", # TODO: https support
auth_type=auth_type,
creation_time=api_utils.generate_lambda_date(),
last_modified_time=api_utils.generate_lambda_date(),
Expand Down
14 changes: 10 additions & 4 deletions localstack/services/opensearch/cluster_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from localstack import config
from localstack.aws.api.opensearch import DomainEndpointOptions, EngineType
from localstack.config import EDGE_BIND_HOST
from localstack.constants import LOCALHOST, LOCALHOST_HOSTNAME
from localstack.constants import LOCALHOST
from localstack.services.opensearch import versions
from localstack.services.opensearch.cluster import (
CustomEndpoint,
Expand All @@ -28,6 +28,7 @@
start_thread,
)
from localstack.utils.serving import Server
from localstack.utils.urls import localstack_host

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -115,11 +116,16 @@ def build_cluster_endpoint(
assigned_port = external_service_ports.reserve_port()
else:
assigned_port = external_service_ports.reserve_port()
return f"{config.LOCALSTACK_HOSTNAME}:{assigned_port}"

host_definition = localstack_host(use_localstack_hostname=True, custom_port=assigned_port)
return host_definition.host_and_port()
if config.OPENSEARCH_ENDPOINT_STRATEGY == "path":
return f"{config.LOCALSTACK_HOSTNAME}:{config.EDGE_PORT}/{engine_domain}/{domain_key.region}/{domain_key.domain_name}"
host_definition = localstack_host(use_localstack_hostname=True)
return f"{host_definition.host_and_port()}/{engine_domain}/{domain_key.region}/{domain_key.domain_name}"

# or through a subdomain (domain-name.region.opensearch.localhost.localstack.cloud)
return f"{domain_key.domain_name}.{domain_key.region}.{engine_domain}.{LOCALHOST_HOSTNAME}:{config.EDGE_PORT}"
host_definition = localstack_host(use_localhost_cloud=True)
return f"{domain_key.domain_name}.{domain_key.region}.{engine_domain}.{host_definition.host_and_port()}"


def determine_custom_endpoint(
Expand Down
11 changes: 8 additions & 3 deletions localstack/services/s3/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@
preprocess_request,
serve_custom_service_request_handlers,
)
from localstack.constants import LOCALHOST_HOSTNAME
from localstack.services.edge import ROUTER
from localstack.services.moto import call_moto
from localstack.services.plugins import ServiceLifecycleHook
Expand Down Expand Up @@ -130,6 +129,7 @@
from localstack.utils.collections import get_safe
from localstack.utils.patch import patch
from localstack.utils.strings import short_uid
from localstack.utils.urls import localstack_host

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -166,8 +166,13 @@ def __init__(self, message=None):

def get_full_default_bucket_location(bucket_name):
if config.HOSTNAME_EXTERNAL != config.LOCALHOST:
return f"{config.get_protocol()}://{config.HOSTNAME_EXTERNAL}:{config.get_edge_port_http()}/{bucket_name}/"
return f"{config.get_protocol()}://{bucket_name}.s3.{LOCALHOST_HOSTNAME}:{config.get_edge_port_http()}/"
host_definition = localstack_host(
use_hostname_external=True, custom_port=config.get_edge_port_http()
)
return f"{config.get_protocol()}://{host_definition.host_and_port()}/{bucket_name}/"
else:
host_definition = localstack_host(use_localhost_cloud=True)
return f"{config.get_protocol()}://{bucket_name}.s3.{host_definition.host_and_port()}/"


class S3Provider(S3Api, ServiceLifecycleHook):
Expand Down
13 changes: 8 additions & 5 deletions localstack/services/s3/s3_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
to_str,
)
from localstack.utils.time import timestamp_millis
from localstack.utils.urls import localstack_host
from localstack.utils.xml import strip_xmlns

# backend port (configured in s3_starter.py on startup)
Expand Down Expand Up @@ -1346,6 +1347,7 @@ def is_multipart_upload(query):

@staticmethod
def get_201_response(key, bucket_name):
host_definition = localstack_host(use_hostname_external=True)
return """
<PostResponse>
<Location>{protocol}://{host}/{encoded_key}</Location>
Expand All @@ -1355,7 +1357,7 @@ def get_201_response(key, bucket_name):
</PostResponse>
""".format(
protocol=get_service_protocol(),
host=config.HOSTNAME_EXTERNAL,
host=host_definition.host,
encoded_key=quote(key, safe=""),
key=key,
bucket=bucket_name,
Expand All @@ -1366,12 +1368,13 @@ def get_201_response(key, bucket_name):
def _update_location(content, bucket_name):
bucket_name = normalize_bucket_name(bucket_name)

host = config.HOSTNAME_EXTERNAL
if ":" not in host:
host = f"{host}:{config.service_port('s3')}"
host_definition = localstack_host(
use_hostname_external=True, custom_port=config.get_edge_port_http()
)
return re.sub(
r"<Location>\s*([a-zA-Z0-9\-]+)://[^/]+/([^<]+)\s*</Location>",
r"<Location>%s://%s/%s/\2</Location>" % (get_service_protocol(), host, bucket_name),
r"<Location>%s://%s/%s/\2</Location>"
% (get_service_protocol(), host_definition.host_and_port(), bucket_name),
content,
flags=re.MULTILINE,
)
Expand Down
14 changes: 10 additions & 4 deletions localstack/services/sqs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from queue import PriorityQueue
from typing import Dict, NamedTuple, Optional, Set

from localstack import config, constants
from localstack import config
from localstack.aws.api import RequestContext
from localstack.aws.api.sqs import (
InvalidAttributeName,
Expand All @@ -21,7 +21,7 @@
ReceiptHandleIsInvalid,
TagMap,
)
from localstack.config import external_service_url
from localstack.config import get_protocol
from localstack.services.sqs import constants as sqs_constants
from localstack.services.sqs.exceptions import (
InvalidAttributeValue,
Expand All @@ -35,6 +35,7 @@
)
from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
from localstack.utils.time import now
from localstack.utils.urls import localstack_host

LOG = logging.getLogger(__name__)

< F421 svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-fold-down"> Expand Down Expand Up @@ -249,13 +250,18 @@ def url(self, context: RequestContext) -> str:
# or us-east-2.queue.localhost.localstack.cloud:4566/000000000000/my-queue
region = "" if self.region == "us-east-1" else self.region + "."
scheme = context.request.scheme
host_url = f"{scheme}://{region}queue.{constants.LOCALHOST_HOSTNAME}:{config.EDGE_PORT}"

host_definition = localstack_host(use_localhost_cloud=True)
host_url = f"{scheme}://{region}queue.{host_definition.host_and_port()}"
elif config.SQS_ENDPOINT_STRATEGY == "path":
# https?://localhost:4566/queue/us-east-1/00000000000/my-queue (us-east-1)
host_url = f"{context.request.host_url}queue/{self.region}"
else:
if config.SQS_PORT_EXTERNAL:
host_url = external_service_url("sqs")
host_definition = localstack_host(
use_hostname_external=True, custom_port=config.SQS_PORT_EXTERNAL
)
host_url = f"{get_protocol()}://{host_definition.host_and_port()}"
Comment on lines 260 to +264
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey! i can see that SQS_PORT_EXTERNAL is still in the control path here although we decided to boot it. can we just remove this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is just matching existing behaviour. I'll get rid of it in a follow up PR targeting the v2 branch once this is merged 👍


return "{host}/{account_id}/{name}".format(
host=host_url.rstrip("/"),
Expand Down
66 changes: 65 additions & 1 deletion localstack/testing/pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import os
import re
import socket
import time
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple

Expand All @@ -21,7 +22,7 @@
from pytes F438 t_httpserver import HTTPServer
from werkzeug import Request, Response

from localstack import config
from localstack import config, constants
from localstack.aws.accounts import get_aws_account_id
from localstack.constants import TEST_AWS_ACCESS_KEY_ID, TEST_AWS_SECRET_ACCESS_KEY
from localstack.services.stores import (
Expand Down Expand Up @@ -2006,6 +2007,69 @@ def factory(**kwargs):
LOG.debug(f"Error cleaning up AppSync API: {api}, {e}")


@pytest.fixture
def assert_host_customisation(monkeypatch):
hostname_external = f"external-host-{short_uid()}"
# `LOCALSTACK_HOSTNAME` is really an internal variable that has been
# exposed to the user at some point in the past. It is used by some
# services that start resources (e.g. OpenSearch) to determine if the
# service has been started correctly (i.e. a health check). This means that
# the value must be resolvable by LocalStack or else the service resources
# won't start properly.
#
# One hostname that's always resolvable is the hostname of the process
# running LocalStack, so use that here.
#
# Note: We cannot use `localhost` since we explicitly check that the URL
# passed in does not contain `localhost`, unless it is requried to.
localstack_hostname = socket.gethostname()
monkeypatch.setattr(config, "HOSTNAME_EXTERNAL", hostname_external)
monkeypatch.setattr(config, "LOCALSTACK_HOSTNAME", localstack_hostname)

def asserter(
url: str,
*,
use_hostname_external: bool = False,
use_localstack_hostname: bool = False,
use_localstack_cloud: bool = False,
use_localhost: bool = False,
custom_host: Optional[str] = None,
):
if use_hostname_external:
assert hostname_external in url

assert localstack_hostname not in url
assert constants.LOCALHOST_HOSTNAME not in url
assert constants.LOCALHOST not in url
elif use_localstack_hostname:
assert localstack_hostname in url

assert hostname_external not in url
assert constants.LOCALHOST_HOSTNAME not in url
assert constants.LOCALHOST not in url
elif use_localstack_cloud:
assert constants.LOCALHOST_HOSTNAME in url

assert hostname_external not in url
assert localstack_hostname not in url
elif use_localhost:
assert constants.LOCALHOST in url

assert constants.LOCALHOST_HOSTNAME not in url
assert hostname_external not in url
assert localstack_hostname not in url
elif custom_host is not None:
assert custom_host in url

assert constants.LOCALHOST_HOSTNAME not in url
assert hostname_external not in url
assert localstack_hostname not in url
else:
raise ValueError("no assertions made")

yield asserter


@pytest.fixture
def echo_http_server(httpserver: HTTPServer):
"""Spins up a local HTTP echo server and returns the endpoint URL"""
Expand Down
41 changes: 41 additions & 0 deletions localstack/utils/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
from dataclasses import dataclass
from typing import Optional

from localstack import config, constants


def path_from_url(url: str) -> str:
return f'/{url.partition("://")[2].partition("/")[2]}' if "://" in url else url


def hostname_from_url(url: str) -> str:
return url.split("://")[-1].split("/")[0].split(":")[0]


@dataclass
class HostDefinition:
host: str
port: int

def host_and_port(self):
return f"{self.host}:{self.port}"


def localstack_host(
use_hostname_external: bool = False,
use_localstack_hostname: bool = False,
use_localhost_cloud: bool = False,
custom_port: Optional[int] = None,
) -> HostDefinition:
"""
Determine the host and port to return to the user based on:
- the user's configuration (e.g environment variable overrides)
- the defaults of the system
"""
port = config.EDGE_PORT
if custom_port is not None:
port = custom_port

host = config.LOCALHOST
if use_hostname_external:
host = config.HOSTNAME_EXTERNAL
elif use_localstack_hostname:
host = config.LOCALSTACK_HOSTNAME
elif use_localhost_cloud:
host = constants.LOCALHOST_HOSTNAME

return HostDefinition(host=host, port=port)
Loading
0