local-development-gateway 0.1.0 → 0.1.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 +64 -41
- data/lib/local_development_gateway/version.rb +1 -1
- data/lib/local_development_gateway.rb +83 -37
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f3e2ce5cd6347553a1923006ebc26ff3ae5178e67515d6741014dc229ca115d
|
|
4
|
+
data.tar.gz: 3059913ab2b89ae89442c80112fc49701d31690b46c5ca62bf981042c765700f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6190a1c1563636f2a0a6ca2a08c8acb10c0a674e24551a01cac437a02c70c766ff7f6ec81251d10c086a08f414a43c445cff09095ff5f3a57cadab3580b880d3
|
|
7
|
+
data.tar.gz: 6f04fd1c800964cb0d44e65dc792116ece3fd622cfee945f8a2d49bf7e23165bff95050422e826dad0a4f1e4497d90797c417e87f6569da639c9833c7dd98ca1
|
data/README.md
CHANGED
|
@@ -8,10 +8,11 @@ It lets several local projects use stable hostnames instead of competing for bro
|
|
|
8
8
|
|
|
9
9
|
Without a gateway, two projects commonly both try to publish a web service on port `8080` and a backend service on port `3000`. Only one can start.
|
|
10
10
|
|
|
11
|
-
With this gateway running, a project
|
|
11
|
+
With this gateway running, a project routes its internal web service through a
|
|
12
|
+
checkout-derived hostname instead:
|
|
12
13
|
|
|
13
14
|
```text
|
|
14
|
-
http://
|
|
15
|
+
http://traditional-knowledge.localhost
|
|
15
16
|
|
|
|
16
17
|
v
|
|
17
18
|
Local Development Gateway on 127.0.0.1:80
|
|
@@ -20,9 +21,13 @@ Local Development Gateway on 127.0.0.1:80
|
|
|
20
21
|
Traditional Knowledge web container on internal port 8080
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
The checkout folder identifies the project. A separately named worktree such as
|
|
25
|
+
`issue-123` uses `http://issue-123.traditional-knowledge.localhost`; the browser
|
|
26
|
+
reaches the web service through the gateway on port `80`, not a published
|
|
27
|
+
application port.
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
These local `*.localhost` hostnames resolve to loopback and do not require a
|
|
30
|
+
public domain, external DNS provider, or internet-facing listener.
|
|
26
31
|
|
|
27
32
|
## Start once per day
|
|
28
33
|
|
|
@@ -61,9 +66,11 @@ Consuming projects do not need a gateway checkout. Add the released gem to
|
|
|
61
66
|
their bundle and pin the compatible minor version:
|
|
62
67
|
|
|
63
68
|
```ruby
|
|
64
|
-
gem "local-development-gateway", "~> 0.1
|
|
69
|
+
gem "local-development-gateway", "~> 0.1"
|
|
65
70
|
```
|
|
66
71
|
|
|
72
|
+
Published gem: [`local-development-gateway`](https://rubygems.org/gems/local-development-gateway)
|
|
73
|
+
|
|
67
74
|
The gem packages the gateway Compose file and pinned Traefik configuration.
|
|
68
75
|
Its executable provides the same lifecycle commands:
|
|
69
76
|
|
|
@@ -76,21 +83,56 @@ bundle exec local-development-gateway down
|
|
|
76
83
|
|
|
77
84
|
`local-development-gateway` uses the installed asset path, the
|
|
78
85
|
`local-gateway` Compose project and network labels, and the pinned minimum Ruby
|
|
79
|
-
version declared by the gem.
|
|
80
|
-
|
|
81
|
-
wrapper around this executable API.
|
|
86
|
+
version declared by the gem. The `~> 0.1` constraint follows the consuming
|
|
87
|
+
project's published Gemfile.
|
|
88
|
+
The repository's `bin/dev` is only a thin wrapper around this executable API.
|
|
82
89
|
|
|
83
|
-
|
|
90
|
+
### Ruby API lifecycle
|
|
91
|
+
|
|
92
|
+
Wrap a command that needs the gateway with the block-oriented helper:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
require "local_development_gateway"
|
|
96
|
+
|
|
97
|
+
result = LocalDevelopmentGateway.with_running do
|
|
98
|
+
run_development_command
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The helper starts or reuses the gateway before yielding, returns the block's
|
|
103
|
+
result, and conditionally stops an unused gateway afterward. Cleanup also runs
|
|
104
|
+
when the block raises without replacing the original exception. For a shutdown
|
|
105
|
+
command that must not start a missing gateway, pass
|
|
106
|
+
`ensure_running: false`; the block still runs and unused-gateway cleanup is
|
|
107
|
+
attempted.
|
|
108
|
+
|
|
109
|
+
## Development checks
|
|
110
|
+
|
|
111
|
+
Install the locked development tools with `bundle install`. Run the Ruby
|
|
112
|
+
formatter check and tests with:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
bundle exec ruby "$(bundle show syntax_tree)/exe/stree" check \
|
|
116
|
+
Gemfile \
|
|
117
|
+
Rakefile \
|
|
118
|
+
lib/local_development_gateway.rb \
|
|
119
|
+
lib/local_development_gateway/version.rb \
|
|
120
|
+
bin/dev \
|
|
121
|
+
bin/local-development-gateway \
|
|
122
|
+
test/local_development_gateway_test.rb
|
|
123
|
+
rake test
|
|
124
|
+
```
|
|
84
125
|
|
|
85
|
-
|
|
86
|
-
2. Run `dev up` in Traditional Knowledge. It writes role-based generated values to `.env.development.local`, including `WEB_PORT`, `BACKEND_PORT`, and `WEB_HOSTNAME=port-<WEB_PORT>.traditional-knowledge.localhost`.
|
|
87
|
-
3. With the gateway running, Traditional Knowledge's web container joins `local-gateway`, its direct web publishing is removed, and the generated hostname routes through the gateway on `127.0.0.1:80`.
|
|
88
|
-
4. Open `WEB_HOSTNAME` from `.env.development.local`.
|
|
89
|
-
5. When WRAP adopts the same contract, start it normally. It can then use its own generated route labels and hostname through this same gateway.
|
|
126
|
+
## Use case: Traditional Knowledge beside WRAP
|
|
90
127
|
|
|
91
|
-
|
|
128
|
+
1. Use the published gateway gem; it starts or reuses the shared gateway when Traditional Knowledge starts.
|
|
129
|
+
2. Run `bundle install` in Traditional Knowledge so the published gateway gem is available.
|
|
130
|
+
3. Run `dev up --watch` in Traditional Knowledge. The gateway is the browser entrypoint; the base checkout uses `http://traditional-knowledge.localhost`, with the API at `http://api.traditional-knowledge.localhost` and MailDev at `http://mail.traditional-knowledge.localhost`.
|
|
131
|
+
4. A separately named checkout or worktree derives its own hostname, such as `http://issue-123.traditional-knowledge.localhost`. The API and MailDev hosts use the corresponding `api.` and `mail.` prefixes.
|
|
132
|
+
5. Run `dev down` to stop the application services. It stops the shared gateway only when no other project is attached.
|
|
133
|
+
6. When WRAP adopts the same contract, it can use its own checkout-derived hostnames through this shared gateway.
|
|
92
134
|
|
|
93
|
-
|
|
135
|
+
The gateway owns the single browser port on `127.0.0.1:80`. Participating projects keep service ports internal to Docker and route browser traffic with explicit Traefik labels.
|
|
94
136
|
|
|
95
137
|
## Contract for participating projects
|
|
96
138
|
|
|
@@ -101,15 +143,7 @@ The gateway:
|
|
|
101
143
|
- discovers only services explicitly labelled for routing;
|
|
102
144
|
- has the Docker label `local-gateway=true` so a project can detect it.
|
|
103
145
|
|
|
104
|
-
A participating project
|
|
105
|
-
|
|
106
|
-
```dotenv
|
|
107
|
-
WEB_PORT=35053
|
|
108
|
-
BACKEND_PORT=38261
|
|
109
|
-
WEB_HOSTNAME=port-35053.traditional-knowledge.localhost
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
With the gateway available, its web service joins `local-gateway` and supplies explicit route labels. Use a project-prefixed identifier containing the published `BACKEND_PORT` for the internal router/service name; do not generate a separate `STACK_IDENTIFIER`:
|
|
146
|
+
A participating project joins routed services to `local-gateway` and supplies a checkout-derived hostname plus explicit service labels:
|
|
113
147
|
|
|
114
148
|
```yaml
|
|
115
149
|
services:
|
|
@@ -120,10 +154,9 @@ services:
|
|
|
120
154
|
labels:
|
|
121
155
|
- "traefik.enable=true"
|
|
122
156
|
- "traefik.docker.network=local-gateway"
|
|
123
|
-
- "traefik.http.routers.<project
|
|
124
|
-
- "traefik.http.routers.<project
|
|
125
|
-
- "traefik.http.
|
|
126
|
-
- "traefik.http.services.<project>-<backend-port>.loadbalancer.server.port=8080"
|
|
157
|
+
- "traefik.http.routers.<compose-project>-web.rule=Host(`<web-hostname>`)"
|
|
158
|
+
- "traefik.http.routers.<compose-project>-web.entrypoints=web"
|
|
159
|
+
- "traefik.http.services.<compose-project>-web.loadbalancer.server.port=8080"
|
|
127
160
|
|
|
128
161
|
networks:
|
|
129
162
|
local-gateway:
|
|
@@ -131,19 +164,9 @@ networks:
|
|
|
131
164
|
name: local-gateway
|
|
132
165
|
```
|
|
133
166
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
```yaml
|
|
137
|
-
services:
|
|
138
|
-
web:
|
|
139
|
-
ports:
|
|
140
|
-
- "127.0.0.1:${WEB_PORT}:8080"
|
|
141
|
-
backend:
|
|
142
|
-
ports:
|
|
143
|
-
- "127.0.0.1:${BACKEND_PORT}:3000"
|
|
144
|
-
```
|
|
167
|
+
Use a project- and service-prefixed router/service identifier. Never derive a destination port from the hostname; each routed service must name one explicit internal destination port.
|
|
145
168
|
|
|
146
|
-
|
|
169
|
+
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.
|
|
147
170
|
|
|
148
171
|
## Authentication callbacks
|
|
149
172
|
|
|
@@ -12,7 +12,8 @@ module LocalDevelopmentGateway
|
|
|
12
12
|
COMPOSE_FILE = File.join(ASSET_ROOT, "docker-compose.yml")
|
|
13
13
|
TRAEFIK_CONFIG_FILE = File.join(ASSET_ROOT, "config", "traefik.yml")
|
|
14
14
|
|
|
15
|
-
class Error < StandardError
|
|
15
|
+
class Error < StandardError
|
|
16
|
+
end
|
|
16
17
|
|
|
17
18
|
class DockerError < Error
|
|
18
19
|
attr_reader :command, :output
|
|
@@ -29,7 +30,9 @@ module LocalDevelopmentGateway
|
|
|
29
30
|
command = ["docker", *args]
|
|
30
31
|
if capture
|
|
31
32
|
stdout, stderr, status = Open3.capture3(*command)
|
|
32
|
-
|
|
33
|
+
unless status.success?
|
|
34
|
+
raise DockerError.new(command, stderr.empty? ? stdout : stderr)
|
|
35
|
+
end
|
|
33
36
|
|
|
34
37
|
stdout
|
|
35
38
|
elsif system(*command)
|
|
@@ -41,7 +44,8 @@ module LocalDevelopmentGateway
|
|
|
41
44
|
end
|
|
42
45
|
|
|
43
46
|
class Client
|
|
44
|
-
CONTAINER_FORMAT =
|
|
47
|
+
CONTAINER_FORMAT =
|
|
48
|
+
"{{.ID}}\t{{.Label \"com.docker.compose.project\"}}\t{{.Label \"com.docker.compose.service\"}}\t{{.Label \"local-gateway\"}}"
|
|
45
49
|
|
|
46
50
|
def initialize(
|
|
47
51
|
runner: Docker.new,
|
|
@@ -68,7 +72,10 @@ module LocalDevelopmentGateway
|
|
|
68
72
|
alias start ensure_running
|
|
69
73
|
|
|
70
74
|
def ready?
|
|
71
|
-
network_exists? &&
|
|
75
|
+
network_exists? &&
|
|
76
|
+
gateway_container_ids(status: "running").any? do |id|
|
|
77
|
+
healthy_container?(id)
|
|
78
|
+
end
|
|
72
79
|
end
|
|
73
80
|
|
|
74
81
|
def status
|
|
@@ -87,13 +94,29 @@ module LocalDevelopmentGateway
|
|
|
87
94
|
:stopped
|
|
88
95
|
end
|
|
89
96
|
|
|
97
|
+
def with_running(ensure_running: true)
|
|
98
|
+
begin
|
|
99
|
+
self.ensure_running if ensure_running
|
|
100
|
+
yield
|
|
101
|
+
ensure
|
|
102
|
+
block_error = $!
|
|
103
|
+
begin
|
|
104
|
+
stop_if_unused
|
|
105
|
+
rescue StandardError => cleanup_error
|
|
106
|
+
raise cleanup_error unless block_error
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
90
111
|
def compose(*args, capture: true)
|
|
91
112
|
@runner.call(
|
|
92
113
|
"compose",
|
|
93
|
-
"--project-name",
|
|
94
|
-
|
|
114
|
+
"--project-name",
|
|
115
|
+
PROJECT_NAME,
|
|
116
|
+
"--file",
|
|
117
|
+
COMPOSE_FILE,
|
|
95
118
|
*args,
|
|
96
|
-
capture: capture
|
|
119
|
+
capture: capture
|
|
97
120
|
)
|
|
98
121
|
end
|
|
99
122
|
|
|
@@ -102,21 +125,26 @@ module LocalDevelopmentGateway
|
|
|
102
125
|
def wait_until_ready
|
|
103
126
|
deadline = @clock.call + @timeout
|
|
104
127
|
until ready?
|
|
105
|
-
|
|
128
|
+
if @clock.call >= deadline
|
|
129
|
+
raise Error, "Local Development Gateway did not become ready"
|
|
130
|
+
end
|
|
106
131
|
|
|
107
132
|
@sleeper.call(@poll_interval)
|
|
108
133
|
end
|
|
109
134
|
end
|
|
110
135
|
|
|
111
136
|
def network_exists?
|
|
112
|
-
labels =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
labels =
|
|
138
|
+
@runner.call(
|
|
139
|
+
"network",
|
|
140
|
+
"inspect",
|
|
141
|
+
NETWORK_NAME,
|
|
142
|
+
"--format",
|
|
143
|
+
"{{index .Labels \"com.docker.compose.project\"}}\t{{index .Labels \"com.docker.compose.network\"}}"
|
|
144
|
+
)
|
|
145
|
+
labels.lines.any? do |line|
|
|
146
|
+
line.strip == "#{PROJECT_NAME}\t#{NETWORK_NAME}"
|
|
147
|
+
end
|
|
120
148
|
rescue DockerError
|
|
121
149
|
false
|
|
122
150
|
end
|
|
@@ -128,30 +156,44 @@ module LocalDevelopmentGateway
|
|
|
128
156
|
def gateway_container_ids(status: nil)
|
|
129
157
|
args = ["ps"]
|
|
130
158
|
args << "--all" unless status
|
|
131
|
-
args.concat(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
159
|
+
args.concat(
|
|
160
|
+
[
|
|
161
|
+
"--filter",
|
|
162
|
+
"network=#{NETWORK_NAME}",
|
|
163
|
+
"--filter",
|
|
164
|
+
"label=com.docker.compose.project=#{PROJECT_NAME}",
|
|
165
|
+
"--filter",
|
|
166
|
+
"label=com.docker.compose.service=#{SERVICE_NAME}",
|
|
167
|
+
"--filter",
|
|
168
|
+
"label=#{GATEWAY_LABEL}=true"
|
|
169
|
+
]
|
|
170
|
+
)
|
|
137
171
|
args.push("--filter", "status=#{status}") if status
|
|
138
172
|
output = @runner.call(*args, "--quiet")
|
|
139
173
|
output.lines.map(&:strip).reject(&:empty?)
|
|
140
174
|
end
|
|
141
175
|
|
|
142
176
|
def healthy_container?(id)
|
|
143
|
-
@runner.call(
|
|
177
|
+
@runner.call(
|
|
178
|
+
"inspect",
|
|
179
|
+
"--format",
|
|
180
|
+
"{{.State.Health.Status}}",
|
|
181
|
+
id
|
|
182
|
+
).strip == "healthy"
|
|
144
183
|
rescue DockerError
|
|
145
184
|
false
|
|
146
185
|
end
|
|
147
186
|
|
|
148
187
|
def attached_containers
|
|
149
|
-
output =
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
188
|
+
output =
|
|
189
|
+
@runner.call(
|
|
190
|
+
"ps",
|
|
191
|
+
"--all",
|
|
192
|
+
"--filter",
|
|
193
|
+
"network=#{NETWORK_NAME}",
|
|
194
|
+
"--format",
|
|
195
|
+
CONTAINER_FORMAT
|
|
196
|
+
)
|
|
155
197
|
output.lines.filter_map do |line|
|
|
156
198
|
id, project, service, label = line.strip.split("\t", -1)
|
|
157
199
|
next if id.nil? || id.empty?
|
|
@@ -161,15 +203,12 @@ module LocalDevelopmentGateway
|
|
|
161
203
|
end
|
|
162
204
|
|
|
163
205
|
def non_gateway_containers_attached?
|
|
164
|
-
attached_containers.any?
|
|
165
|
-
!gateway_container?(container)
|
|
166
|
-
end
|
|
206
|
+
attached_containers.any? { |container| !gateway_container?(container) }
|
|
167
207
|
end
|
|
168
208
|
|
|
169
209
|
def gateway_container?(container)
|
|
170
210
|
container[:project] == PROJECT_NAME &&
|
|
171
|
-
container[:service] == SERVICE_NAME &&
|
|
172
|
-
container[:label] == "true"
|
|
211
|
+
container[:service] == SERVICE_NAME && container[:label] == "true"
|
|
173
212
|
end
|
|
174
213
|
end
|
|
175
214
|
|
|
@@ -200,7 +239,9 @@ module LocalDevelopmentGateway
|
|
|
200
239
|
|
|
201
240
|
def run
|
|
202
241
|
command = @argv.shift
|
|
203
|
-
|
|
242
|
+
if command.nil? || %w[help --help -h].include?(command)
|
|
243
|
+
return print_usage(0)
|
|
244
|
+
end
|
|
204
245
|
|
|
205
246
|
case command
|
|
206
247
|
when "up", "ensure"
|
|
@@ -235,8 +276,9 @@ module LocalDevelopmentGateway
|
|
|
235
276
|
def report_stop(result)
|
|
236
277
|
messages = {
|
|
237
278
|
not_running: "Local Development Gateway is not running",
|
|
238
|
-
in_use:
|
|
239
|
-
|
|
279
|
+
in_use:
|
|
280
|
+
"Leaving Local Development Gateway running because another container is attached",
|
|
281
|
+
stopped: "Stopped Local Development Gateway"
|
|
240
282
|
}
|
|
241
283
|
@output.puts messages.fetch(result)
|
|
242
284
|
end
|
|
@@ -247,6 +289,10 @@ module LocalDevelopmentGateway
|
|
|
247
289
|
Client.new.ensure_running(*args)
|
|
248
290
|
end
|
|
249
291
|
|
|
292
|
+
def with_running(ensure_running: true, &block)
|
|
293
|
+
Client.new.with_running(ensure_running: ensure_running, &block)
|
|
294
|
+
end
|
|
295
|
+
|
|
250
296
|
def start(*args)
|
|
251
297
|
ensure_running(*args)
|
|
252
298
|
end
|