From 180c7a5328457aefdf847ada366e296fef4744f1 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 10 Jul 2021 16:37:30 -0700 Subject: [PATCH] --uds option for binding to Unix domain socket, closes #1388 --- datasette/cli.py | 7 +++++++ docs/datasette-serve-help.txt | 1 + docs/deploying.rst | 23 ++++++++++++++++++++++- tests/conftest.py | 20 +++++++++++++++++++- tests/test_cli.py | 1 + tests/test_cli_serve_server.py | 15 +++++++++++++++ 6 files changed, 65 insertions(+), 2 deletions(-) diff --git a/datasette/cli.py b/datasette/cli.py index 12ee92c3..09aebcc8 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -333,6 +333,10 @@ def uninstall(packages, yes): type=click.IntRange(0, 65535), help="Port for server, defaults to 8001. Use -p 0 to automatically assign an available port.", ) +@click.option( + "--uds", + help="Bind to a Unix domain socket", +) @click.option( "--reload", is_flag=True, @@ -428,6 +432,7 @@ def serve( immutable, host, port, + uds, reload, cors, sqlite_extensions, @@ -569,6 +574,8 @@ def serve( uvicorn_kwargs = dict( host=host, port=port, log_level="info", lifespan="on", workers=1 ) + if uds: + uvicorn_kwargs["uds"] = uds if ssl_keyfile: uvicorn_kwargs["ssl_keyfile"] = ssl_keyfile if ssl_certfile: diff --git a/docs/datasette-serve-help.txt b/docs/datasette-serve-help.txt index db51dd80..ec3f41a0 100644 --- a/docs/datasette-serve-help.txt +++ b/docs/datasette-serve-help.txt @@ -12,6 +12,7 @@ Options: machines. -p, --port INTEGER RANGE Port for server, defaults to 8001. Use -p 0 to automatically assign an available port. [0<=x<=65535] + --uds TEXT Bind to a Unix domain socket --reload Automatically reload if code or metadata change detected - useful for development --cors Enable CORS by serving Access-Control-Allow-Origin: * diff --git a/docs/deploying.rst b/docs/deploying.rst index 44ddd07b..f3680034 100644 --- a/docs/deploying.rst +++ b/docs/deploying.rst @@ -148,7 +148,6 @@ Here is an example of an `nginx `__ configuration file that http { server { listen 80; - location /my-datasette { proxy_pass http://127.0.0.1:8009/my-datasette; proxy_set_header X-Real-IP $remote_addr; @@ -157,6 +156,28 @@ Here is an example of an `nginx `__ configuration file that } } +You can also use the ``--uds`` option to Datasette to listen on a Unix domain socket instead of a port, configuring the nginx upstream proxy like this:: + + daemon off; + events { + worker_connections 1024; + } + http { + server { + listen 80; + location / { + proxy_pass http://datasette; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } + upstream datasette { + server unix:/tmp/datasette.sock; + } + } + +Then run Datasette with ``datasette --uds /tmp/datasette.sock path/to/database.db``. + Apache proxy configuration -------------------------- diff --git a/tests/conftest.py b/tests/conftest.py index c6a3eee6..34a64efc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -131,7 +131,6 @@ def ds_localhost_https_server(tmp_path_factory): for blob in server_cert.cert_chain_pems: blob.write_to_path(path=certfile, append=True) ca.cert_pem.write_to_path(path=client_cert) - ds_proc = subprocess.Popen( [ "datasette", @@ -154,3 +153,22 @@ def ds_localhost_https_server(tmp_path_factory): yield ds_proc, client_cert # Shut it down at the end of the pytest session ds_proc.terminate() + + +@pytest.fixture(scope="session") +def ds_unix_domain_socket_server(tmp_path_factory): + socket_folder = tmp_path_factory.mktemp("uds") + uds = str(socket_folder / "datasette.sock") + ds_proc = subprocess.Popen( + ["datasette", "--memory", "--uds", uds], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=tempfile.gettempdir(), + ) + # Give the server time to start + time.sleep(1.5) + # Check it started successfully + assert not ds_proc.poll(), ds_proc.stdout.read().decode("utf-8") + yield ds_proc, uds + # Shut it down at the end of the pytest session + ds_proc.terminate() diff --git a/tests/test_cli.py b/tests/test_cli.py index e094ccb6..e31a305e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -132,6 +132,7 @@ def test_metadata_yaml(): immutable=[], host="127.0.0.1", port=8001, + uds=None, reload=False, cors=False, sqlite_extensions=[], diff --git a/tests/test_cli_serve_server.py b/tests/test_cli_serve_server.py index 6f5366d1..73439125 100644 --- a/tests/test_cli_serve_server.py +++ b/tests/test_cli_serve_server.py @@ -1,5 +1,6 @@ import httpx import pytest +import socket @pytest.mark.serial @@ -21,3 +22,17 @@ def test_serve_localhost_https(ds_localhost_https_server): "path": "/_memory", "tables": [], }.items() <= response.json().items() + + +@pytest.mark.serial +@pytest.mark.skipif(not hasattr(socket, "AF_UNIX"), reason="Requires socket.AF_UNIX support") +def test_serve_unix_domain_socket(ds_unix_domain_socket_server): + _, uds = ds_unix_domain_socket_server + transport = httpx.HTTPTransport(uds=uds) + client = httpx.Client(transport=transport) + response = client.get("http://localhost/_memory.json") + assert { + "database": "_memory", + "path": "/_memory", + "tables": [], + }.items() <= response.json().items()