gdkbox 0.1.5 → 0.1.10
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 +7 -3
- data/lib/gdkbox/box.rb +71 -2
- data/lib/gdkbox/cli.rb +134 -1
- data/lib/gdkbox/completion.rb +2 -1
- data/lib/gdkbox/config.rb +20 -0
- data/lib/gdkbox/docker.rb +6 -2
- data/lib/gdkbox/hosts_file.rb +84 -0
- data/lib/gdkbox/ports.rb +24 -0
- data/lib/gdkbox/provisioner.rb +41 -0
- data/lib/gdkbox/ssh_config.rb +5 -0
- data/lib/gdkbox/version.rb +1 -1
- data/lib/gdkbox.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 75c5c8ed1f89cb877856f68e45c39f4b5460131ea5a312e5af10e7cb7de2688b
|
|
4
|
+
data.tar.gz: a0eb3697bb1b630665be5799d33ffb8bb4aec0999858e075861e3469bd312431
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 344bba38a105dcf4bf899893d18eb7ef9f82ef03ef6d2229af810346e27f18b24c46e09c02c7f83e406255f79e8d033ff37c159ced868aad284e540c6cf7948c
|
|
7
|
+
data.tar.gz: ce33e8b7bd1aa403ea5fe77127b7c46fd1c4eb7079736689cc6447b6b0649bd270b880db82d31a33bb0d090f818366b1e2de847e6b3d70f918dbae257200014a
|
data/README.md
CHANGED
|
@@ -26,7 +26,7 @@ gdkbox ls # list your boxes and their status
|
|
|
26
26
|
┌──────────────┼─────────────────────────────────────────────┐
|
|
27
27
|
│ ▼ │
|
|
28
28
|
│ 1. docker pull <official GDK image> │
|
|
29
|
-
│ 2. docker run -p 127.0.0.1:2222:22 -p 127.0.0.1:3000:3000
|
|
29
|
+
│ 2. docker run --hostname gdk.local -p 127.0.0.1:2222:22 -p 127.0.0.1:3000:3000
|
|
30
30
|
│ 3. provision install + start sshd, authorize gdkbox key │
|
|
31
31
|
│ 4. provision npm install -g @anthropic-ai/claude-code │
|
|
32
32
|
│ 5. register Host gdkbox-demo -> ~/.gdkbox/ssh_config │
|
|
@@ -89,6 +89,9 @@ Or run it straight from the checkout without installing:
|
|
|
89
89
|
| `gdkbox ssh NAME` | Open an interactive SSH session into the box. |
|
|
90
90
|
| `gdkbox claude NAME` | Install Claude Code inside the box. |
|
|
91
91
|
| `gdkbox set-key NAME` | Seed/rotate the Anthropic API key in the box for unattended dispatch. |
|
|
92
|
+
| `gdkbox set-host` | Add the `gdk.local` entry to your `/etc/hosts` so box web UIs resolve (`--remove` to undo). Also runs during `up`. |
|
|
93
|
+
| `gdkbox set-git NAME` | Seed your git identity (`user.name`/`user.email`) into the box so `git commit` works. Also runs during `up`. |
|
|
94
|
+
| `gdkbox set-remote NAME [REMOTE]` | Point the box's GitLab checkout at a different remote (URL or `namespace/project`, e.g. `gitlab-org/gitlab`); default from config.yml. |
|
|
92
95
|
| `gdkbox start NAME` | Start a stopped box (and re-enable SSH). |
|
|
93
96
|
| `gdkbox stop NAME` | Stop a running box. |
|
|
94
97
|
| `gdkbox rm NAME` | Remove a box: container, metadata, and SSH entry. |
|
|
@@ -104,11 +107,12 @@ Or run it straight from the checkout without installing:
|
|
|
104
107
|
| Option | Default | Description |
|
|
105
108
|
| --- | --- | --- |
|
|
106
109
|
| `--image` | official GDK image | Override the container image. |
|
|
107
|
-
| `--ssh-port` | next free from 2222 | Host port to publish SSH on. |
|
|
108
|
-
| `--web-port` | next free from 3000 | Host port to publish the GDK web UI on. |
|
|
110
|
+
| `--ssh-port` | next free from 2222 | Host port to publish SSH on. Auto-allocation skips ports in use on the host (e.g. a local GDK). |
|
|
111
|
+
| `--web-port` | next free from 3000 | Host port to publish the GDK web UI on. Auto-allocation skips ports in use on the host. |
|
|
109
112
|
| `--harness ID` | `claude` (or config.yml) | Agent harness to install: claude, codex, opencode, pi. |
|
|
110
113
|
| `--no-agent` | (agent installed) | Skip installing the agent harness. |
|
|
111
114
|
| `--skill NAME [NAME...]` | (none) | Seed skill(s) into the box at creation for dispatched agents. |
|
|
115
|
+
| `--gitlab-remote REMOTE` | config.yml, else image default | Point the GitLab checkout at this remote (URL or `namespace/project`); the image ships pointing at the community mirror. |
|
|
112
116
|
|
|
113
117
|
## Typical workflow
|
|
114
118
|
|
data/lib/gdkbox/box.rb
CHANGED
|
@@ -104,6 +104,10 @@ module GDKBox
|
|
|
104
104
|
@docker.run_container(
|
|
105
105
|
name: cname,
|
|
106
106
|
image: image,
|
|
107
|
+
# GDK's services resolve and redirect to this hostname; without it
|
|
108
|
+
# set inside the container, workhorse and the http-router crash-loop
|
|
109
|
+
# on DNS timeouts.
|
|
110
|
+
hostname: Config::GDK_HOSTNAME,
|
|
107
111
|
publish: [
|
|
108
112
|
"127.0.0.1:#{ssh_port}:#{Config::SSH_CONTAINER_PORT}",
|
|
109
113
|
"127.0.0.1:#{web_port}:#{Config::GDK_WEB_CONTAINER_PORT}"
|
|
@@ -133,6 +137,15 @@ module GDKBox
|
|
|
133
137
|
)
|
|
134
138
|
@store.save(@data)
|
|
135
139
|
|
|
140
|
+
# Best-effort: a host without a git identity (or a transient exec
|
|
141
|
+
# failure) should not abort the box; `gdkbox set-git` can seed it later.
|
|
142
|
+
begin
|
|
143
|
+
git_name, git_email = host_git_identity
|
|
144
|
+
provisioner.setup_git_identity(cname, name: git_name, email: git_email) if git_name || git_email
|
|
145
|
+
rescue StandardError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
136
149
|
provisioner.setup_agent(cname, harness) if install_agent
|
|
137
150
|
@data["agent_installed"] = install_agent
|
|
138
151
|
|
|
@@ -162,6 +175,41 @@ module GDKBox
|
|
|
162
175
|
@store.delete(name)
|
|
163
176
|
end
|
|
164
177
|
|
|
178
|
+
# The full git URL for a remote given as a URL or a "namespace/project"
|
|
179
|
+
# shorthand (expanded against gitlab.com over SSH, so pushes ride the
|
|
180
|
+
# forwarded agent).
|
|
181
|
+
def self.expand_remote(token)
|
|
182
|
+
return token if token.include?("://") || token.include?(":")
|
|
183
|
+
|
|
184
|
+
"git@gitlab.com:#{token}.git"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Point the box's GitLab checkout at a different remote (URL or
|
|
188
|
+
# "namespace/project"). Returns the URL that origin now uses.
|
|
189
|
+
def set_gitlab_remote!(remote)
|
|
190
|
+
url = self.class.expand_remote(remote)
|
|
191
|
+
Provisioner.new(docker: @docker, config: @config)
|
|
192
|
+
.setup_gitlab_remote(container_name, url)
|
|
193
|
+
url
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Seed a git identity into the box so `git commit` works there. Falls back
|
|
197
|
+
# to the host's `git config` when name/email are not given; raises when
|
|
198
|
+
# neither source has anything to seed. Returns the [name, email] seeded.
|
|
199
|
+
def set_git_identity!(git_name: nil, git_email: nil)
|
|
200
|
+
host_name, host_email = host_git_identity
|
|
201
|
+
git_name ||= host_name
|
|
202
|
+
git_email ||= host_email
|
|
203
|
+
if git_name.nil? && git_email.nil?
|
|
204
|
+
raise Error, "No git identity found. Pass --name/--email or set " \
|
|
205
|
+
"`git config --global user.name/user.email` on the host."
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
Provisioner.new(docker: @docker, config: @config)
|
|
209
|
+
.setup_git_identity(container_name, name: git_name, email: git_email)
|
|
210
|
+
[git_name, git_email]
|
|
211
|
+
end
|
|
212
|
+
|
|
165
213
|
# (Re)install this box's agent harness inside the container.
|
|
166
214
|
def install_agent!
|
|
167
215
|
Provisioner.new(docker: @docker, config: @config).setup_agent(container_name, harness)
|
|
@@ -263,11 +311,28 @@ module GDKBox
|
|
|
263
311
|
|
|
264
312
|
private
|
|
265
313
|
|
|
314
|
+
# The host's git identity as [name, email], each nil when unset.
|
|
315
|
+
def host_git_identity
|
|
316
|
+
read = lambda do |key|
|
|
317
|
+
value = @shell.run("git", "config", "--get", key).stdout.strip
|
|
318
|
+
value.empty? ? nil : value
|
|
319
|
+
end
|
|
320
|
+
[read.call("user.name"), read.call("user.email")]
|
|
321
|
+
end
|
|
322
|
+
|
|
266
323
|
# Choose free host ports and persist a preliminary record claiming them,
|
|
267
324
|
# all while holding an exclusive lock so concurrent `create!` calls in
|
|
268
325
|
# separate processes serialize and cannot pick the same ports. Returns the
|
|
269
|
-
# chosen [ssh_port, web_port]. Caller-supplied ports are honored
|
|
326
|
+
# chosen [ssh_port, web_port]. Caller-supplied ports are honored, but fail
|
|
327
|
+
# fast with a clear message when something already holds them — better
|
|
328
|
+
# than the cryptic bind error `docker run` would produce later.
|
|
270
329
|
def reserve_ports!(cname, ssh_port, web_port)
|
|
330
|
+
{ "--ssh-port" => ssh_port, "--web-port" => web_port }.each do |flag, port|
|
|
331
|
+
if port && Ports.bound?(port)
|
|
332
|
+
raise Error, "Port #{port} (#{flag}) is already in use on 127.0.0.1."
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
271
336
|
@config.ensure_dirs!
|
|
272
337
|
File.open(@config.lock_path, File::RDWR | File::CREAT, 0o644) do |lock|
|
|
273
338
|
lock.flock(File::LOCK_EX)
|
|
@@ -286,10 +351,14 @@ module GDKBox
|
|
|
286
351
|
end
|
|
287
352
|
end
|
|
288
353
|
|
|
354
|
+
# The next port at or above base that is neither recorded by gdkbox nor
|
|
355
|
+
# actually bound on the host. The store only covers gdkbox's own boxes;
|
|
356
|
+
# a locally running GDK (typically on 2222/3000) is invisible to it, so
|
|
357
|
+
# each candidate is also probed with a real bind.
|
|
289
358
|
def next_port(base, exclude: [])
|
|
290
359
|
used = @store.used_ports + exclude
|
|
291
360
|
port = base
|
|
292
|
-
port += 1 while used.include?(port)
|
|
361
|
+
port += 1 while used.include?(port) || Ports.bound?(port)
|
|
293
362
|
port
|
|
294
363
|
end
|
|
295
364
|
end
|
data/lib/gdkbox/cli.rb
CHANGED
|
@@ -37,6 +37,9 @@ module GDKBox
|
|
|
37
37
|
desc: "Skill name(s) or path(s) to install into the box for dispatched agents"
|
|
38
38
|
option :default_skills, type: :boolean, default: true,
|
|
39
39
|
desc: "Also install the default skills from ~/.gdkbox/config.yml"
|
|
40
|
+
option :gitlab_remote, type: :string,
|
|
41
|
+
desc: "Point the GitLab checkout at this remote (URL or namespace/project; " \
|
|
42
|
+
"default from config.yml, else the image's community mirror)"
|
|
40
43
|
def up(name)
|
|
41
44
|
ensure_docker!
|
|
42
45
|
box = build_box(name)
|
|
@@ -55,12 +58,15 @@ module GDKBox
|
|
|
55
58
|
)
|
|
56
59
|
rewrite_ssh_config
|
|
57
60
|
seed_skills(box)
|
|
61
|
+
repoint_gitlab_remote(box)
|
|
58
62
|
|
|
59
63
|
if options[:json]
|
|
60
64
|
puts JSON.generate(box.summary)
|
|
61
65
|
return
|
|
62
66
|
end
|
|
63
67
|
|
|
68
|
+
setup_host
|
|
69
|
+
|
|
64
70
|
say "\nBox '#{name}' is up.", :green
|
|
65
71
|
print_connection_details(box)
|
|
66
72
|
unless api_key
|
|
@@ -234,6 +240,94 @@ module GDKBox
|
|
|
234
240
|
end
|
|
235
241
|
map "set-key" => :set_key
|
|
236
242
|
|
|
243
|
+
desc "set-git NAME", "Seed your git identity (user.name/email) into the box"
|
|
244
|
+
long_desc <<~DESC
|
|
245
|
+
Copies your git identity into the box's global git config so `git
|
|
246
|
+
commit` works there. Defaults to the host's `git config user.name` and
|
|
247
|
+
`user.email`; override either with --name/--email. Runs automatically
|
|
248
|
+
during `gdkbox up`, so this is mainly for boxes created before that or
|
|
249
|
+
for changing the identity later.
|
|
250
|
+
|
|
251
|
+
Pushing uses your host's ssh-agent, forwarded into interactive sessions
|
|
252
|
+
(`gdkbox ssh` / VS Code) by the generated SSH config — no keys or tokens
|
|
253
|
+
are stored in the box.
|
|
254
|
+
DESC
|
|
255
|
+
option :name, type: :string, desc: "git user.name to seed (defaults to the host's)"
|
|
256
|
+
option :email, type: :string, desc: "git user.email to seed (defaults to the host's)"
|
|
257
|
+
def set_git(name)
|
|
258
|
+
box = load_box!(name)
|
|
259
|
+
git_name, git_email = box.set_git_identity!(
|
|
260
|
+
git_name: options[:name], git_email: options[:email]
|
|
261
|
+
)
|
|
262
|
+
say "Seeded git identity into '#{name}': #{[git_name, git_email].compact.join(' <')}#{'>' if git_email}", :green
|
|
263
|
+
end
|
|
264
|
+
map "set-git" => :set_git
|
|
265
|
+
|
|
266
|
+
desc "set-remote NAME [REMOTE]", "Point the box's GitLab checkout at a different remote"
|
|
267
|
+
long_desc <<~DESC
|
|
268
|
+
The GDK-in-a-box image clones GitLab from the community mirror
|
|
269
|
+
(gitlab-community/gitlab-org/gitlab). This repoints the checkout's
|
|
270
|
+
origin — pass a full git URL or a namespace/project shorthand (expanded
|
|
271
|
+
to git@gitlab.com:namespace/project.git), or set `gitlab_remote:` in
|
|
272
|
+
~/.gdkbox/config.yml and omit REMOTE. Runs `git remote set-url` plus a
|
|
273
|
+
fetch inside the box; `gdkbox up` does the same automatically when a
|
|
274
|
+
remote is configured. Pushing uses your forwarded ssh-agent and your own
|
|
275
|
+
permissions — no credentials are stored in the box.
|
|
276
|
+
DESC
|
|
277
|
+
def set_remote(name, remote = nil)
|
|
278
|
+
box = load_box!(name)
|
|
279
|
+
remote ||= config.default_gitlab_remote
|
|
280
|
+
unless remote
|
|
281
|
+
raise Error, "No remote. Pass REMOTE (URL or namespace/project) " \
|
|
282
|
+
"or set `gitlab_remote:` in ~/.gdkbox/config.yml."
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
say "Pointing '#{name}' GitLab checkout at #{Box.expand_remote(remote)} (fetching)...", :green
|
|
286
|
+
url = box.set_gitlab_remote!(remote)
|
|
287
|
+
say "origin now #{url}.", :green
|
|
288
|
+
end
|
|
289
|
+
map "set-remote" => :set_remote
|
|
290
|
+
|
|
291
|
+
desc "set-host", "Add (default) or remove the gdk.local entry in /etc/hosts"
|
|
292
|
+
long_desc <<~DESC
|
|
293
|
+
GDK generates URLs and redirects that use the `gdk.local` hostname, so
|
|
294
|
+
your browser needs it mapped to loopback to reach a box's web UI. This
|
|
295
|
+
adds `#{HostsFile::ENTRY}` to /etc/hosts when no gdk.local entry exists
|
|
296
|
+
yet (writing needs sudo, so you may be prompted). `gdkbox up` runs the
|
|
297
|
+
same check automatically.
|
|
298
|
+
|
|
299
|
+
With --remove, the gdkbox-managed entry is removed again; lines you
|
|
300
|
+
added yourself are never touched.
|
|
301
|
+
DESC
|
|
302
|
+
option :add, type: :boolean, default: true, desc: "Add the entry (the default action)"
|
|
303
|
+
option :remove, type: :boolean, default: false,
|
|
304
|
+
desc: "Remove the gdkbox-managed entry instead"
|
|
305
|
+
def set_host
|
|
306
|
+
hosts = HostsFile.new
|
|
307
|
+
if options[:remove]
|
|
308
|
+
case hosts.remove
|
|
309
|
+
when :absent
|
|
310
|
+
say "No gdkbox-managed '#{HostsFile::HOSTNAME}' entry in #{hosts.path}. Nothing to do."
|
|
311
|
+
when :removed
|
|
312
|
+
say "Removed the '#{HostsFile::HOSTNAME}' entry from #{hosts.path}.", :green
|
|
313
|
+
when :manual
|
|
314
|
+
say "Could not write #{hosts.path}. Remove the line tagged " \
|
|
315
|
+
"'#{HostsFile::MARKER}' manually (e.g. with `sudoedit #{hosts.path}`).", :yellow
|
|
316
|
+
end
|
|
317
|
+
else
|
|
318
|
+
case hosts.add
|
|
319
|
+
when :present
|
|
320
|
+
say "'#{HostsFile::HOSTNAME}' already resolves via #{hosts.path}. Nothing to do."
|
|
321
|
+
when :added
|
|
322
|
+
say "Added '#{HostsFile::ENTRY}' to #{hosts.path}.", :green
|
|
323
|
+
when :manual
|
|
324
|
+
say "Could not write #{hosts.path}. Add the entry manually:", :yellow
|
|
325
|
+
say " echo '#{HostsFile::ENTRY}' | sudo tee -a #{hosts.path}", :yellow
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
map "set-host" => :set_host
|
|
330
|
+
|
|
237
331
|
desc "start NAME", "Start a stopped box (and re-enable SSH)"
|
|
238
332
|
def start(name)
|
|
239
333
|
box = load_box!(name)
|
|
@@ -370,6 +464,45 @@ module GDKBox
|
|
|
370
464
|
end
|
|
371
465
|
end
|
|
372
466
|
|
|
467
|
+
# Point the new box's GitLab checkout at the configured remote, when one
|
|
468
|
+
# is set (--gitlab-remote wins over config.yml; no setting keeps the
|
|
469
|
+
# image's community mirror). Best-effort like seed_skills: the box is
|
|
470
|
+
# already up, so a bad remote should warn, not abort. Muted in JSON mode.
|
|
471
|
+
def repoint_gitlab_remote(box)
|
|
472
|
+
remote = options[:gitlab_remote] || config.default_gitlab_remote
|
|
473
|
+
return unless remote
|
|
474
|
+
|
|
475
|
+
url = box.set_gitlab_remote!(remote)
|
|
476
|
+
say "Pointed the GitLab checkout at #{url}.", :green unless options[:json]
|
|
477
|
+
rescue StandardError => e
|
|
478
|
+
unless options[:json]
|
|
479
|
+
say "Could not repoint the GitLab checkout: #{e.message}", :yellow
|
|
480
|
+
say " Retry later with: gdkbox set-remote #{box.name} #{remote}", :yellow
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Ensure gdk.local resolves on the host so the box's web UI is reachable
|
|
485
|
+
# (GDK redirects to that hostname). Adding the entry needs sudo, so warn
|
|
486
|
+
# before any password prompt appears. Failures are non-fatal: the box is
|
|
487
|
+
# already up, and the user can run `gdkbox set-host` later. Skipped in
|
|
488
|
+
# --json mode, where prompting would break unattended orchestrators.
|
|
489
|
+
def setup_host
|
|
490
|
+
hosts = HostsFile.new
|
|
491
|
+
return if hosts.entry?
|
|
492
|
+
|
|
493
|
+
say "\nAdding '#{HostsFile::HOSTNAME}' to #{hosts.path} so the web UI resolves " \
|
|
494
|
+
"(may prompt for sudo)...", :green
|
|
495
|
+
case hosts.add
|
|
496
|
+
when :added
|
|
497
|
+
say "Added '#{HostsFile::ENTRY}' to #{hosts.path}.", :green
|
|
498
|
+
when :manual
|
|
499
|
+
say "Could not write #{hosts.path}. Run `gdkbox set-host`, or add it manually:", :yellow
|
|
500
|
+
say " echo '#{HostsFile::ENTRY}' | sudo tee -a #{hosts.path}", :yellow
|
|
501
|
+
end
|
|
502
|
+
rescue StandardError => e
|
|
503
|
+
say "Skipped #{hosts.path} setup: #{e.message}", :yellow
|
|
504
|
+
end
|
|
505
|
+
|
|
373
506
|
# Show the discovered skills and let the user pick by number (or "all").
|
|
374
507
|
def prompt_for_skills
|
|
375
508
|
found = Skills.discover
|
|
@@ -432,7 +565,7 @@ module GDKBox
|
|
|
432
565
|
say ""
|
|
433
566
|
say " SSH: ssh #{box.ssh_host_alias}", :cyan
|
|
434
567
|
say " VS Code: gdkbox code #{box.name}", :cyan
|
|
435
|
-
say " Web UI: #{box.web_url}", :cyan
|
|
568
|
+
say " Web UI: #{box.web_url} (http://#{HostsFile::HOSTNAME}:#{box.web_port})", :cyan
|
|
436
569
|
say " Agent: ssh #{box.ssh_host_alias} -t #{box.harness.bin}", :cyan
|
|
437
570
|
end
|
|
438
571
|
end
|
data/lib/gdkbox/completion.rb
CHANGED
|
@@ -11,7 +11,8 @@ module GDKBox
|
|
|
11
11
|
SHELLS = %w[bash zsh].freeze
|
|
12
12
|
|
|
13
13
|
# Subcommands whose first positional argument is an existing box.
|
|
14
|
-
BOX_COMMANDS = %w[status dispatch ssh code install-agent set-key
|
|
14
|
+
BOX_COMMANDS = %w[status dispatch ssh code install-agent set-key set-git set-remote start
|
|
15
|
+
stop rm add-skill].freeze
|
|
15
16
|
|
|
16
17
|
def initialize(cli_class = CLI)
|
|
17
18
|
@cli = cli_class
|
data/lib/gdkbox/config.rb
CHANGED
|
@@ -14,6 +14,7 @@ module GDKBox
|
|
|
14
14
|
#
|
|
15
15
|
# # ~/.gdkbox/config.yml
|
|
16
16
|
# image: registry.example.com/my-gdk:latest # default image for `up`
|
|
17
|
+
# gitlab_remote: gitlab-org/gitlab # repoint the GitLab checkout
|
|
17
18
|
# skills: # transferred into every box
|
|
18
19
|
# - gdkbox-fleet
|
|
19
20
|
# - code-history
|
|
@@ -22,6 +23,12 @@ module GDKBox
|
|
|
22
23
|
DEFAULT_IMAGE =
|
|
23
24
|
"registry.gitlab.com/gitlab-org/gitlab-development-kit/gitlab-gdk-in-a-box:main"
|
|
24
25
|
|
|
26
|
+
# The hostname GDK is configured for. It is set as the container's own
|
|
27
|
+
# hostname (so services inside the box can resolve it) and mapped to
|
|
28
|
+
# loopback in the host's /etc/hosts by HostsFile (so the host browser can
|
|
29
|
+
# follow GDK's redirects to it).
|
|
30
|
+
GDK_HOSTNAME = "gdk.local"
|
|
31
|
+
|
|
25
32
|
CONTAINER_PREFIX = "gdkbox-"
|
|
26
33
|
HOST_ALIAS_PREFIX = "gdkbox-"
|
|
27
34
|
|
|
@@ -96,6 +103,19 @@ module GDKBox
|
|
|
96
103
|
key.empty? ? Harness.default : key
|
|
97
104
|
end
|
|
98
105
|
|
|
106
|
+
# The git remote to point each box's GitLab checkout at, from config.yml
|
|
107
|
+
# (`gitlab_remote:`), or nil to keep the image's default (the community
|
|
108
|
+
# mirror). A URL or a "namespace/project" shorthand.
|
|
109
|
+
def default_gitlab_remote
|
|
110
|
+
remote = file_settings["gitlab_remote"].to_s.strip
|
|
111
|
+
remote.empty? ? nil : remote
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Where the GitLab repository lives inside the box.
|
|
115
|
+
def gitlab_checkout_path
|
|
116
|
+
File.join(remote_path, "gitlab")
|
|
117
|
+
end
|
|
118
|
+
|
|
99
119
|
# An API key read from config.yml under `config_key` (e.g.
|
|
100
120
|
# "anthropic_api_key", "openai_api_key"), or nil. This is a plaintext
|
|
101
121
|
# secret on disk, so it is the lowest-priority source (a flag or the
|
data/lib/gdkbox/docker.rb
CHANGED
|
@@ -192,9 +192,13 @@ module GDKBox
|
|
|
192
192
|
end
|
|
193
193
|
end
|
|
194
194
|
|
|
195
|
-
# Start a detached container, returning its id.
|
|
196
|
-
|
|
195
|
+
# Start a detached container, returning its id. When a hostname is given,
|
|
196
|
+
# Docker also maps it to the container's IP in the container's /etc/hosts
|
|
197
|
+
# and re-applies that mapping on every start (a manual edit would be lost:
|
|
198
|
+
# Docker regenerates the file when the container starts).
|
|
199
|
+
def run_container(name:, image:, publish: [], labels: {}, env: {}, args: [], hostname: nil)
|
|
197
200
|
cmd = ["docker", "run", "-d", "--name", name]
|
|
201
|
+
cmd.push("--hostname", hostname) if hostname
|
|
198
202
|
labels.each { |key, value| cmd.push("--label", "#{key}=#{value}") }
|
|
199
203
|
env.each { |key, value| cmd.push("-e", "#{key}=#{value}") }
|
|
200
204
|
publish.each { |mapping| cmd.push("-p", mapping) }
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GDKBox
|
|
4
|
+
# Manages the gdk.local entry in the host's /etc/hosts.
|
|
5
|
+
#
|
|
6
|
+
# GDK generates URLs and redirects that use the `gdk.local` hostname, so the
|
|
7
|
+
# host's browser needs it mapped to loopback to reach a box's web UI. The
|
|
8
|
+
# line gdkbox adds is tagged with a marker comment so it can be recognized
|
|
9
|
+
# and removed later without touching entries the user wrote themselves.
|
|
10
|
+
#
|
|
11
|
+
# /etc/hosts is root-owned: writes are attempted directly first (covers
|
|
12
|
+
# tests and unusual setups), then via `sudo tee`. Callers get a symbol
|
|
13
|
+
# describing what happened — :present/:added/:removed/:absent/:manual —
|
|
14
|
+
# and decide how to talk to the user; :manual means nothing was changed
|
|
15
|
+
# and the user has to edit the file themselves.
|
|
16
|
+
class HostsFile
|
|
17
|
+
HOSTNAME = Config::GDK_HOSTNAME
|
|
18
|
+
MARKER = "# added by gdkbox"
|
|
19
|
+
ENTRY = "127.0.0.1 #{HOSTNAME} #{MARKER}"
|
|
20
|
+
|
|
21
|
+
attr_reader :path
|
|
22
|
+
|
|
23
|
+
def initialize(path: "/etc/hosts", shell: Shell.new)
|
|
24
|
+
@path = path
|
|
25
|
+
@shell = shell
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Whether any active (non-comment) line already maps HOSTNAME, regardless
|
|
29
|
+
# of who added it — a user's own entry counts, so we never duplicate it.
|
|
30
|
+
def entry?
|
|
31
|
+
return false unless File.readable?(path)
|
|
32
|
+
|
|
33
|
+
File.foreach(path).any? { |line| maps_hostname?(line) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Ensure the entry exists. Returns :present when it already does, :added
|
|
37
|
+
# on success, or :manual when the file could not be written (with sudo
|
|
38
|
+
# declined/failed or disabled).
|
|
39
|
+
def add(sudo: true)
|
|
40
|
+
return :present if entry?
|
|
41
|
+
|
|
42
|
+
File.open(path, "a") { |f| f.puts(ENTRY) }
|
|
43
|
+
:added
|
|
44
|
+
rescue Errno::EACCES
|
|
45
|
+
return :manual unless sudo
|
|
46
|
+
|
|
47
|
+
result = @shell.run("sudo", "tee", "-a", path, input: "#{ENTRY}\n")
|
|
48
|
+
result.success? ? :added : :manual
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Remove the gdkbox-managed entry (matched by MARKER; user-written lines
|
|
52
|
+
# are never touched). Returns :absent when there is nothing to remove,
|
|
53
|
+
# :removed on success, or :manual when the file could not be written.
|
|
54
|
+
def remove(sudo: true)
|
|
55
|
+
lines = File.readable?(path) ? File.readlines(path) : []
|
|
56
|
+
kept = lines.reject { |line| managed?(line) }
|
|
57
|
+
return :absent if kept.size == lines.size
|
|
58
|
+
|
|
59
|
+
content = kept.join
|
|
60
|
+
begin
|
|
61
|
+
File.write(path, content)
|
|
62
|
+
:removed
|
|
63
|
+
rescue Errno::EACCES
|
|
64
|
+
return :manual unless sudo
|
|
65
|
+
|
|
66
|
+
result = @shell.run("sudo", "tee", path, input: content)
|
|
67
|
+
result.success? ? :removed : :manual
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Does this line actively map HOSTNAME? Comments are stripped first, so a
|
|
74
|
+
# commented-out entry does not count.
|
|
75
|
+
def maps_hostname?(line)
|
|
76
|
+
fields = line.sub(/#.*/, "").split
|
|
77
|
+
fields.length >= 2 && fields[1..].include?(HOSTNAME)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def managed?(line)
|
|
81
|
+
line.include?(MARKER) && line.include?(HOSTNAME)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/gdkbox/ports.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
module GDKBox
|
|
6
|
+
# Answers whether a host port is actually usable.
|
|
7
|
+
#
|
|
8
|
+
# gdkbox's metadata store only knows about ports gdkbox itself allocated;
|
|
9
|
+
# anything else on the machine — typically a locally running GDK on 2222 and
|
|
10
|
+
# 3000 — is invisible to it. Binding is the authoritative test: gdkbox
|
|
11
|
+
# publishes container ports on 127.0.0.1, so try to bind exactly there.
|
|
12
|
+
module Ports
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# True when something on the host already holds the port (or the bind is
|
|
16
|
+
# not permitted, which is just as unusable for publishing).
|
|
17
|
+
def bound?(port)
|
|
18
|
+
TCPServer.new("127.0.0.1", port).close
|
|
19
|
+
false
|
|
20
|
+
rescue SystemCallError
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/gdkbox/provisioner.rb
CHANGED
|
@@ -131,6 +131,27 @@ module GDKBox
|
|
|
131
131
|
done
|
|
132
132
|
BASH
|
|
133
133
|
|
|
134
|
+
# Seeds the GDK user's global git identity so `git commit` works inside
|
|
135
|
+
# the box. Values arrive via the environment (dodging shell quoting) and
|
|
136
|
+
# blanks are skipped, so a partial host config seeds what it can.
|
|
137
|
+
GIT_IDENTITY_SETUP = <<~'BASH'
|
|
138
|
+
set -e
|
|
139
|
+
[ -n "$GDKBOX_GIT_NAME" ] && git config --global user.name "$GDKBOX_GIT_NAME"
|
|
140
|
+
[ -n "$GDKBOX_GIT_EMAIL" ] && git config --global user.email "$GDKBOX_GIT_EMAIL"
|
|
141
|
+
true
|
|
142
|
+
BASH
|
|
143
|
+
|
|
144
|
+
# Points the GitLab checkout's origin at a different repository. The image
|
|
145
|
+
# hardcodes the community mirror (gitlab-community/gitlab-org/gitlab);
|
|
146
|
+
# team members typically want the canonical gitlab-org/gitlab. The mirror
|
|
147
|
+
# shares its history, so a set-url plus fetch is all it takes (the
|
|
148
|
+
# checkout is treeless, keeping the fetch cheap).
|
|
149
|
+
GITLAB_REMOTE_SETUP = <<~'BASH'
|
|
150
|
+
set -e
|
|
151
|
+
git remote set-url origin "$GDKBOX_GITLAB_REMOTE"
|
|
152
|
+
git fetch --quiet origin
|
|
153
|
+
BASH
|
|
154
|
+
|
|
134
155
|
def initialize(docker:, config:)
|
|
135
156
|
@docker = docker
|
|
136
157
|
@config = config
|
|
@@ -168,6 +189,26 @@ module GDKBox
|
|
|
168
189
|
)
|
|
169
190
|
end
|
|
170
191
|
|
|
192
|
+
def setup_git_identity(container_name, name:, email:)
|
|
193
|
+
@docker.exec(
|
|
194
|
+
container_name, GIT_IDENTITY_SETUP,
|
|
195
|
+
user: @config.ssh_user,
|
|
196
|
+
env: {
|
|
197
|
+
"GDKBOX_GIT_NAME" => name.to_s,
|
|
198
|
+
"GDKBOX_GIT_EMAIL" => email.to_s
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def setup_gitlab_remote(container_name, url)
|
|
204
|
+
@docker.exec(
|
|
205
|
+
container_name, GITLAB_REMOTE_SETUP,
|
|
206
|
+
user: @config.ssh_user,
|
|
207
|
+
workdir: @config.gitlab_checkout_path,
|
|
208
|
+
env: { "GDKBOX_GITLAB_REMOTE" => url }
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
171
212
|
def setup_api_key(container_name, api_key, key_env)
|
|
172
213
|
@docker.exec(
|
|
173
214
|
container_name, API_KEY_SETUP,
|
data/lib/gdkbox/ssh_config.rb
CHANGED
|
@@ -32,6 +32,11 @@ module GDKBox
|
|
|
32
32
|
lines << " User #{box['ssh_user']}"
|
|
33
33
|
lines << " IdentityFile #{@config.private_key_path}"
|
|
34
34
|
lines << " IdentitiesOnly yes"
|
|
35
|
+
# Forward the host's ssh-agent so `git push` inside the box uses the
|
|
36
|
+
# user's own keys — which never enter the container, and are only
|
|
37
|
+
# reachable while a human session is connected (dispatched agents run
|
|
38
|
+
# via `docker exec` and never see the agent).
|
|
39
|
+
lines << " ForwardAgent yes"
|
|
35
40
|
lines << " StrictHostKeyChecking no"
|
|
36
41
|
lines << " UserKnownHostsFile /dev/null"
|
|
37
42
|
lines << ""
|
data/lib/gdkbox/version.rb
CHANGED
data/lib/gdkbox.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gdkbox
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- jotolo
|
|
@@ -62,6 +62,8 @@ files:
|
|
|
62
62
|
- lib/gdkbox/config.rb
|
|
63
63
|
- lib/gdkbox/docker.rb
|
|
64
64
|
- lib/gdkbox/harness.rb
|
|
65
|
+
- lib/gdkbox/hosts_file.rb
|
|
66
|
+
- lib/gdkbox/ports.rb
|
|
65
67
|
- lib/gdkbox/progress_bar.rb
|
|
66
68
|
- lib/gdkbox/provisioner.rb
|
|
67
69
|
- lib/gdkbox/shell.rb
|