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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d72f55ac50271b2247bd8eb5189f4b6c25c30a62a8cb0d451cecc38b050ea98e
4
- data.tar.gz: ea4e0fda3f4f88cb39666580be77960eeab97994adfd599e3b4e2db02c96f5cc
3
+ metadata.gz: 70776ca78ffae5950698180364c42f598368971d93dcec7a262088b9a599fde1
4
+ data.tar.gz: 4fe4a0b113e7d7f6efd6a30f7d9a537fddd9b215ac5c746f22c5699d7e7a557e
5
5
  SHA512:
6
- metadata.gz: 6bbf54e670547b4ff3a4b332094493ad1c825df43a50d29079b6ebfe88d85922ad6a1d73877a8de28ce22fcf2974b48db28c70250b3389d77a062a6fd14a19d9
7
- data.tar.gz: 74cfbf75708d8e46a47e19ffc4a94b732e502372edfc565702c98e7ab8177e98208e543d3be45c822a6ed76bbdd83cb56bf3b50931efb334c2d3516dff4228e7
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 can publish its internal web service through a generated hostname instead:
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://port-35053.traditional-knowledge.localhost
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
- `port-35053` is derived from Traditional Knowledge's generated `WEB_PORT` value. It identifies the project instance; the browser reaches the web service through the gateway on port `80`, not directly on port `35053`.
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
- The generated hostname is a local `*.localhost` name. It resolves to loopback and does not require a public domain, external DNS provider, or internet-facing listener.
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.0"
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. Upgrade the version constraint when a new
80
- compatible release is published. The repository's `bin/dev` is only a thin
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
- ## Use case: Traditional Knowledge beside WRAP
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
- 1. Start this gateway once.
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
- Each project receives an independent hostname, while the gateway owns the single browser port. The generated `WEB_PORT` is an identity seed in gateway mode, not the browser-facing port.
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
- Without the gateway, Traditional Knowledge publishes the same generated values directly, including `localhost:<WEB_PORT>` for the web service and `localhost:<BACKEND_PORT>` for the backend. `dev up` regenerates unavailable assignments; deleting `.env.development.local` forces a fresh assignment.
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 writes role-based generated values to its local `.env.development.local`:
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>-<backend-port>.rule=Host(`<web-hostname>`)"
124
- - "traefik.http.routers.<project>-<backend-port>.entrypoints=web"
125
- - "traefik.http.routers.<project>-<backend-port>.service=<project>-<backend-port>"
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
- Without the gateway, publish the same generated values directly:
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
- Use a unique generated `WEB_PORT` and `BACKEND_PORT` for every running project instance. Never derive a destination port from the hostname; each route must name one explicit Docker service and internal port.
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LocalDevelopmentGateway
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
@@ -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; end
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
- raise DockerError.new(command, stderr.empty? ? stdout : stderr) unless status.success?
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 = "{{.ID}}\t{{.Label \"com.docker.compose.project\"}}\t{{.Label \"com.docker.compose.service\"}}\t{{.Label \"local-gateway\"}}"
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? && gateway_container_ids(status: "running").any? { |id| healthy_container?(id) }
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", PROJECT_NAME,
94
- "--file", COMPOSE_FILE,
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
- raise Error, "Local Development Gateway did not become ready" if @clock.call >= deadline
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 = @runner.call(
113
- "network",
114
- "inspect",
115
- NETWORK_NAME,
116
- "--format",
117
- "{{index .Labels \"com.docker.compose.project\"}}\t{{index .Labels \"com.docker.compose.network\"}}",
118
- )
119
- labels.lines.any? { |line| line.strip == "#{PROJECT_NAME}\t#{NETWORK_NAME}" }
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
- "--filter", "network=#{NETWORK_NAME}",
133
- "--filter", "label=com.docker.compose.project=#{PROJECT_NAME}",
134
- "--filter", "label=com.docker.compose.service=#{SERVICE_NAME}",
135
- "--filter", "label=#{GATEWAY_LABEL}=true",
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("inspect", "--format", "{{.State.Health.Status}}", id).strip == "healthy"
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 = @runner.call(
150
- "ps",
151
- "--all",
152
- "--filter", "network=#{NETWORK_NAME}",
153
- "--format", CONTAINER_FORMAT,
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? do |container|
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
- return print_usage(0) if command.nil? || %w[help --help -h].include?(command)
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: "Leaving Local Development Gateway running because another container is attached",
239
- stopped: "Stopped Local Development Gateway",
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: local-development-gateway
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marlen Brunner