local-development-gateway 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +207 -14
- data/docs/architecture/local-tcp-routing.md +356 -0
- data/lib/local_development_gateway/database_router/docker_routes.rb +24 -9
- data/lib/local_development_gateway/database_router/drivers/postgre_sql_driver.rb +79 -13
- data/lib/local_development_gateway/database_router/drivers/sql_server_driver.rb +27 -5
- data/lib/local_development_gateway/database_router/postgre_sql/cancellation_routes.rb +47 -0
- data/lib/local_development_gateway/database_router/postgre_sql/session.rb +46 -0
- data/lib/local_development_gateway/database_router/wire.rb +63 -2
- data/lib/local_development_gateway/database_router.rb +31 -30
- data/lib/local_development_gateway/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31c635514c35283b8ebab124d6a9f9ac8e17e8de5faaeb8573b0c73153b3efd4
|
|
4
|
+
data.tar.gz: 946d3a12335cff8a00f10a5ac330de37b5f5ec509a33779e44cefc265b8c0554
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04b07e9aeb612b723744759bebb4f8e19319ac885a9dff3163446eaebcaf9a424cb203c36c89649a2378d1db675c5a167aade5bc0f33e6c82ccb70c11d7bcec6
|
|
7
|
+
data.tar.gz: 28528704687762d7a053b62f7c1fc1e7f5af76700e4c332efadaef32cb5c594e420af8bbd53803a1e3d0cecdafa3d892ceeee5edbac100aa388ea72e98a80de1
|
data/README.md
CHANGED
|
@@ -112,19 +112,102 @@ The gateway includes protocol drivers for encrypted SQL Server and PostgreSQL
|
|
|
112
112
|
connections. No host agent, DNS server, administrator privileges, or
|
|
113
113
|
`/etc/hosts` changes are required.
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
### Three connection paths
|
|
116
|
+
|
|
117
|
+
| Caller | Database hostname | Routing |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| Application/API, background job, test container | `${COMPOSE_PROJECT_NAME}-<database-service>` | Qualified alias on that checkout's private `default` network; internal database port |
|
|
120
|
+
| Desktop client, such as DBeaver | `db.<worktree>.<project>.localhost` | Encrypted, hostname-labelled gateway endpoint on loopback |
|
|
121
|
+
| Database-local console, backup, clone command | `localhost` | Explicitly select the intended database container first |
|
|
122
|
+
|
|
123
|
+
Do not use bare `db` or `db_trav_com` for application connections in the current
|
|
124
|
+
gateway topology. An API attached to both `default` and `local-gateway` can see
|
|
125
|
+
other projects' database service aliases on the shared network. Docker DNS or
|
|
126
|
+
network ordering cannot reliably choose its own database. Separate database
|
|
127
|
+
volumes do not prevent connecting to the wrong server.
|
|
128
|
+
|
|
129
|
+
The qualified alias belongs **only on the private `default` network**. Compose
|
|
130
|
+
project names must be distinct for concurrent checkouts. `.localhost` desktop
|
|
131
|
+
names resolve to loopback, not to an internal database container; they are not
|
|
132
|
+
substitutes for private aliases. Database cloning is independent of this
|
|
133
|
+
hostname correction: it selects source/destination containers explicitly and
|
|
134
|
+
connects locally.
|
|
135
|
+
|
|
136
|
+
### Canonical development and test Compose pattern
|
|
137
|
+
|
|
138
|
+
Merge these entries into the project's existing development base and gateway
|
|
139
|
+
overlay. Keep its database images, credentials, volumes, and application
|
|
140
|
+
commands. The base must not publish database host ports. Use the driver's
|
|
141
|
+
existing configuration keys: `AZURE_SQL_HOST` is a SQL Server application
|
|
142
|
+
example; `PGHOST` is the PostgreSQL/libpq equivalent. An application with a
|
|
143
|
+
different PostgreSQL configuration key must set that key instead.
|
|
144
|
+
|
|
145
|
+
For example, a wrapper-derived `COMPOSE_PROJECT_NAME=issue-123-example` gives
|
|
146
|
+
private hosts `issue-123-example-db` and `issue-123-example-db_trav_com`.
|
|
147
|
+
`GATEWAY_HOSTNAME=issue-123.example.localhost` separately identifies desktop
|
|
148
|
+
routes. This requires no new Ruby API, generic database environment variable,
|
|
149
|
+
or consumer runtime.
|
|
117
150
|
|
|
118
151
|
```yaml
|
|
152
|
+
# docker-compose.development.yml: also safe without the gateway overlay.
|
|
119
153
|
services:
|
|
154
|
+
api:
|
|
155
|
+
environment:
|
|
156
|
+
AZURE_SQL_HOST: "${COMPOSE_PROJECT_NAME}-db"
|
|
157
|
+
PGHOST: "${COMPOSE_PROJECT_NAME}-db_trav_com"
|
|
158
|
+
jobs:
|
|
159
|
+
environment:
|
|
160
|
+
AZURE_SQL_HOST: "${COMPOSE_PROJECT_NAME}-db"
|
|
161
|
+
PGHOST: "${COMPOSE_PROJECT_NAME}-db_trav_com"
|
|
162
|
+
test_api:
|
|
163
|
+
environment:
|
|
164
|
+
AZURE_SQL_HOST: "${COMPOSE_PROJECT_NAME}-db"
|
|
165
|
+
PGHOST: "${COMPOSE_PROJECT_NAME}-db_trav_com"
|
|
120
166
|
db:
|
|
167
|
+
networks:
|
|
168
|
+
default:
|
|
169
|
+
aliases:
|
|
170
|
+
- "${COMPOSE_PROJECT_NAME}-db"
|
|
171
|
+
db_trav_com:
|
|
172
|
+
networks:
|
|
173
|
+
default:
|
|
174
|
+
aliases:
|
|
175
|
+
- "${COMPOSE_PROJECT_NAME}-db_trav_com"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The example's driver assignments are illustrative. In
|
|
179
|
+
[Travel Authorization's overlay](https://github.com/icefoganalytics/travel-authorization/blob/main/docker-compose.development.gateway.yml),
|
|
180
|
+
`db` is PostgreSQL and `db_trav_com` is SQL Server: its PostgreSQL host setting
|
|
181
|
+
must use `${COMPOSE_PROJECT_NAME}-db`, and its SQL Server host setting must use
|
|
182
|
+
`${COMPOSE_PROJECT_NAME}-db_trav_com`, with labels/ports matching those drivers.
|
|
183
|
+
Apply the rule separately to **every** database service, even when two services
|
|
184
|
+
use the same driver. Preserve each context's existing credentials, internal
|
|
185
|
+
port, and database name; tests must still select the test database rather than
|
|
186
|
+
development data.
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
# docker-compose.development.gateway.yml
|
|
190
|
+
services:
|
|
191
|
+
api:
|
|
121
192
|
networks:
|
|
122
193
|
- default
|
|
123
194
|
- local-gateway
|
|
195
|
+
db:
|
|
196
|
+
networks:
|
|
197
|
+
default: {}
|
|
198
|
+
local-gateway: {}
|
|
124
199
|
labels:
|
|
125
200
|
- "local-gateway.tcp.driver=sql_server"
|
|
126
|
-
- "local-gateway.tcp.hostname=db.${GATEWAY_HOSTNAME
|
|
201
|
+
- "local-gateway.tcp.hostname=db.${GATEWAY_HOSTNAME}"
|
|
127
202
|
- "local-gateway.tcp.port=1433"
|
|
203
|
+
db_trav_com:
|
|
204
|
+
networks:
|
|
205
|
+
default: {}
|
|
206
|
+
local-gateway: {}
|
|
207
|
+
labels:
|
|
208
|
+
- "local-gateway.tcp.driver=postgresql"
|
|
209
|
+
- "local-gateway.tcp.hostname=db.${GATEWAY_HOSTNAME}"
|
|
210
|
+
- "local-gateway.tcp.port=5432"
|
|
128
211
|
|
|
129
212
|
networks:
|
|
130
213
|
local-gateway:
|
|
@@ -132,16 +215,64 @@ networks:
|
|
|
132
215
|
name: local-gateway
|
|
133
216
|
```
|
|
134
217
|
|
|
135
|
-
|
|
136
|
-
|
|
218
|
+
Use **mapping-form** database networks in both files. Compose merges
|
|
219
|
+
`default: {}` with the base's `default.aliases`; replacing that mapping with a
|
|
220
|
+
network list can lose the private alias. Do not add qualified aliases to
|
|
221
|
+
`local-gateway`. Jobs/tests need only `default` unless they themselves expose a
|
|
222
|
+
routed service; qualified database names remain safe if an existing overlay
|
|
223
|
+
also attaches them to the shared network.
|
|
224
|
+
|
|
225
|
+
The shared router owns `127.0.0.1:1433` and `127.0.0.1:5432`. No participating
|
|
226
|
+
database publishes either port, so multiple development/test stacks can start
|
|
227
|
+
concurrently. Internal connections use each server's internal port and existing
|
|
228
|
+
encryption policy; they do not pass through the router or require a published
|
|
229
|
+
port.
|
|
230
|
+
|
|
231
|
+
### Explicit non-gateway fallback
|
|
232
|
+
|
|
233
|
+
Container-only development/tests work with the base alone. If desktop access
|
|
234
|
+
without the gateway is required, select a separate, project-local publishing
|
|
235
|
+
overlay **instead of** the gateway overlay:
|
|
236
|
+
|
|
237
|
+
```yaml
|
|
238
|
+
# docker-compose.development.ports.yml: opt-in, non-gateway mode only.
|
|
239
|
+
services:
|
|
240
|
+
db:
|
|
241
|
+
ports:
|
|
242
|
+
- "127.0.0.1:1433:1433"
|
|
243
|
+
db_trav_com:
|
|
244
|
+
ports:
|
|
245
|
+
- "127.0.0.1:5432:5432"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
These fallback ports must be free; this mode does not promise parallel desktop
|
|
249
|
+
access. Do not combine it with the gateway or leave these publications in the
|
|
250
|
+
base/test files. When migrating a project that retains base publications, its
|
|
251
|
+
gateway overlay must explicitly clear each database's ports with
|
|
252
|
+
`ports: !reset []` (Docker Compose 2.24.4 or newer); simply omitting `ports` or
|
|
253
|
+
setting `ports: []` does not remove inherited publications. Prefer the
|
|
254
|
+
port-free base plus explicit fallback above.
|
|
255
|
+
|
|
256
|
+
Use the project's documented development wrapper to select Compose modes and
|
|
257
|
+
start/test/stop services. Inspect its rendered `config`: gateway mode must have
|
|
258
|
+
no database host publications and must retain the private aliases. A base-only
|
|
259
|
+
test wrapper must use the same qualified hosts.
|
|
260
|
+
|
|
261
|
+
### Desktop database clients
|
|
262
|
+
|
|
263
|
+
For `GATEWAY_HOSTNAME=issue-123.example.localhost`, connect to
|
|
264
|
+
`db.issue-123.example.localhost` on the driver's standard port:
|
|
137
265
|
|
|
138
266
|
| Driver label | Host port | Client requirement |
|
|
139
267
|
| --- | ---: | --- |
|
|
140
268
|
| `sql_server` | `1433` | Encryption enabled and **Trust server certificate** selected |
|
|
141
|
-
| `postgresql` | `5432` | SSL mode `require
|
|
269
|
+
| `postgresql` | `5432` | SSL mode `require`, SNI enabled, development certificate accepted |
|
|
142
270
|
|
|
143
271
|
Use the database container's actual internal port in
|
|
144
|
-
`local-gateway.tcp.port`; it
|
|
272
|
+
`local-gateway.tcp.port`; it need not match the host listener. Different drivers
|
|
273
|
+
can share a hostname because their listeners differ. Two databases using the
|
|
274
|
+
same driver need distinct labels, for example `db.${GATEWAY_HOSTNAME}` and
|
|
275
|
+
`db_trav_com.${GATEWAY_HOSTNAME}`.
|
|
145
276
|
|
|
146
277
|
All `.localhost` names resolve to loopback. The gateway publishes loopback-only
|
|
147
278
|
listeners, discovers labelled containers through Docker, and uses the database
|
|
@@ -149,12 +280,38 @@ protocol's encrypted handshake to select the hostname-labelled container.
|
|
|
149
280
|
Container replacements and stopped routes require no consumer lifecycle code
|
|
150
281
|
or cleanup.
|
|
151
282
|
|
|
152
|
-
SQL Server encryption remains end to end through the router.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
283
|
+
SQL Server encryption remains end to end through the router. PostgreSQL TLS
|
|
284
|
+
terminates at the gateway; the backend stream is plaintext on the private Docker
|
|
285
|
+
network. New PostgreSQL sessions require TLS and SNI, but query cancellation
|
|
286
|
+
does not: the gateway substitutes a session-specific cancellation identity and
|
|
287
|
+
routes ordinary `psql` and JDBC cancellation packets to that session's backend.
|
|
288
|
+
Closed sessions lose their cancellation mapping. See
|
|
156
289
|
[`docs/architecture/local-tcp-routing.md`](docs/architecture/local-tcp-routing.md)
|
|
157
|
-
for the complete contract and security boundary
|
|
290
|
+
for the complete contract and security boundary, including the reason for
|
|
291
|
+
retaining cancellation-aware PostgreSQL routing despite Traefik's STARTTLS support.
|
|
292
|
+
|
|
293
|
+
### Downstream adoption checklist
|
|
294
|
+
|
|
295
|
+
1. Give each concurrent checkout a distinct Compose project name and desktop
|
|
296
|
+
route hostname. Qualify every API/job/test database host with
|
|
297
|
+
`${COMPOSE_PROJECT_NAME}-<database-service>` using its existing driver setting.
|
|
298
|
+
2. Advertise each qualified alias only on its private `default` network;
|
|
299
|
+
preserve it through mapping-form network merges in base-only and gateway modes.
|
|
300
|
+
3. Remove fixed database publications from development/test stacks; retain
|
|
301
|
+
desktop fallback publications only in an explicitly selected non-gateway file.
|
|
302
|
+
4. Recreate database and consumer services so changed aliases and environment
|
|
303
|
+
settings take effect. Inspect both rendered Compose configurations.
|
|
304
|
+
5. Query real, read-only server/database identity from API, job, and test
|
|
305
|
+
contexts and compare it with the explicitly selected local database
|
|
306
|
+
container. Stop that checkout's database while another is available: its
|
|
307
|
+
consumers must fail, not select the other checkout. Record the actual DNS or
|
|
308
|
+
connection error; there is no universal platform error code.
|
|
309
|
+
6. Verify both encrypted desktop routes, and restore normal gateway attachments
|
|
310
|
+
after test wrappers recreate services using base-only Compose.
|
|
311
|
+
|
|
312
|
+
Shared development networks are not a hostile-container security boundary.
|
|
313
|
+
Qualified private aliases prevent accidental cross-stack selection; they do not
|
|
314
|
+
prevent an attached, malicious container from connecting to another database.
|
|
158
315
|
|
|
159
316
|
## Development checks
|
|
160
317
|
|
|
@@ -169,6 +326,7 @@ bundle exec ruby "$(bundle show syntax_tree)/exe/stree" check \
|
|
|
169
326
|
lib/local_development_gateway/database_router.rb \
|
|
170
327
|
lib/local_development_gateway/database_router/*.rb \
|
|
171
328
|
lib/local_development_gateway/database_router/drivers/*.rb \
|
|
329
|
+
lib/local_development_gateway/database_router/postgre_sql/*.rb \
|
|
172
330
|
lib/local_development_gateway/database_router/tds/*.rb \
|
|
173
331
|
lib/local_development_gateway/version.rb \
|
|
174
332
|
bin/dev \
|
|
@@ -176,6 +334,7 @@ bundle exec ruby "$(bundle show syntax_tree)/exe/stree" check \
|
|
|
176
334
|
test/*.rb \
|
|
177
335
|
test/database_router/*.rb \
|
|
178
336
|
test/database_router/drivers/*.rb \
|
|
337
|
+
test/database_router/postgre_sql/*.rb \
|
|
179
338
|
test/database_router/tds/*.rb
|
|
180
339
|
rake test
|
|
181
340
|
```
|
|
@@ -195,8 +354,8 @@ The gateway owns the single browser port on `127.0.0.1:80`. Participating projec
|
|
|
195
354
|
|
|
196
355
|
The gateway:
|
|
197
356
|
|
|
198
|
-
- listens only on `127.0.0.1:80`;
|
|
199
|
-
- creates the external Docker network `local-gateway
|
|
357
|
+
- listens only on `127.0.0.1:80`, `127.0.0.1:1433`, and `127.0.0.1:5432`;
|
|
358
|
+
- creates the external Docker network `local-gateway` for HTTP and labelled database ingress;
|
|
200
359
|
- discovers only services explicitly labelled for routing;
|
|
201
360
|
- has the Docker label `local-gateway=true` so a project can detect it.
|
|
202
361
|
|
|
@@ -225,6 +384,14 @@ Use a project- and service-prefixed router/service identifier. Never derive a de
|
|
|
225
384
|
|
|
226
385
|
Without the gateway, a participating project may use its own loopback port-publishing fallback. That fallback is project-specific and is not part of the shared gateway contract.
|
|
227
386
|
|
|
387
|
+
Application/job/test database connections follow the
|
|
388
|
+
[private-alias contract](#three-connection-paths), not desktop gateway hostnames
|
|
389
|
+
or bare database service names. Database services retain their private network
|
|
390
|
+
and add gateway labels without publishing host ports. The
|
|
391
|
+
[architecture decision](docs/architecture/local-tcp-routing.md#internal-database-isolation-decision)
|
|
392
|
+
explains why a future HTTP/database ingress split requires coordinated consumer
|
|
393
|
+
migration rather than a transparent gem update.
|
|
394
|
+
|
|
228
395
|
## Authentication callbacks
|
|
229
396
|
|
|
230
397
|
A wildcard hostname lets an identity provider allow one local callback pattern instead of every generated port. For Traditional Knowledge, configure:
|
|
@@ -251,3 +418,29 @@ It mounts the Docker socket so it can discover labelled containers. A read-only
|
|
|
251
418
|
| Generated hostname returns `404` | Confirm the web container has the route labels and is attached to `local-gateway`. |
|
|
252
419
|
| A project uses normal ports unexpectedly | Confirm the gateway container is running and has `local-gateway=true`. |
|
|
253
420
|
| Authentication rejects the callback | Confirm the generated hostname matches the allowed wildcard callback pattern. |
|
|
421
|
+
|
|
422
|
+
## Release 0.2.2
|
|
423
|
+
|
|
424
|
+
- Canonical SQL Server/PostgreSQL Compose integration supports concurrent
|
|
425
|
+
development/test stacks without per-stack database host publications.
|
|
426
|
+
- Application, job, and test connections use checkout-qualified private aliases
|
|
427
|
+
instead of ambiguous shared-network service names. Base-only mode, explicit
|
|
428
|
+
non-gateway fallback, multiple databases, and adoption checks are documented.
|
|
429
|
+
- The packaged architecture reference records the current private-alias
|
|
430
|
+
decision, real-container Linux validation, and coordinated migration required
|
|
431
|
+
for a future HTTP/database ingress split.
|
|
432
|
+
- Gateway routing and the public Ruby API are unchanged. Consumers must adopt
|
|
433
|
+
the Compose contract; upgrading the gem does not rewrite their configuration.
|
|
434
|
+
|
|
435
|
+
## Release 0.2.1
|
|
436
|
+
|
|
437
|
+
- PostgreSQL query cancellation works across concurrent worktrees, including
|
|
438
|
+
plaintext cancellation from `psql` and JDBC and encrypted cancellation.
|
|
439
|
+
Session-specific identities prevent backend key collisions and expire when
|
|
440
|
+
their session closes.
|
|
441
|
+
- Invalid database labels and duplicate hostname identities reject only the
|
|
442
|
+
affected routes, leaving unrelated worktrees and database drivers available.
|
|
443
|
+
- SQL Server negotiation can fall back from a stalled backend within the
|
|
444
|
+
shared handshake deadline.
|
|
445
|
+
- PostgreSQL STARTTLS support in Traefik is documented accurately. The gateway
|
|
446
|
+
retains cancellation-aware session routing without changing consumer labels.
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# Local TCP Routing Architecture
|
|
2
|
+
|
|
3
|
+
Status: SQL Server and PostgreSQL protocol routing implemented in PR #22 and
|
|
4
|
+
validated on Linux. Windows Docker Desktop validation remains outstanding.
|
|
5
|
+
|
|
6
|
+
## Request
|
|
7
|
+
|
|
8
|
+
Extend Local Development Gateway so a participating Docker Compose project can
|
|
9
|
+
expose hostname-routed database services with the same simple declarative
|
|
10
|
+
integration it already uses for HTTP services.
|
|
11
|
+
|
|
12
|
+
For WRAP, database routing should require only a small change to
|
|
13
|
+
`docker-compose.development.gateway.yml`. WRAP must not implement address
|
|
14
|
+
allocation, routing, DNS, host-file management, startup orchestration, or
|
|
15
|
+
cleanup.
|
|
16
|
+
|
|
17
|
+
The initial endpoints are:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
db.<worktree>.wrap.localhost:1433 # SQL Server
|
|
21
|
+
db.<worktree>.wrap.localhost:5432 # PostgreSQL
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Each must connect from DBeaver to that worktree's labelled database container.
|
|
25
|
+
|
|
26
|
+
## Consumer Contract
|
|
27
|
+
|
|
28
|
+
A participating project keeps its databases on its private `default` network
|
|
29
|
+
and adds the existing `local-gateway` network plus three gateway-owned labels:
|
|
30
|
+
driver, desktop hostname, and internal port. The copyable SQL Server and
|
|
31
|
+
PostgreSQL base/overlay examples live in the README's
|
|
32
|
+
[canonical Compose pattern](../../README.md#canonical-development-and-test-compose-pattern).
|
|
33
|
+
|
|
34
|
+
| Connection path | Destination selection |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| API, background job, test | `${COMPOSE_PROJECT_NAME}-<database-service>` on private Docker DNS, using the driver's existing host setting and internal port |
|
|
37
|
+
| Desktop database client | Encrypted `db.<worktree>.<project>.localhost` endpoint; driver/hostname labels select the backend |
|
|
38
|
+
| Database-local console, backup, clone | Explicitly selected container, then `localhost`; not gateway routing |
|
|
39
|
+
|
|
40
|
+
Each concurrent checkout has a distinct Compose project name. Each database
|
|
41
|
+
advertises its qualified alias only on that project's `default` network.
|
|
42
|
+
Use mapping-form database networks in the base and gateway overlay so Compose
|
|
43
|
+
preserves `default.aliases`. Configure all application/job/test contexts with
|
|
44
|
+
their appropriate aliases, existing credentials, and intended database names;
|
|
45
|
+
test contexts still select their own test database.
|
|
46
|
+
|
|
47
|
+
Gateway-mode development and test stacks publish no database host ports.
|
|
48
|
+
Container consumers work with the base alone. Desktop access without the
|
|
49
|
+
gateway is an explicit project-local port-publishing overlay, not an inherited
|
|
50
|
+
base default and not a parallel-stack guarantee. The README covers migration
|
|
51
|
+
of inherited port publications, fallback selection, and adoption checks.
|
|
52
|
+
|
|
53
|
+
Internal database hostnames and desktop `.localhost` hostnames are separate
|
|
54
|
+
contracts. Internal connections need neither a published host port nor the
|
|
55
|
+
gateway's encrypted-handshake protocol. Database cloning does not depend on
|
|
56
|
+
the internal hostname correction.
|
|
57
|
+
|
|
58
|
+
Requirements:
|
|
59
|
+
|
|
60
|
+
- No consumer Ruby API calls.
|
|
61
|
+
- No consumer-side address allocator.
|
|
62
|
+
- No per-worktree host-port selection.
|
|
63
|
+
- No bind-address environment variable passed from application code.
|
|
64
|
+
- No consumer startup or shutdown hook solely for gateway routing.
|
|
65
|
+
- No consumer cleanup call.
|
|
66
|
+
- No manually maintained DBeaver port per worktree.
|
|
67
|
+
- The consumer supplies only declarative service identity and internal-port metadata.
|
|
68
|
+
|
|
69
|
+
- No administrator or root installation.
|
|
70
|
+
- No host `/etc/hosts`, DNS, resolver, systemd, or Windows-service changes.
|
|
71
|
+
- The same gateway Compose stack must run through Docker on Linux and Windows.
|
|
72
|
+
|
|
73
|
+
## Internal Database Isolation Decision
|
|
74
|
+
|
|
75
|
+
**Decision:** retain the current shared ingress topology and standardize
|
|
76
|
+
checkout-qualified private aliases. This resolves
|
|
77
|
+
[#23](https://github.com/icefoganalytics/local-development-gateway/issues/23)'s
|
|
78
|
+
parallel-stack contract and
|
|
79
|
+
[#26](https://github.com/icefoganalytics/local-development-gateway/issues/26)'s
|
|
80
|
+
internal database selection contract without new runtime machinery or public
|
|
81
|
+
Ruby lifecycle changes.
|
|
82
|
+
|
|
83
|
+
Bare `db` is not safe for an API attached to both its private bridge and
|
|
84
|
+
`local-gateway`: other attached databases advertise the same Compose service
|
|
85
|
+
alias on shared ingress. Docker DNS/network ordering, container IDs, separate
|
|
86
|
+
volumes, and gateway TLS/SNI cannot disambiguate an ordinary internal connection.
|
|
87
|
+
The qualified alias identifies only its own private endpoint. If that endpoint
|
|
88
|
+
is absent, its consumers must fail rather than connect to another checkout.
|
|
89
|
+
Do not advertise the qualified alias on shared ingress.
|
|
90
|
+
|
|
91
|
+
An HTTP/database ingress split is a possible future centralization, not a
|
|
92
|
+
transparent gem-only patch:
|
|
93
|
+
|
|
94
|
+
- HTTP services/APIs: private checkout network plus existing HTTP ingress.
|
|
95
|
+
- Databases: private checkout network plus a distinct gateway-owned database
|
|
96
|
+
ingress network. APIs/jobs/tests must not join database ingress.
|
|
97
|
+
- Traefik: HTTP ingress only. Database router: database ingress only.
|
|
98
|
+
- Route discovery must read backend addresses on database ingress instead of
|
|
99
|
+
`DockerRoutes::NETWORK_NAME`'s current `local-gateway`.
|
|
100
|
+
- Gateway readiness/discovery must check the appropriate networks and services.
|
|
101
|
+
Unused-gateway shutdown must retain running consumers on either ingress,
|
|
102
|
+
including database-only stacks, without counting exited containers.
|
|
103
|
+
- Consumer network membership, installed gem versions, and already-running
|
|
104
|
+
shared gateways require coordinated migration. Old consumers still
|
|
105
|
+
advertising database aliases on HTTP ingress remain ambiguous after a
|
|
106
|
+
gateway-only update.
|
|
107
|
+
|
|
108
|
+
That alternative requires packaged install/readiness, database-only lifecycle,
|
|
109
|
+
multi-project startup/stop/replacement, SQL Server routing, PostgreSQL routing
|
|
110
|
+
and cancellation, mixed-version migration, and unchanged HTTP-routing evidence
|
|
111
|
+
before adoption. No structural split is implemented in this release; downstream
|
|
112
|
+
qualified-alias mitigations must remain until a verified replacement is adopted.
|
|
113
|
+
|
|
114
|
+
These development networks are not hostile-container security boundaries.
|
|
115
|
+
Private qualified aliases prevent accidental selection, not deliberate
|
|
116
|
+
connections by another attached container.
|
|
117
|
+
|
|
118
|
+
## Gateway Responsibilities
|
|
119
|
+
|
|
120
|
+
All implementation belongs in this repository. The gateway must:
|
|
121
|
+
|
|
122
|
+
1. Discover participating containers and their routing metadata through Docker.
|
|
123
|
+
2. Derive or read the requested hostname and target internal port.
|
|
124
|
+
3. Distinguish concurrent worktrees even when they expose the same internal TCP port.
|
|
125
|
+
4. Make `db.<worktree>.wrap.localhost` resolve or connect to the correct worktree route on the local machine.
|
|
126
|
+
5. Forward the raw TCP stream to the labelled container.
|
|
127
|
+
6. Add, update, and remove routes automatically as containers start, restart, and stop.
|
|
128
|
+
7. Preserve stable routing for a worktree while it remains active.
|
|
129
|
+
8. Keep all listeners and generated routes local to the developer machine.
|
|
130
|
+
9. Preserve the existing HTTP routing behavior.
|
|
131
|
+
|
|
132
|
+
## Implemented Routing Shape
|
|
133
|
+
|
|
134
|
+
Every `.localhost` name intentionally resolves to loopback on both Linux and
|
|
135
|
+
Windows. A shared port therefore requires a protocol-level hostname. The
|
|
136
|
+
gateway uses one Docker-label discovery path and a small driver for each
|
|
137
|
+
supported database handshake:
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
DBeaver
|
|
141
|
+
-> db.<worktree>.wrap.localhost:<driver port>
|
|
142
|
+
-> Docker publishes the database router on loopback
|
|
143
|
+
-> driver reads the encrypted handshake hostname
|
|
144
|
+
-> router selects the matching Docker label
|
|
145
|
+
-> router forwards the database stream to the labelled container
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The `sql_server` driver mediates Tabular Data Stream (TDS) PRELOGIN, reads SNI
|
|
149
|
+
from the wrapped TLS ClientHello, then preserves the encrypted client/backend
|
|
150
|
+
stream. The
|
|
151
|
+
`postgresql` driver accepts PostgreSQL's SSLRequest, terminates the client TLS
|
|
152
|
+
session to obtain SNI, and forwards the plaintext PostgreSQL stream on the
|
|
153
|
+
private Docker network.
|
|
154
|
+
|
|
155
|
+
Traefik remains responsible for HTTP. It cannot inspect TLS SNI wrapped inside
|
|
156
|
+
TDS PRELOGIN. It **does** support PostgreSQL SSLRequest/STARTTLS and subsequent
|
|
157
|
+
TLS-SNI routing, as documented in its
|
|
158
|
+
[TCP TLS reference](https://doc.traefik.io/traefik/reference/routing-configuration/tcp/tls/).
|
|
159
|
+
|
|
160
|
+
STARTTLS alone is therefore not a reason to implement PostgreSQL routing here.
|
|
161
|
+
The gateway also owns PostgreSQL query cancellation: `psql` and JDBC can open
|
|
162
|
+
a separate plaintext CancelRequest connection containing only a backend PID and
|
|
163
|
+
secret, with no hostname. A stateless SNI router cannot select that backend.
|
|
164
|
+
Keeping negotiation and cancellation-aware session forwarding together preserves
|
|
165
|
+
the existing three-label consumer contract without per-route internal listeners,
|
|
166
|
+
generated Traefik configuration, or another discovery/control plane.
|
|
167
|
+
|
|
168
|
+
Normal database sessions require encrypted handshakes so the requested hostname
|
|
169
|
+
is present as TLS SNI. PostgreSQL cancellation is a bounded control-message
|
|
170
|
+
exception, not support for plaintext database sessions. The router parses
|
|
171
|
+
PostgreSQL backend messages only until BackendKeyData, replaces that key, then
|
|
172
|
+
forwards the remainder without interpreting query results.
|
|
173
|
+
|
|
174
|
+
## Code Organization
|
|
175
|
+
|
|
176
|
+
`DatabaseRouter` owns listener concurrency and backend connection lifecycle.
|
|
177
|
+
`Wire` owns bounded handshake I/O and bidirectional forwarding, including
|
|
178
|
+
shutdown that wakes both relay directions. `DockerApi` and `DockerRoutes` form
|
|
179
|
+
the external Docker boundary and produce validated `Route` values.
|
|
180
|
+
Behavior-bearing classes each have one file, and directories map to Ruby
|
|
181
|
+
namespaces: drivers live under `DatabaseRouter::Drivers`, TDS framing and TLS
|
|
182
|
+
parsing under `DatabaseRouter::Tds`, and PostgreSQL session-key forwarding and
|
|
183
|
+
cancellation lookup under `DatabaseRouter::PostgreSql`. Tiny immutable `Route`,
|
|
184
|
+
`Connection`, cancellation destination, and TDS `Packet` records stay with
|
|
185
|
+
their owners.
|
|
186
|
+
|
|
187
|
+
Source and test imports resolve from the gem's `lib` load path and start at
|
|
188
|
+
`local_development_gateway`; domain files never traverse sibling paths with
|
|
189
|
+
`require_relative`.
|
|
190
|
+
|
|
191
|
+
The focused tests mirror those boundaries under `test/database_router/`.
|
|
192
|
+
Protocol scenarios keep their literal setup beside the behavior they verify
|
|
193
|
+
instead of sharing a generic fixture layer.
|
|
194
|
+
|
|
195
|
+
## Hostname Contract
|
|
196
|
+
|
|
197
|
+
- Keep the established WRAP hostname family.
|
|
198
|
+
- HTTP remains:
|
|
199
|
+
- `<worktree>.wrap.localhost`
|
|
200
|
+
- `api.<worktree>.wrap.localhost`
|
|
201
|
+
- `mail.<worktree>.wrap.localhost`
|
|
202
|
+
- SQL Server is:
|
|
203
|
+
- `db.<worktree>.wrap.localhost`
|
|
204
|
+
- PostgreSQL uses the same hostname on port `5432`.
|
|
205
|
+
- Do not migrate consumers to `.test`, `.alt`, `.local`, `home.arpa`, or an externally registered domain as part of this issue.
|
|
206
|
+
- Do not require host resolver configuration; `.localhost` must retain its native loopback behavior.
|
|
207
|
+
|
|
208
|
+
## Security Boundary
|
|
209
|
+
|
|
210
|
+
- No routed service may be reachable from another machine.
|
|
211
|
+
- Bind host listeners only to loopback addresses.
|
|
212
|
+
- Do not publish generated names through public DNS.
|
|
213
|
+
- Do not expose SQL Server on `0.0.0.0` or a LAN interface.
|
|
214
|
+
- Do not require privileged host integration.
|
|
215
|
+
- Do not grant a long-running container unrestricted write access to arbitrary host files.
|
|
216
|
+
- The database router receives the same read-only Docker socket already required by Traefik so it can resolve labels to current container addresses.
|
|
217
|
+
- SQL Server remains encrypted end to end. PostgreSQL is plaintext only inside the private Docker network after the gateway terminates client TLS.
|
|
218
|
+
|
|
219
|
+
## Lifecycle
|
|
220
|
+
|
|
221
|
+
- Normal session selection reads current Docker metadata; starting, restarting,
|
|
222
|
+
or stopping a labelled container requires no stored route configuration or
|
|
223
|
+
consumer cleanup.
|
|
224
|
+
- Containers outside `local-gateway` are excluded before label validation.
|
|
225
|
+
Invalid participating routes are warned about and quarantined individually.
|
|
226
|
+
- Duplicate active driver/hostname identities reject every route for that
|
|
227
|
+
identity, not unrelated identities or database drivers.
|
|
228
|
+
- A connection snapshot never changes backend after selection.
|
|
229
|
+
- PostgreSQL rewrites BackendKeyData with a random, collision-checked virtual
|
|
230
|
+
identity whose PID is positive for older client compatibility. The active
|
|
231
|
+
mapping retains the exact backend address, port, and original cancellation key.
|
|
232
|
+
- Cancellation uses that mapping without Docker discovery or hostname
|
|
233
|
+
negotiation. Unknown and closed-session identities fail closed, and malformed
|
|
234
|
+
cancellation lengths never fall through to normal backend forwarding.
|
|
235
|
+
- Backend EOF, client disconnect, or relay failure removes the session mapping.
|
|
236
|
+
Gateway restart closes sessions and clears their cancellation identities;
|
|
237
|
+
normal route configuration is reconstructed from Docker metadata.
|
|
238
|
+
- Handshakes and active sessions have separate bounded admission pools.
|
|
239
|
+
Cancellation and closed health probes do not consume an active-session slot,
|
|
240
|
+
so cancellation remains available when the normal session limit is reached.
|
|
241
|
+
- SQL Server provisional attempts have a bounded sub-deadline within the shared
|
|
242
|
+
handshake deadline. Connection establishment uses only the remaining budget;
|
|
243
|
+
stalled provisional sockets are closed before a healthy alternative is tried.
|
|
244
|
+
|
|
245
|
+
## Compatibility
|
|
246
|
+
|
|
247
|
+
- DBeaver connects to `db.<worktree>.wrap.localhost` on `1433` for SQL Server
|
|
248
|
+
or `5432` for PostgreSQL.
|
|
249
|
+
- SQL Server clients must enable encryption and trust the development server
|
|
250
|
+
certificate.
|
|
251
|
+
- PostgreSQL clients must use SSL mode `require`, send SNI, and permit the
|
|
252
|
+
gateway's generated development certificate.
|
|
253
|
+
- Concurrent SQL Server routes must use compatible PRELOGIN encryption
|
|
254
|
+
settings because the client receives a provisional backend's negotiation
|
|
255
|
+
response before SNI identifies the final backend.
|
|
256
|
+
- PostgreSQL backends must accept a plaintext connection from the private
|
|
257
|
+
`local-gateway` Docker network.
|
|
258
|
+
- PostgreSQL cancellation PIDs reported by client APIs are virtual. Use
|
|
259
|
+
`SELECT pg_backend_pid()` when inspecting the physical backend in server views.
|
|
260
|
+
- Existing browser routes through Traefik on `127.0.0.1:80` remain unchanged.
|
|
261
|
+
- Multiple active worktrees can use each database driver's standard port
|
|
262
|
+
simultaneously.
|
|
263
|
+
- Adding another protocol requires another explicit driver; ports are never
|
|
264
|
+
inferred from labels.
|
|
265
|
+
|
|
266
|
+
## Acceptance Scenarios
|
|
267
|
+
|
|
268
|
+
1. Start worktree A and worktree B with the same database driver and internal
|
|
269
|
+
port.
|
|
270
|
+
2. Connect a DBeaver-equivalent client to
|
|
271
|
+
`db.<A>.wrap.localhost:<driver port>`; observe bytes reaching only A.
|
|
272
|
+
3. Connect to `db.<B>.wrap.localhost:<driver port>`; observe bytes reaching
|
|
273
|
+
only B.
|
|
274
|
+
4. Repeat the routing check for both `sql_server` and `postgresql`.
|
|
275
|
+
5. Keep both connections possible concurrently without alternate host ports.
|
|
276
|
+
6. Recreate A's database container; the same hostname routes to its replacement.
|
|
277
|
+
7. Stop A; A's route disappears while B remains available.
|
|
278
|
+
8. Verify existing HTTP hostnames still use Traefik.
|
|
279
|
+
9. Verify every host listener is loopback-only.
|
|
280
|
+
10. Demonstrate consumer integration as a Compose-only diff.
|
|
281
|
+
|
|
282
|
+
## Non-Goals
|
|
283
|
+
|
|
284
|
+
- Changing consumer application code, adding new development wrapper runtimes,
|
|
285
|
+
or automatically mutating consumer networks. Existing Compose connection
|
|
286
|
+
settings and file selection must adopt the documented contract.
|
|
287
|
+
- Requiring consumers to run DNS, a proxy, or host installation.
|
|
288
|
+
- Assigning a different database port to each worktree.
|
|
289
|
+
- Publishing development routes outside the machine.
|
|
290
|
+
- Replacing existing HTTP routing.
|
|
291
|
+
- Building a generic service mesh or full database proxy.
|
|
292
|
+
- Supporting plaintext database sessions, PostgreSQL direct TLS negotiation, or
|
|
293
|
+
database protocols without an implemented driver. Plaintext PostgreSQL
|
|
294
|
+
cancellation control packets are supported.
|
|
295
|
+
|
|
296
|
+
## Verification Evidence
|
|
297
|
+
|
|
298
|
+
PR #22 demonstrated:
|
|
299
|
+
|
|
300
|
+
1. Two exact `db.<worktree>.wrap.localhost` names selected different labelled
|
|
301
|
+
containers for SQL Server and PostgreSQL.
|
|
302
|
+
2. Docker-label discovery updated routes without consumer lifecycle code;
|
|
303
|
+
stopped routes were rejected while surviving routes remained reachable.
|
|
304
|
+
3. Fragmented and oversized handshakes, stalled clients, duplicate routes,
|
|
305
|
+
unsupported drivers, and unreachable provisional backends failed safely.
|
|
306
|
+
4. Linux validation required no host resolver, administrator/root, or
|
|
307
|
+
operating-system service setup. Windows Docker Desktop was unavailable for
|
|
308
|
+
equivalent validation.
|
|
309
|
+
5. Existing HTTP routes remained unchanged and every published listener was
|
|
310
|
+
loopback-only.
|
|
311
|
+
|
|
312
|
+
Issue #24's corrective release additionally demonstrated:
|
|
313
|
+
|
|
314
|
+
1. PostgreSQL 14.4 `psql` cancellation stopped A's query while B stayed active.
|
|
315
|
+
2. DBeaver's Java runtime with PostgreSQL JDBC 42.7.13 and 42.2.27 returned
|
|
316
|
+
SQLSTATE `57014` for A while B completed its query normally.
|
|
317
|
+
3. Two SQL Server 2022 backends remained reachable with encrypted `sqlcmd`
|
|
318
|
+
connections despite a first labelled backend that accepted TCP but never
|
|
319
|
+
answered PRELOGIN.
|
|
320
|
+
4. Focused regressions cover invalid-route isolation, cancellation identity
|
|
321
|
+
collisions after PID normalization, variable backend keys, session cleanup,
|
|
322
|
+
malformed cancellation packets, and cancellation at full session capacity.
|
|
323
|
+
|
|
324
|
+
Issues #23 and #26's documentation release demonstrated on Linux with Docker
|
|
325
|
+
Compose 5.6.0:
|
|
326
|
+
|
|
327
|
+
1. Two isolated Compose projects, each with SQL Server 2022 `db` and PostgreSQL
|
|
328
|
+
17 `db_trav_com`, started concurrently without database host publications.
|
|
329
|
+
The repository's `bin/dev` Compose passthrough selected the temporary
|
|
330
|
+
consumer fixtures; the existing shared gateway was reused.
|
|
331
|
+
2. Real SQL/PG client containers representing API, job, and test contexts joined
|
|
332
|
+
both private and shared networks. `getent hosts` selected only their private
|
|
333
|
+
qualified aliases. Read-only SQL Server `@@SERVERNAME` / `DB_NAME()` and
|
|
334
|
+
PostgreSQL `cluster_name` / `current_database()` / `inet_server_addr()`
|
|
335
|
+
identified their own servers. Test contexts selected `qa_test`; explicit
|
|
336
|
+
database-container `localhost` queries matched the consumer identities.
|
|
337
|
+
3. Stopping A's two databases made every A context fail while B remained
|
|
338
|
+
usable. `getent hosts` returned exit 1; SQL Server reported login timeout,
|
|
339
|
+
TCP Provider `0x2AFA`, and server-not-found/not-accessible; PostgreSQL reported
|
|
340
|
+
`could not translate host name ... to address: Try again`. These are observed
|
|
341
|
+
platform errors, not required portable error codes.
|
|
342
|
+
4. Both projects were recreated using base-only Compose. All consumer identity
|
|
343
|
+
checks passed with private-only attachments. Recreating with the gateway
|
|
344
|
+
overlay restored shared attachments and preserved private aliases and test
|
|
345
|
+
database selection.
|
|
346
|
+
5. Desktop-equivalent host-network clients selected both labelled worktrees:
|
|
347
|
+
`sqlcmd` 18.6 with `-Nm -C` (mandatory encryption, trusted development
|
|
348
|
+
certificate), and PostgreSQL 17 `psql` with `sslmode=require`, SNI, hostname,
|
|
349
|
+
and explicit loopback `hostaddr`. SQL Server strict/direct TLS is outside the
|
|
350
|
+
supported TDS PRELOGIN contract.
|
|
351
|
+
6. Docker inspection confirmed the shared gateway publishes only
|
|
352
|
+
`127.0.0.1:80`, `127.0.0.1:1433`, and `127.0.0.1:5432`.
|
|
353
|
+
|
|
354
|
+
This validates real database client contexts, not an automatic downstream
|
|
355
|
+
application migration. Consumers still need to adopt the README checklist.
|
|
356
|
+
Windows Docker Desktop validation remains outstanding.
|
|
@@ -18,18 +18,35 @@ module LocalDevelopmentGateway
|
|
|
18
18
|
routes =
|
|
19
19
|
@client
|
|
20
20
|
.get("/containers/json")
|
|
21
|
-
.filter_map
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
.filter_map do |container|
|
|
22
|
+
begin
|
|
23
|
+
route(container)
|
|
24
|
+
rescue Error => error
|
|
25
|
+
warn error.message
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
identities = routes.group_by { |route| [route.driver, route.hostname] }
|
|
30
|
+
identities.each do |identity, matches|
|
|
31
|
+
next if matches.length == 1
|
|
32
|
+
|
|
33
|
+
warn "Duplicate labelled database hostname: #{identity.join(" ")}"
|
|
25
34
|
end
|
|
26
35
|
|
|
27
|
-
routes
|
|
36
|
+
routes
|
|
37
|
+
.reject do |route|
|
|
38
|
+
identities.fetch([route.driver, route.hostname]).length > 1
|
|
39
|
+
end
|
|
40
|
+
.sort_by { |route| [route.driver, route.hostname] }
|
|
28
41
|
end
|
|
29
42
|
|
|
30
43
|
private
|
|
31
44
|
|
|
32
45
|
def route(container)
|
|
46
|
+
target_address =
|
|
47
|
+
container.dig("NetworkSettings", "Networks", NETWORK_NAME, "IPAddress")
|
|
48
|
+
return if target_address.nil? || target_address.empty?
|
|
49
|
+
|
|
33
50
|
labels = container.fetch("Labels")
|
|
34
51
|
driver = labels[DRIVER_LABEL]
|
|
35
52
|
hostname = labels[HOSTNAME_LABEL]
|
|
@@ -52,16 +69,14 @@ module LocalDevelopmentGateway
|
|
|
52
69
|
raise Error, "Invalid labelled database port: #{port}"
|
|
53
70
|
end
|
|
54
71
|
|
|
55
|
-
target_address =
|
|
56
|
-
container.dig("NetworkSettings", "Networks", NETWORK_NAME, "IPAddress")
|
|
57
|
-
return if target_address.nil? || target_address.empty?
|
|
58
|
-
|
|
59
72
|
DatabaseRouter::Route.new(
|
|
60
73
|
driver: driver,
|
|
61
74
|
hostname: hostname,
|
|
62
75
|
port: port,
|
|
63
76
|
target_address: target_address
|
|
64
77
|
)
|
|
78
|
+
rescue KeyError
|
|
79
|
+
raise Error, "Incomplete labelled database route"
|
|
65
80
|
rescue ArgumentError, TypeError
|
|
66
81
|
raise Error, "Invalid labelled database port: #{port}"
|
|
67
82
|
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "openssl"
|
|
4
|
+
|
|
4
5
|
require "local_development_gateway/database_router/wire"
|
|
5
6
|
require "local_development_gateway/database_router/drivers/postgre_sql_certificate"
|
|
7
|
+
require "local_development_gateway/database_router/postgre_sql/session"
|
|
6
8
|
|
|
7
9
|
module LocalDevelopmentGateway
|
|
8
10
|
module DatabaseRouter::Drivers
|
|
@@ -10,9 +12,14 @@ module LocalDevelopmentGateway
|
|
|
10
12
|
NAME = "postgresql"
|
|
11
13
|
LISTEN_PORT = 5432
|
|
12
14
|
SSL_REQUEST = [8, 80_877_103].pack("NN").freeze
|
|
15
|
+
CANCEL_REQUEST = [16, 80_877_102].pack("NN").freeze
|
|
13
16
|
|
|
14
|
-
def initialize(
|
|
17
|
+
def initialize(
|
|
18
|
+
certificate: PostgreSqlCertificate.new,
|
|
19
|
+
cancellations: DatabaseRouter::PostgreSql::CancellationRoutes.new
|
|
20
|
+
)
|
|
15
21
|
@certificate = certificate
|
|
22
|
+
@cancellations = cancellations
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
def name
|
|
@@ -23,18 +30,15 @@ module LocalDevelopmentGateway
|
|
|
23
30
|
LISTEN_PORT
|
|
24
31
|
end
|
|
25
32
|
|
|
26
|
-
def connect(client, routes
|
|
33
|
+
def connect(client, routes:, connector:)
|
|
27
34
|
deadline = DatabaseRouter::Wire.deadline
|
|
28
35
|
request =
|
|
29
|
-
DatabaseRouter::Wire.read_exactly(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
)
|
|
34
|
-
raise Error, "PostgreSQL SSL is required" unless request == SSL_REQUEST
|
|
35
|
-
if routes.empty?
|
|
36
|
-
raise Error, "No labelled postgresql routes are available"
|
|
36
|
+
DatabaseRouter::Wire.read_exactly(client, 8, deadline: deadline)
|
|
37
|
+
if cancel_request?(request)
|
|
38
|
+
cancel(client, connector, deadline)
|
|
39
|
+
return
|
|
37
40
|
end
|
|
41
|
+
raise Error, "PostgreSQL SSL is required" unless request == SSL_REQUEST
|
|
38
42
|
|
|
39
43
|
client.write("S")
|
|
40
44
|
hostname = nil
|
|
@@ -44,16 +48,78 @@ module LocalDevelopmentGateway
|
|
|
44
48
|
nil
|
|
45
49
|
end
|
|
46
50
|
tls = OpenSSL::SSL::SSLSocket.new(client, context)
|
|
47
|
-
tls.sync_close =
|
|
51
|
+
tls.sync_close = true
|
|
48
52
|
DatabaseRouter::Wire.accept_tls(tls, deadline: deadline)
|
|
53
|
+
|
|
54
|
+
startup = DatabaseRouter::Wire.read_exactly(tls, 8, deadline: deadline)
|
|
55
|
+
if cancel_request?(startup)
|
|
56
|
+
cancel(tls, connector, deadline)
|
|
57
|
+
tls.close
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
length, version = startup.unpack("NN")
|
|
62
|
+
unless length >= 9 && (version >> 16) == 3
|
|
63
|
+
raise Error, "Unsupported PostgreSQL startup request"
|
|
64
|
+
end
|
|
65
|
+
|
|
49
66
|
unless hostname
|
|
50
67
|
raise Error, "PostgreSQL TLS ClientHello does not contain SNI"
|
|
51
68
|
end
|
|
52
69
|
|
|
53
|
-
selected = routes.find { |route| route.hostname == hostname }
|
|
70
|
+
selected = routes.call.find { |route| route.hostname == hostname }
|
|
54
71
|
raise Error, "No PostgreSQL route for #{hostname}" unless selected
|
|
55
72
|
|
|
56
|
-
|
|
73
|
+
target = connector.call(selected, deadline: deadline)
|
|
74
|
+
target.write(startup)
|
|
75
|
+
DatabaseRouter::Connection.new(
|
|
76
|
+
source: tls,
|
|
77
|
+
target: target,
|
|
78
|
+
route: selected
|
|
79
|
+
)
|
|
80
|
+
rescue StandardError
|
|
81
|
+
tls&.close
|
|
82
|
+
target&.close
|
|
83
|
+
raise
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def forward(connection)
|
|
87
|
+
session = DatabaseRouter::PostgreSql::Session.new(@cancellations)
|
|
88
|
+
DatabaseRouter::Wire.proxy(
|
|
89
|
+
connection.source,
|
|
90
|
+
connection.target
|
|
91
|
+
) do |backend, client|
|
|
92
|
+
session.forward_response(backend, client, connection.route)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def cancel_request?(header)
|
|
99
|
+
return false unless header.byteslice(4, 4).unpack1("N") == 80_877_102
|
|
100
|
+
unless header == CANCEL_REQUEST
|
|
101
|
+
raise Error, "Invalid PostgreSQL cancellation length"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
true
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def cancel(client, connector, deadline)
|
|
108
|
+
identity =
|
|
109
|
+
DatabaseRouter::Wire.read_exactly(client, 8, deadline: deadline)
|
|
110
|
+
destination = @cancellations.resolve(identity)
|
|
111
|
+
return unless destination
|
|
112
|
+
|
|
113
|
+
target = connector.call(destination.route, deadline: deadline)
|
|
114
|
+
packet = [destination.backend_key.bytesize + 8, 80_877_102].pack("NN")
|
|
115
|
+
target.write(packet + destination.backend_key)
|
|
116
|
+
DatabaseRouter::Wire.read_until_eof(
|
|
117
|
+
target,
|
|
118
|
+
max_bytes: 0,
|
|
119
|
+
deadline: deadline
|
|
120
|
+
)
|
|
121
|
+
ensure
|
|
122
|
+
target&.close
|
|
57
123
|
end
|
|
58
124
|
end
|
|
59
125
|
end
|
|
@@ -6,6 +6,8 @@ require "local_development_gateway/database_router/tds/tls_client_hello"
|
|
|
6
6
|
module LocalDevelopmentGateway
|
|
7
7
|
module DatabaseRouter::Drivers
|
|
8
8
|
class SqlServerDriver
|
|
9
|
+
NEGOTIATION_ATTEMPT_TIMEOUT = 1
|
|
10
|
+
|
|
9
11
|
NAME = "sql_server"
|
|
10
12
|
LISTEN_PORT = 1433
|
|
11
13
|
|
|
@@ -17,9 +19,10 @@ module LocalDevelopmentGateway
|
|
|
17
19
|
LISTEN_PORT
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
def connect(client, routes
|
|
22
|
+
def connect(client, routes:, connector:)
|
|
21
23
|
deadline = DatabaseRouter::Wire.deadline
|
|
22
24
|
prelogin = DatabaseRouter::Tds::Message.read(client, deadline: deadline)
|
|
25
|
+
routes = routes.call
|
|
23
26
|
if routes.empty?
|
|
24
27
|
raise Error, "No labelled sql_server routes are available"
|
|
25
28
|
end
|
|
@@ -33,7 +36,7 @@ module LocalDevelopmentGateway
|
|
|
33
36
|
|
|
34
37
|
if selected != provisional
|
|
35
38
|
target.close
|
|
36
|
-
target = connector.call(selected)
|
|
39
|
+
target = connector.call(selected, deadline: deadline)
|
|
37
40
|
DatabaseRouter::Tds::Message.write(target, prelogin)
|
|
38
41
|
DatabaseRouter::Tds::Message.read(target, deadline: deadline)
|
|
39
42
|
end
|
|
@@ -41,22 +44,41 @@ module LocalDevelopmentGateway
|
|
|
41
44
|
messages.each do |message|
|
|
42
45
|
DatabaseRouter::Tds::Message.write(target, message)
|
|
43
46
|
end
|
|
44
|
-
|
|
47
|
+
DatabaseRouter::Connection.new(
|
|
48
|
+
source: client,
|
|
49
|
+
target: target,
|
|
50
|
+
route: selected
|
|
51
|
+
)
|
|
45
52
|
rescue StandardError
|
|
46
53
|
target&.close
|
|
47
54
|
raise
|
|
48
55
|
end
|
|
49
56
|
|
|
57
|
+
def forward(connection)
|
|
58
|
+
DatabaseRouter::Wire.proxy(connection.source, connection.target)
|
|
59
|
+
end
|
|
60
|
+
|
|
50
61
|
private
|
|
51
62
|
|
|
52
63
|
def negotiate(routes, prelogin, connector, deadline)
|
|
53
64
|
routes.each do |route|
|
|
54
|
-
|
|
65
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
66
|
+
break unless remaining.positive?
|
|
67
|
+
|
|
68
|
+
attempt_deadline = [
|
|
69
|
+
deadline,
|
|
70
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) +
|
|
71
|
+
NEGOTIATION_ATTEMPT_TIMEOUT
|
|
72
|
+
].min
|
|
73
|
+
target = connector.call(route, deadline: attempt_deadline)
|
|
55
74
|
DatabaseRouter::Tds::Message.write(target, prelogin)
|
|
56
75
|
return [
|
|
57
76
|
route,
|
|
58
77
|
target,
|
|
59
|
-
DatabaseRouter::Tds::Message.read(
|
|
78
|
+
DatabaseRouter::Tds::Message.read(
|
|
79
|
+
target,
|
|
80
|
+
deadline: attempt_deadline
|
|
81
|
+
)
|
|
60
82
|
]
|
|
61
83
|
rescue Error, EOFError, IOError, SystemCallError
|
|
62
84
|
target&.close
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module LocalDevelopmentGateway
|
|
6
|
+
module DatabaseRouter::PostgreSql
|
|
7
|
+
class CancellationRoutes
|
|
8
|
+
Destination = Data.define(:route, :backend_key)
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@destinations = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def register(route, backend_key)
|
|
16
|
+
@mutex.synchronize do
|
|
17
|
+
identity = new_identity
|
|
18
|
+
identity = new_identity while @destinations.key?(identity)
|
|
19
|
+
|
|
20
|
+
identity.freeze
|
|
21
|
+
@destinations[identity] = Destination.new(
|
|
22
|
+
route: route,
|
|
23
|
+
backend_key: backend_key.dup.freeze
|
|
24
|
+
)
|
|
25
|
+
identity
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resolve(identity)
|
|
30
|
+
@mutex.synchronize { @destinations[identity] }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def remove(identity)
|
|
34
|
+
@mutex.synchronize { @destinations.delete(identity) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def new_identity
|
|
40
|
+
identity = SecureRandom.random_bytes(8)
|
|
41
|
+
identity.setbyte(0, identity.getbyte(0) & 0x7f)
|
|
42
|
+
identity.setbyte(3, 1) if identity.unpack1("N").zero?
|
|
43
|
+
identity
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "local_development_gateway/database_router/postgre_sql/cancellation_routes"
|
|
4
|
+
|
|
5
|
+
module LocalDevelopmentGateway
|
|
6
|
+
class DatabaseRouter::PostgreSql::Session
|
|
7
|
+
MAX_BACKEND_KEY_BYTES = 260
|
|
8
|
+
|
|
9
|
+
def initialize(cancellations)
|
|
10
|
+
@cancellations = cancellations
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def forward_response(backend, client, route)
|
|
14
|
+
identity = nil
|
|
15
|
+
loop do
|
|
16
|
+
header = backend.read(5)
|
|
17
|
+
raise EOFError unless header&.bytesize == 5
|
|
18
|
+
|
|
19
|
+
payload_length = header.byteslice(1, 4).unpack1("N") - 4
|
|
20
|
+
if payload_length.negative?
|
|
21
|
+
raise Error, "Invalid PostgreSQL message length"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if header.getbyte(0) == "K".ord
|
|
25
|
+
unless (8..MAX_BACKEND_KEY_BYTES).cover?(payload_length)
|
|
26
|
+
raise Error, "Invalid PostgreSQL backend key length"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
backend_key = backend.read(payload_length)
|
|
30
|
+
raise EOFError unless backend_key&.bytesize == payload_length
|
|
31
|
+
|
|
32
|
+
identity = @cancellations.register(route, backend_key)
|
|
33
|
+
client.write("K" + [12].pack("N") + identity)
|
|
34
|
+
IO.copy_stream(backend, client)
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
client.write(header)
|
|
39
|
+
copied = IO.copy_stream(backend, client, payload_length)
|
|
40
|
+
raise EOFError unless copied == payload_length
|
|
41
|
+
end
|
|
42
|
+
ensure
|
|
43
|
+
@cancellations.remove(identity) if identity
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
3
6
|
module LocalDevelopmentGateway
|
|
4
7
|
module DatabaseRouter::Wire
|
|
5
8
|
HANDSHAKE_TIMEOUT = 5
|
|
@@ -13,12 +16,70 @@ module LocalDevelopmentGateway
|
|
|
13
16
|
def read_exactly(io, length, deadline:)
|
|
14
17
|
bytes = +""
|
|
15
18
|
while bytes.bytesize < length
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
20
|
+
raise Error, "Database handshake timed out" unless remaining.positive?
|
|
21
|
+
|
|
22
|
+
chunk = io.read_nonblock(length - bytes.bytesize, exception: false)
|
|
23
|
+
case chunk
|
|
24
|
+
when :wait_readable
|
|
25
|
+
wait(io, readable: true, deadline: deadline)
|
|
26
|
+
when :wait_writable
|
|
27
|
+
wait(io, readable: false, deadline: deadline)
|
|
28
|
+
when nil
|
|
29
|
+
raise EOFError
|
|
30
|
+
else
|
|
31
|
+
bytes << chunk
|
|
32
|
+
end
|
|
18
33
|
end
|
|
19
34
|
bytes
|
|
20
35
|
end
|
|
21
36
|
|
|
37
|
+
def proxy(client, target, &response)
|
|
38
|
+
upstream =
|
|
39
|
+
Thread.new do
|
|
40
|
+
IO.copy_stream(client, target)
|
|
41
|
+
rescue IOError, EOFError, OpenSSL::SSL::SSLError, SystemCallError
|
|
42
|
+
nil
|
|
43
|
+
ensure
|
|
44
|
+
close_write(target)
|
|
45
|
+
end
|
|
46
|
+
downstream =
|
|
47
|
+
Thread.new do
|
|
48
|
+
if response
|
|
49
|
+
response.call(target, client)
|
|
50
|
+
else
|
|
51
|
+
IO.copy_stream(target, client)
|
|
52
|
+
end
|
|
53
|
+
rescue IOError, EOFError, OpenSSL::SSL::SSLError, SystemCallError
|
|
54
|
+
nil
|
|
55
|
+
ensure
|
|
56
|
+
close_connection(client)
|
|
57
|
+
close_connection(target)
|
|
58
|
+
end
|
|
59
|
+
threads = [upstream, downstream]
|
|
60
|
+
threads.each { |thread| thread.report_on_exception = false }
|
|
61
|
+
threads.each(&:join)
|
|
62
|
+
ensure
|
|
63
|
+
close_connection(client)
|
|
64
|
+
close_connection(target)
|
|
65
|
+
threads&.each(&:join)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def close_write(io)
|
|
69
|
+
io.close_write unless io.closed?
|
|
70
|
+
rescue IOError, SystemCallError
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close_connection(io)
|
|
75
|
+
socket = io.to_io
|
|
76
|
+
socket.shutdown(Socket::SHUT_RDWR) unless socket.closed?
|
|
77
|
+
rescue IOError, SystemCallError
|
|
78
|
+
nil
|
|
79
|
+
ensure
|
|
80
|
+
io.close unless io.closed?
|
|
81
|
+
end
|
|
82
|
+
|
|
22
83
|
def read_until_eof(io, max_bytes:, deadline:)
|
|
23
84
|
bytes = +""
|
|
24
85
|
loop do
|
|
@@ -7,7 +7,9 @@ module LocalDevelopmentGateway
|
|
|
7
7
|
class DatabaseRouter
|
|
8
8
|
CONNECT_TIMEOUT = 3
|
|
9
9
|
MAX_CONNECTIONS = 128
|
|
10
|
+
MAX_HANDSHAKES = 128
|
|
10
11
|
Route = Data.define(:driver, :hostname, :port, :target_address)
|
|
12
|
+
Connection = Data.define(:source, :target, :route)
|
|
11
13
|
|
|
12
14
|
def self.run
|
|
13
15
|
new.run
|
|
@@ -23,10 +25,13 @@ module LocalDevelopmentGateway
|
|
|
23
25
|
def initialize(
|
|
24
26
|
routes: DockerRoutes.new,
|
|
25
27
|
drivers: self.class.drivers,
|
|
26
|
-
servers: nil
|
|
28
|
+
servers: nil,
|
|
29
|
+
max_connections: MAX_CONNECTIONS
|
|
27
30
|
)
|
|
28
31
|
@routes = routes
|
|
29
32
|
@drivers = drivers
|
|
33
|
+
@session_slots = SizedQueue.new(max_connections)
|
|
34
|
+
max_connections.times { @session_slots << true }
|
|
30
35
|
@servers =
|
|
31
36
|
servers ||
|
|
32
37
|
drivers.to_h do |driver|
|
|
@@ -35,8 +40,8 @@ module LocalDevelopmentGateway
|
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def run
|
|
38
|
-
slots = SizedQueue.new(
|
|
39
|
-
|
|
43
|
+
slots = SizedQueue.new(MAX_HANDSHAKES)
|
|
44
|
+
MAX_HANDSHAKES.times { slots << true }
|
|
40
45
|
@drivers
|
|
41
46
|
.map do |driver|
|
|
42
47
|
Thread.new do
|
|
@@ -45,9 +50,7 @@ module LocalDevelopmentGateway
|
|
|
45
50
|
client = server.accept
|
|
46
51
|
slots.pop
|
|
47
52
|
Thread.new(client) do |connection|
|
|
48
|
-
route(connection, driver)
|
|
49
|
-
ensure
|
|
50
|
-
slots << true
|
|
53
|
+
route(connection, driver, handshake_slots: slots)
|
|
51
54
|
end
|
|
52
55
|
end
|
|
53
56
|
end
|
|
@@ -55,48 +58,46 @@ module LocalDevelopmentGateway
|
|
|
55
58
|
.each(&:join)
|
|
56
59
|
end
|
|
57
60
|
|
|
58
|
-
def route(client, driver)
|
|
59
|
-
routes =
|
|
61
|
+
def route(client, driver, handshake_slots: nil)
|
|
62
|
+
routes = -> do
|
|
63
|
+
@routes.call.select { |route| route.driver == driver.name }
|
|
64
|
+
end
|
|
65
|
+
connection =
|
|
66
|
+
driver.connect(client, routes: routes, connector: method(:connect))
|
|
67
|
+
handshake_slots&.push(true)
|
|
68
|
+
handshake_slots = nil
|
|
69
|
+
return unless connection
|
|
60
70
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
proxy(source, target)
|
|
71
|
+
session_slot = @session_slots.pop(true)
|
|
72
|
+
driver.forward(connection)
|
|
64
73
|
rescue EOFError
|
|
65
74
|
nil
|
|
75
|
+
rescue ThreadError
|
|
76
|
+
warn "Database session limit reached"
|
|
66
77
|
rescue Error => error
|
|
67
78
|
warn error.message
|
|
68
79
|
rescue StandardError => error
|
|
69
80
|
warn error.full_message
|
|
70
81
|
ensure
|
|
71
|
-
|
|
82
|
+
handshake_slots&.push(true)
|
|
83
|
+
@session_slots << true if session_slot
|
|
84
|
+
connection&.source&.close
|
|
72
85
|
client&.close
|
|
73
|
-
target&.close
|
|
86
|
+
connection&.target&.close
|
|
74
87
|
end
|
|
75
88
|
|
|
76
89
|
private
|
|
77
90
|
|
|
78
|
-
def connect(route)
|
|
91
|
+
def connect(route, deadline:)
|
|
92
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
93
|
+
raise Error, "Database handshake timed out" unless remaining.positive?
|
|
94
|
+
|
|
79
95
|
Socket.tcp(
|
|
80
96
|
route.target_address,
|
|
81
97
|
route.port,
|
|
82
|
-
connect_timeout: CONNECT_TIMEOUT
|
|
98
|
+
connect_timeout: [CONNECT_TIMEOUT, remaining].min
|
|
83
99
|
)
|
|
84
100
|
end
|
|
85
|
-
|
|
86
|
-
def proxy(client, target)
|
|
87
|
-
[[client, target], [target, client]].map do |source, destination|
|
|
88
|
-
Thread.new do
|
|
89
|
-
IO.copy_stream(source, destination)
|
|
90
|
-
rescue IOError, OpenSSL::SSL::SSLError, SystemCallError
|
|
91
|
-
nil
|
|
92
|
-
ensure
|
|
93
|
-
if destination.respond_to?(:close_write) && !destination.closed?
|
|
94
|
-
destination.close_write
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
.each(&:join)
|
|
99
|
-
end
|
|
100
101
|
end
|
|
101
102
|
end
|
|
102
103
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: local-development-gateway
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marlen Brunner
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Ruby API and CLI for starting, monitoring, and conditionally stopping
|
|
14
14
|
the shared Docker gateway.
|
|
@@ -23,6 +23,7 @@ files:
|
|
|
23
23
|
- bin/local-development-gateway
|
|
24
24
|
- config/traefik.yml
|
|
25
25
|
- docker-compose.yml
|
|
26
|
+
- docs/architecture/local-tcp-routing.md
|
|
26
27
|
- lib/local_development_gateway.rb
|
|
27
28
|
- lib/local_development_gateway/database_router.rb
|
|
28
29
|
- lib/local_development_gateway/database_router/docker_api.rb
|
|
@@ -30,6 +31,8 @@ files:
|
|
|
30
31
|
- lib/local_development_gateway/database_router/drivers/postgre_sql_certificate.rb
|
|
31
32
|
- lib/local_development_gateway/database_router/drivers/postgre_sql_driver.rb
|
|
32
33
|
- lib/local_development_gateway/database_router/drivers/sql_server_driver.rb
|
|
34
|
+
- lib/local_development_gateway/database_router/postgre_sql/cancellation_routes.rb
|
|
35
|
+
- lib/local_development_gateway/database_router/postgre_sql/session.rb
|
|
33
36
|
- lib/local_development_gateway/database_router/tds/message.rb
|
|
34
37
|
- lib/local_development_gateway/database_router/tds/tls_byte_reader.rb
|
|
35
38
|
- lib/local_development_gateway/database_router/tds/tls_client_hello.rb
|