Skip to content

Commit a4f0e80

Browse files
authored
feat(infra/local): Enable AUTHZ_BACKEND in local infrastructure configuration (#174)
## Summary by CodeRabbit ## Release Notes * **New Features** * Added fine-grained access control infrastructure to the platform, excluding Superset * Enabled OAuth-based single sign-on authentication for Superset. * Automated local certificate generation for development environments. Only used for Trino access. * **Documentation** * Updated local development setup instructions with required /etc/hosts configuration. * Expanded service endpoints and access documentation. * Clarified HTTPS requirement for Trino connections. * Reorganised testing guidance to emphasise unit tests. * **Infrastructure** * Configured file-based access control for Trino. * Enhanced Traefik routing with TLS support. * Streamlined authentication realm configuration.
1 parent 54ddf76 commit a4f0e80

41 files changed

Lines changed: 2045 additions & 3639 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/run-pytest-with-uv/action.yml

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ runs:
2424
using: composite
2525
steps:
2626
- name: Install uv
27-
uses: astral-sh/setup-uv@v5
27+
uses: astral-sh/setup-uv@v7
2828
with:
29+
activate-environment: false
2930
cache-dependency-glob: ${{ inputs.uv-cache-dependency-glob }}
3031
version: ${{ inputs.uv-version }}
3132
python-version: ${{ inputs.python-version }}
@@ -43,23 +44,11 @@ runs:
4344
run: uv sync --locked --all-extras --dev
4445
working-directory: ${{ inputs.pyproject-directory }}
4546

46-
- name: Add minio to /etc/hosts
47-
if: inputs.compose-file-path != ''
48-
shell: bash -l {0}
49-
run: |
50-
echo "127.0.0.1 minio" | sudo tee -a /etc/hosts
51-
5247
- name: Run tests
5348
shell: bash -l {0}
5449
run: uv run pytest --durations-min=0.5 --exitfirst "${{ inputs.pytest-file-or-dir }}" --cache-clear
5550
working-directory: ${{ inputs.pyproject-directory }}
5651

57-
- name: Remove minio from /etc/hosts
58-
if: inputs.compose-file-path != ''
59-
shell: bash -l {0}
60-
run: |
61-
sudo sed -i -e '/minio/d' /etc/hosts
62-
6352
- name: Dump Docker Compose logs on failure
6453
if: failure() && inputs.compose-file-path != ''
6554
shell: bash -l {0}

.github/workflows/ci-static.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ jobs:
1212
name: static checks
1313
runs-on: ubuntu-latest
1414
steps:
15-
- uses: actions/checkout@v3
16-
- uses: actions/setup-python@v3
15+
- uses: actions/checkout@v6
16+
- uses: actions/setup-python@v6
1717
- name: "Install prek"
1818
run: >
1919
python -m pip install prek

.github/workflows/docker-superset.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
id-token: write
3434
steps:
3535
- name: Checkout repository
36-
uses: actions/checkout@v5
36+
uses: actions/checkout@v6
3737
- name: Compute fully qualified image name
3838
run: echo "FQ_IMAGE_NAME=${{ env.REGISTRY }}/${{ env.ORG_NAME }}/${{ env.IMAGE_NAME }}" >> $GITHUB_ENV
3939
- name: Log in to the Container registry

.github/workflows/elt-common_e2e_tests.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,22 @@ concurrency:
2020
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
2121
cancel-in-progress: true
2222

23+
env:
24+
TRINO_CATALOG_STORE: memory
25+
2326
jobs:
2427
test:
2528
name: elt-common end-to-end tests
2629
runs-on: ubuntu-latest
2730

2831
steps:
2932
- name: Checkout
30-
uses: actions/checkout@v4
33+
uses: actions/checkout@v6
34+
35+
- name: Add adp-router to /etc/hosts
36+
shell: bash -l {0}
37+
run: |
38+
echo "127.0.0.1 adp-router" | sudo tee -a /etc/hosts
3139
3240
- name: Run end-to-end tests
3341
uses: ./.github/actions/run-pytest-with-uv
@@ -36,3 +44,8 @@ jobs:
3644
pyproject-directory: elt-common
3745
pytest-file-or-dir: tests/e2e_tests
3846
uv-cache-dependency-glob: elt-common/pyproject.toml
47+
48+
- name: Remove adp-router from /etc/hosts
49+
shell: bash -l {0}
50+
run: |
51+
sudo sed -i -e '/adp-router/d' /etc/hosts

.github/workflows/elt-common_unit_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323

2424
steps:
2525
- name: Checkout
26-
uses: actions/checkout@v4
26+
uses: actions/checkout@v6
2727

2828
- name: Run unit tests
2929
uses: ./.github/actions/run-pytest-with-uv

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ uv run pytest unit_tests
5757

5858
```bash
5959
pushd infra/local; docker compose up -d; popd # start the services
60+
echo "127.0.0.1 adp-router" | sudo tee -a /etc/hosts # edit /etc/hosts
6061
cd elt-common/
6162
uv run pytest e2e_tests
6263
pushd infra/local; docker compose down -v; popd # stop the services

elt-common/README.md

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,55 +13,33 @@ Development requires the following tools:
1313
### Setting up a Python virtual environment
1414

1515
Once `uv` is installed, create an environment and install the `elt-common`
16-
package in editable mode using the following command:
16+
package in editable mode, along with the development dependencies:
1717

1818
```bash
19+
> uv venv
20+
> source .venv/bin/activate
1921
> uv pip install --editable . --group dev
2022
```
2123

22-
## Running end-to-end tests
23-
24-
The end-to-end (e2e) tests for the `pyiceberg` destination require a running Iceberg
25-
rest catalog to test complete functionality.
26-
A (`docker-compose`)[./tests/docker-compose.yml] file is provided to both run the
27-
required services and provide a `python-uv` service for executing test commands.
28-
Please ensure you have docker and docker compose available on your command line
29-
before continuing.
30-
31-
To run the end-to-end tests, from this directory execute
32-
33-
```bash
34-
> docker compose -f tests/docker-compose.yml run python-uv uv run pytest tests/e2e_tests
35-
```
24+
## Running unit tests
3625

37-
When you have finished running the tests run
26+
Run the unit tests using `pytest`:
3827

3928
```bash
40-
> docker compose -f tests/docker-compose.yml down
29+
> pytest tests/unit_tests
4130
```
4231

43-
to bring down the dependent services.
44-
45-
### Debugging
46-
47-
Using a debugger to debug the end-to-end tests is more complicated as it requires
48-
the dependent services to be accessible using their service names from within
49-
the compose file.
50-
51-
To workaround this the `/etc/hosts` file can be edited to map the service names
52-
to localhost (127.0.0.1). Open `/etc/hosts` and add
32+
## Running end-to-end tests
5333

54-
```text
55-
# docker compose services
56-
127.0.0.1 minio
57-
127.0.0.1 keycloak
58-
127.0.0.1 lakekeeper
59-
```
34+
The end-to-end (e2e) tests for the `pyiceberg` destination require a running Iceberg
35+
rest catalog to test complete functionality.
36+
The local, docker-compose-based configuration provided by
37+
[infra/local/docker-compose.yml](../infra/local/docker-compose.yml) is the easiest way to
38+
spin up a set of services compatible with running the tests.
39+
_Note the requirement to edit `/etc/hosts` described in [here](../infra/local/README.md)._
6040

61-
Now bring up the services:
41+
Once the compose services are running, execute the e2e tests using `pytest`:
6242

6343
```bash
64-
> docker compose -f tests/docker-compose.yml up -d
44+
> pytest tests/e2e_tests
6545
```
66-
67-
and start your debugger as normal.

elt-common/src/elt_common/dlt_destinations/pyiceberg/configuration.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
@configspec(init=False)
1616
class PyIcebergRestCatalogCredentials(CredentialsConfiguration):
1717
uri: str = None # type: ignore
18+
project_id: Optional[str] = None
1819
warehouse: Optional[str] = None
1920
access_delegation: TPyIcebergAccessDelegation = "vended-credentials"
2021
oauth2_server_uri: Optional[str] = None # This is the endpoint to use to retrieve a token
@@ -29,8 +30,9 @@ def as_dict(self) -> Dict[str, str]:
2930
properties = {"credential": self.client_credential()} if self.client_id else {}
3031

3132
field_aliases: Dict[str, str] = {
32-
"access_delegation": f"{CATALOG_HEADER_PREFIX}X-Iceberg-Access-Delegation",
33+
"access_delegation": f"{CATALOG_HEADER_PREFIX}x-iceberg-access-delegation",
3334
"oauth2_server_uri": "oauth2-server-uri",
35+
"project_id": f"{CATALOG_HEADER_PREFIX}x-project-id",
3436
}
3537
skip_fields = ("client_id", "client_secret")
3638
properties.update(

elt-common/src/elt_common/iceberg/trino.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,6 @@ def _create_engine(self, credentials: TrinoCredentials) -> Engine:
109109
connect_args={
110110
"auth": BasicAuthentication(credentials.user, credentials.password),
111111
"http_scheme": credentials.http_scheme,
112+
"verify": False,
112113
},
113114
)

elt-common/tests/e2e_tests/conftest.py

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from contextlib import contextmanager
22
import dataclasses
33
from pathlib import Path
4+
import time
45
from typing import Any, Callable, Dict, Generator, List
56
import urllib.parse
67
import uuid
@@ -28,7 +29,7 @@
2829

2930
_RETRY_ARGS = {
3031
"wait": tenacity.wait_exponential(max=10),
31-
"stop": tenacity.stop_after_attempt(10),
32+
"stop": tenacity.stop_after_attempt(5),
3233
"reraise": True,
3334
}
3435

@@ -76,33 +77,36 @@ class Settings(BaseSettings):
7677
# The default values assume the docker-compose.yml in the infra/local has been used.
7778
# These are provided for the convenience of easily running a debugger without having
7879
# to set up remote debugging
79-
host_netloc: str = "localhost:58080"
80-
docker_netloc: str = "traefik"
81-
s3_access_key: str = "adpuser"
80+
host_netloc: str = "localhost:50080"
81+
docker_netloc: str = "adp-router:50080"
82+
s3_access_key: str = "adpsuperuser"
8283
s3_secret_key: str = "adppassword"
83-
s3_bucket: str = "e2e-tests-warehouse"
84-
s3_endpoint: str = "http://minio:59000"
84+
s3_bucket: str = "e2e-tests"
85+
s3_endpoint: str = "http://adp-router:59000"
8586
s3_region: str = "local-01"
8687
s3_path_style_access: bool = True
87-
openid_client_id: str = "localinfra"
88+
openid_client_id: str = "machine-infra"
8889
openid_client_secret: str = "s3cr3t"
8990
openid_scope: str = "lakekeeper"
91+
project_id: str = "c4fcd44f-7ce7-4446-9f7c-dcc7ba76dd22"
9092
warehouse_name: str = "e2e_tests"
9193

9294
# trino
93-
trino_http_scheme: str = "http"
95+
trino_http_scheme: str = "https"
9496
trino_host: str = "localhost"
95-
trino_port: str = "59088"
96-
trino_user: str = "trino"
97-
trino_password: str = ""
97+
trino_port: str = "58443"
98+
trino_user: str = "machine-infra"
99+
trino_password: str = "s3cr3t"
98100

99101
@property
100102
def lakekeeper_url(self) -> Endpoint:
101103
return Endpoint(f"http://{self.host_netloc}/iceberg", self.docker_netloc)
102104

103105
@property
104106
def openid_provider_uri(self) -> Endpoint:
105-
return Endpoint(f"http://{self.host_netloc}/auth/realms/iceberg", self.docker_netloc)
107+
return Endpoint(
108+
f"http://{self.host_netloc}/auth/realms/analytics-data-platform", self.docker_netloc
109+
)
106110

107111
def storage_config(self) -> Dict[str, Any]:
108112
return {
@@ -129,26 +133,12 @@ def storage_config(self) -> Dict[str, Any]:
129133

130134

131135
class Server:
136+
"""Wraps a Lakekeeper instance. It is assumed that the instance is bootstrapped."""
137+
132138
def __init__(self, access_token: str, settings: Settings):
133139
self.access_token = access_token
134140
self.settings = settings
135141

136-
# Bootstrap server once
137-
management_endpoint_v1 = self.management_endpoint(version=1)
138-
server_info = self._request_with_auth(
139-
requests.get,
140-
url=management_endpoint_v1 + "/info",
141-
)
142-
server_info.raise_for_status()
143-
server_info = server_info.json()
144-
if not server_info["bootstrapped"]:
145-
response = self._request_with_auth(
146-
requests.post,
147-
management_endpoint_v1 + "/bootstrap",
148-
json={"accept-terms-of-use": True},
149-
)
150-
response.raise_for_status()
151-
152142
@property
153143
def token_endpoint(self) -> Endpoint:
154144
return self.settings.openid_provider_uri + "/protocol/openid-connect/token"
@@ -169,13 +159,11 @@ def management_endpoint(self, *, version: int | None = None) -> Endpoint:
169159
def warehouse_endpoint(self, *, version: int = 1) -> Endpoint:
170160
return self.management_endpoint(version=version) + "/warehouse"
171161

172-
def create_warehouse(
173-
self, name: str, project_id: uuid.UUID, storage_config: dict
174-
) -> "Warehouse":
162+
def create_warehouse(self, name: str, project_id: str, storage_config: dict) -> "Warehouse":
175163
"""Create a warehouse in this server"""
176164

177165
payload = {
178-
"project-id": str(project_id),
166+
"project-id": project_id,
179167
**storage_config,
180168
}
181169

@@ -233,6 +221,7 @@ def connect(self) -> PyIcebergCatalog:
233221
"""Connect to the warehouse in the catalog"""
234222
creds = PyIcebergCatalogCredentials()
235223
creds.uri = str(self.server.catalog_endpoint())
224+
creds.project_id = self.server.settings.project_id
236225
creds.warehouse = self.name
237226
creds.oauth2_server_uri = str(self.server.token_endpoint)
238227
creds.client_id = self.server.settings.openid_client_id
@@ -326,12 +315,7 @@ def server(access_token: str) -> Server:
326315

327316

328317
@pytest.fixture(scope="session")
329-
def project() -> uuid.UUID:
330-
return uuid.UUID("{00000000-0000-0000-0000-000000000000}")
331-
332-
333-
@pytest.fixture(scope="session")
334-
def warehouse(server: Server, project: uuid.UUID) -> Generator:
318+
def warehouse(server: Server) -> Generator:
335319
if not settings.warehouse_name:
336320
raise ValueError("Empty 'warehouse_name' is not allowed.")
337321

@@ -349,7 +333,9 @@ def warehouse(server: Server, project: uuid.UUID) -> Generator:
349333
minio_client.make_bucket(bucket_name=bucket_name)
350334
print(f"Bucket {bucket_name} created.")
351335

352-
warehouse = server.create_warehouse(settings.warehouse_name, project, storage_config)
336+
warehouse = server.create_warehouse(
337+
settings.warehouse_name, server.settings.project_id, storage_config
338+
)
353339
print(f"Warehouse {warehouse.project_id} created.")
354340
try:
355341
yield warehouse
@@ -360,6 +346,8 @@ def _remove_bucket(bucket_name):
360346
minio_client.remove_bucket(bucket_name=bucket_name)
361347

362348
try:
349+
# Allow a brief pause for the test operations to complete
350+
time.sleep(1)
363351
server.purge_warehouse(warehouse)
364352
server.delete_warehouse(warehouse)
365353
_remove_bucket(bucket_name)

0 commit comments

Comments
 (0)