local-development-gateway 0.1.0 → 0.1.1
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 +45 -41
- data/lib/local_development_gateway/version.rb +1 -1
- data/lib/local_development_gateway.rb +65 -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: 70776ca78ffae5950698180364c42f598368971d93dcec7a262088b9a599fde1
|
|
4
|
+
data.tar.gz: 4fe4a0b113e7d7f6efd6a30f7d9a537fddd9b215ac5c746f22c5699d7e7a557e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6543f1423068cdf0ab5aca3215487ae1a343beea7a583c1031de21357e9d1e7f1690fd20cb9bb73748d15d01cd8c33197c610cc5230c5540f6cda94ca9831ba
|
|
7
|
+
data.tar.gz: 5ea9608dd49f657d3ae4de5b8da00752f787b9a08da3e83651eeb6dffb92b41005b93587f97e66e2c5fe9302c1494914b0ab9bd0a6b11bddcfd621afe60e6329
|
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,37 @@ 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
|
+
## Development checks
|
|
91
|
+
|
|
92
|
+
Install the locked development tools with `bundle install`. Run the Ruby
|
|
93
|
+
formatter check and tests with:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
bundle exec ruby "$(bundle show syntax_tree)/exe/stree" check \
|
|
97
|
+
Gemfile \
|
|
98
|
+
Rakefile \
|
|
99
|
+
lib/local_development_gateway.rb \
|
|
100
|
+
lib/local_development_gateway/version.rb \
|
|
101
|
+
bin/dev \
|
|
102
|
+
bin/local-development-gateway \
|
|
103
|
+
test/local_development_gateway_test.rb
|
|
104
|
+
rake test
|
|
105
|
+
```
|
|
84
106
|
|
|
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.
|
|
107
|
+
## Use case: Traditional Knowledge beside WRAP
|
|
90
108
|
|
|
91
|
-
|
|
109
|
+
1. Use the published gateway gem; it starts or reuses the shared gateway when Traditional Knowledge starts.
|
|
110
|
+
2. Run `bundle install` in Traditional Knowledge so the published gateway gem is available.
|
|
111
|
+
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`.
|
|
112
|
+
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.
|
|
113
|
+
5. Run `dev down` to stop the application services. It stops the shared gateway only when no other project is attached.
|
|
114
|
+
6. When WRAP adopts the same contract, it can use its own checkout-derived hostnames through this shared gateway.
|
|
92
115
|
|
|
93
|
-
|
|
116
|
+
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
117
|
|
|
95
118
|
## Contract for participating projects
|
|
96
119
|
|
|
@@ -101,15 +124,7 @@ The gateway:
|
|
|
101
124
|
- discovers only services explicitly labelled for routing;
|
|
102
125
|
- has the Docker label `local-gateway=true` so a project can detect it.
|
|
103
126
|
|
|
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`:
|
|
127
|
+
A participating project joins routed services to `local-gateway` and supplies a checkout-derived hostname plus explicit service labels:
|
|
113
128
|
|
|
114
129
|
```yaml
|
|
115
130
|
services:
|
|
@@ -120,10 +135,9 @@ services:
|
|
|
120
135
|
labels:
|
|
121
136
|
- "traefik.enable=true"
|
|
122
137
|
- "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"
|
|
138
|
+
- "traefik.http.routers.<compose-project>-web.rule=Host(`<web-hostname>`)"
|
|
139
|
+
- "traefik.http.routers.<compose-project>-web.entrypoints=web"
|
|
140
|
+
- "traefik.http.services.<compose-project>-web.loadbalancer.server.port=8080"
|
|
127
141
|
|
|
128
142
|
networks:
|
|
129
143
|
local-gateway:
|
|
@@ -131,19 +145,9 @@ networks:
|
|
|
131
145
|
name: local-gateway
|
|
132
146
|
```
|
|
133
147
|
|
|
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
|
-
```
|
|
148
|
+
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
149
|
|
|
146
|
-
|
|
150
|
+
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
151
|
|
|
148
152
|
## Authentication callbacks
|
|
149
153
|
|
|
@@ -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
|
|
@@ -90,10 +97,12 @@ module LocalDevelopmentGateway
|
|
|
90
97
|
def compose(*args, capture: true)
|
|
91
98
|
@runner.call(
|
|
92
99
|
"compose",
|
|
93
|
-
"--project-name",
|
|
94
|
-
|
|
100
|
+
"--project-name",
|
|
101
|
+
PROJECT_NAME,
|
|
102
|
+
"--file",
|
|
103
|
+
COMPOSE_FILE,
|
|
95
104
|
*args,
|
|
96
|
-
capture: capture
|
|
105
|
+
capture: capture
|
|
97
106
|
)
|
|
98
107
|
end
|
|
99
108
|
|
|
@@ -102,21 +111,26 @@ module LocalDevelopmentGateway
|
|
|
102
111
|
def wait_until_ready
|
|
103
112
|
deadline = @clock.call + @timeout
|
|
104
113
|
until ready?
|
|
105
|
-
|
|
114
|
+
if @clock.call >= deadline
|
|
115
|
+
raise Error, "Local Development Gateway did not become ready"
|
|
116
|
+
end
|
|
106
117
|
|
|
107
118
|
@sleeper.call(@poll_interval)
|
|
108
119
|
end
|
|
109
120
|
end
|
|
110
121
|
|
|
111
122
|
def network_exists?
|
|
112
|
-
labels =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
labels =
|
|
124
|
+
@runner.call(
|
|
125
|
+
"network",
|
|
126
|
+
"inspect",
|
|
127
|
+
NETWORK_NAME,
|
|
128
|
+
"--format",
|
|
129
|
+
"{{index .Labels \"com.docker.compose.project\"}}\t{{index .Labels \"com.docker.compose.network\"}}"
|
|
130
|
+
)
|
|
131
|
+
labels.lines.any? do |line|
|
|
132
|
+
line.strip == "#{PROJECT_NAME}\t#{NETWORK_NAME}"
|
|
133
|
+
end
|
|
120
134
|
rescue DockerError
|
|
121
135
|
false
|
|
122
136
|
end
|
|
@@ -128,30 +142,44 @@ module LocalDevelopmentGateway
|
|
|
128
142
|
def gateway_container_ids(status: nil)
|
|
129
143
|
args = ["ps"]
|
|
130
144
|
args << "--all" unless status
|
|
131
|
-
args.concat(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
args.concat(
|
|
146
|
+
[
|
|
147
|
+
"--filter",
|
|
148
|
+
"network=#{NETWORK_NAME}",
|
|
149
|
+
"--filter",
|
|
150
|
+
"label=com.docker.compose.project=#{PROJECT_NAME}",
|
|
151
|
+
"--filter",
|
|
152
|
+
"label=com.docker.compose.service=#{SERVICE_NAME}",
|
|
153
|
+
"--filter",
|
|
154
|
+
"label=#{GATEWAY_LABEL}=true"
|
|
155
|
+
]
|
|
156
|
+
)
|
|
137
157
|
args.push("--filter", "status=#{status}") if status
|
|
138
158
|
output = @runner.call(*args, "--quiet")
|
|
139
159
|
output.lines.map(&:strip).reject(&:empty?)
|
|
140
160
|
end
|
|
141
161
|
|
|
142
162
|
def healthy_container?(id)
|
|
143
|
-
@runner.call(
|
|
163
|
+
@runner.call(
|
|
164
|
+
"inspect",
|
|
165
|
+
"--format",
|
|
166
|
+
"{{.State.Health.Status}}",
|
|
167
|
+
id
|
|
168
|
+
).strip == "healthy"
|
|
144
169
|
rescue DockerError
|
|
145
170
|
false
|
|
146
171
|
end
|
|
147
172
|
|
|
148
173
|
def attached_containers
|
|
149
|
-
output =
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
174
|
+
output =
|
|
175
|
+
@runner.call(
|
|
176
|
+
"ps",
|
|
177
|
+
"--all",
|
|
178
|
+
"--filter",
|
|
179
|
+
"network=#{NETWORK_NAME}",
|
|
180
|
+
"--format",
|
|
181
|
+
CONTAINER_FORMAT
|
|
182
|
+
)
|
|
155
183
|
output.lines.filter_map do |line|
|
|
156
184
|
id, project, service, label = line.strip.split("\t", -1)
|
|
157
185
|
next if id.nil? || id.empty?
|
|
@@ -161,15 +189,12 @@ module LocalDevelopmentGateway
|
|
|
161
189
|
end
|
|
162
190
|
|
|
163
191
|
def non_gateway_containers_attached?
|
|
164
|
-
attached_containers.any?
|
|
165
|
-
!gateway_container?(container)
|
|
166
|
-
end
|
|
192
|
+
attached_containers.any? { |container| !gateway_container?(container) }
|
|
167
193
|
end
|
|
168
194
|
|
|
169
195
|
def gateway_container?(container)
|
|
170
196
|
container[:project] == PROJECT_NAME &&
|
|
171
|
-
container[:service] == SERVICE_NAME &&
|
|
172
|
-
container[:label] == "true"
|
|
197
|
+
container[:service] == SERVICE_NAME && container[:label] == "true"
|
|
173
198
|
end
|
|
174
199
|
end
|
|
175
200
|
|
|
@@ -200,7 +225,9 @@ module LocalDevelopmentGateway
|
|
|
200
225
|
|
|
201
226
|
def run
|
|
202
227
|
command = @argv.shift
|
|
203
|
-
|
|
228
|
+
if command.nil? || %w[help --help -h].include?(command)
|
|
229
|
+
return print_usage(0)
|
|
230
|
+
end
|
|
204
231
|
|
|
205
232
|
case command
|
|
206
233
|
when "up", "ensure"
|
|
@@ -235,8 +262,9 @@ module LocalDevelopmentGateway
|
|
|
235
262
|
def report_stop(result)
|
|
236
263
|
messages = {
|
|
237
264
|
not_running: "Local Development Gateway is not running",
|
|
238
|
-
in_use:
|
|
239
|
-
|
|
265
|
+
in_use:
|
|
266
|
+
"Leaving Local Development Gateway running because another container is attached",
|
|
267
|
+
stopped: "Stopped Local Development Gateway"
|
|
240
268
|
}
|
|
241
269
|
@output.puts messages.fetch(result)
|
|
242
270
|
end
|