gdkbox 0.1.5 → 0.1.8
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 +4 -3
- data/lib/gdkbox/box.rb +18 -2
- data/lib/gdkbox/cli.rb +65 -1
- data/lib/gdkbox/config.rb +6 -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/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: e1c7b818ffd457d8f32f3d80073585222be0140ba3ae5c32a76b587bae5a98f5
|
|
4
|
+
data.tar.gz: 35b56df278015bbf0ad44cf175665c481cddd3939bc2130a5b72d193aec44c90
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0fd71a1460e658cd975298666fc7afaa1110a2b154a0da005bc80c7c4e9bdb21545ab544091c5a2f2773eab48e2e9520311e24b39c16962a7ae2416b9024bcf
|
|
7
|
+
data.tar.gz: 44638a681108d58ddafdb1a9d4703f5a1f84cd6da764dae608d25ead66a3ebba8f4bb9057d57c38176ecd4f3c49e5c6809636c159c5c8ebbd0803c4b0ff2415d
|
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,7 @@ 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`. |
|
|
92
93
|
| `gdkbox start NAME` | Start a stopped box (and re-enable SSH). |
|
|
93
94
|
| `gdkbox stop NAME` | Stop a running box. |
|
|
94
95
|
| `gdkbox rm NAME` | Remove a box: container, metadata, and SSH entry. |
|
|
@@ -104,8 +105,8 @@ Or run it straight from the checkout without installing:
|
|
|
104
105
|
| Option | Default | Description |
|
|
105
106
|
| --- | --- | --- |
|
|
106
107
|
| `--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. |
|
|
108
|
+
| `--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). |
|
|
109
|
+
| `--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
110
|
| `--harness ID` | `claude` (or config.yml) | Agent harness to install: claude, codex, opencode, pi. |
|
|
110
111
|
| `--no-agent` | (agent installed) | Skip installing the agent harness. |
|
|
111
112
|
| `--skill NAME [NAME...]` | (none) | Seed skill(s) into the box at creation for dispatched agents. |
|
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}"
|
|
@@ -266,8 +270,16 @@ module GDKBox
|
|
|
266
270
|
# Choose free host ports and persist a preliminary record claiming them,
|
|
267
271
|
# all while holding an exclusive lock so concurrent `create!` calls in
|
|
268
272
|
# separate processes serialize and cannot pick the same ports. Returns the
|
|
269
|
-
# chosen [ssh_port, web_port]. Caller-supplied ports are honored
|
|
273
|
+
# chosen [ssh_port, web_port]. Caller-supplied ports are honored, but fail
|
|
274
|
+
# fast with a clear message when something already holds them — better
|
|
275
|
+
# than the cryptic bind error `docker run` would produce later.
|
|
270
276
|
def reserve_ports!(cname, ssh_port, web_port)
|
|
277
|
+
{ "--ssh-port" => ssh_port, "--web-port" => web_port }.each do |flag, port|
|
|
278
|
+
if port && Ports.bound?(port)
|
|
279
|
+
raise Error, "Port #{port} (#{flag}) is already in use on 127.0.0.1."
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
271
283
|
@config.ensure_dirs!
|
|
272
284
|
File.open(@config.lock_path, File::RDWR | File::CREAT, 0o644) do |lock|
|
|
273
285
|
lock.flock(File::LOCK_EX)
|
|
@@ -286,10 +298,14 @@ module GDKBox
|
|
|
286
298
|
end
|
|
287
299
|
end
|
|
288
300
|
|
|
301
|
+
# The next port at or above base that is neither recorded by gdkbox nor
|
|
302
|
+
# actually bound on the host. The store only covers gdkbox's own boxes;
|
|
303
|
+
# a locally running GDK (typically on 2222/3000) is invisible to it, so
|
|
304
|
+
# each candidate is also probed with a real bind.
|
|
289
305
|
def next_port(base, exclude: [])
|
|
290
306
|
used = @store.used_ports + exclude
|
|
291
307
|
port = base
|
|
292
|
-
port += 1 while used.include?(port)
|
|
308
|
+
port += 1 while used.include?(port) || Ports.bound?(port)
|
|
293
309
|
port
|
|
294
310
|
end
|
|
295
311
|
end
|
data/lib/gdkbox/cli.rb
CHANGED
|
@@ -61,6 +61,8 @@ module GDKBox
|
|
|
61
61
|
return
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
setup_host
|
|
65
|
+
|
|
64
66
|
say "\nBox '#{name}' is up.", :green
|
|
65
67
|
print_connection_details(box)
|
|
66
68
|
unless api_key
|
|
@@ -234,6 +236,46 @@ module GDKBox
|
|
|
234
236
|
end
|
|
235
237
|
map "set-key" => :set_key
|
|
236
238
|
|
|
239
|
+
desc "set-host", "Add (default) or remove the gdk.local entry in /etc/hosts"
|
|
240
|
+
long_desc <<~DESC
|
|
241
|
+
GDK generates URLs and redirects that use the `gdk.local` hostname, so
|
|
242
|
+
your browser needs it mapped to loopback to reach a box's web UI. This
|
|
243
|
+
adds `#{HostsFile::ENTRY}` to /etc/hosts when no gdk.local entry exists
|
|
244
|
+
yet (writing needs sudo, so you may be prompted). `gdkbox up` runs the
|
|
245
|
+
same check automatically.
|
|
246
|
+
|
|
247
|
+
With --remove, the gdkbox-managed entry is removed again; lines you
|
|
248
|
+
added yourself are never touched.
|
|
249
|
+
DESC
|
|
250
|
+
option :add, type: :boolean, default: true, desc: "Add the entry (the default action)"
|
|
251
|
+
option :remove, type: :boolean, default: false,
|
|
252
|
+
desc: "Remove the gdkbox-managed entry instead"
|
|
253
|
+
def set_host
|
|
254
|
+
hosts = HostsFile.new
|
|
255
|
+
if options[:remove]
|
|
256
|
+
case hosts.remove
|
|
257
|
+
when :absent
|
|
258
|
+
say "No gdkbox-managed '#{HostsFile::HOSTNAME}' entry in #{hosts.path}. Nothing to do."
|
|
259
|
+
when :removed
|
|
260
|
+
say "Removed the '#{HostsFile::HOSTNAME}' entry from #{hosts.path}.", :green
|
|
261
|
+
when :manual
|
|
262
|
+
say "Could not write #{hosts.path}. Remove the line tagged " \
|
|
263
|
+
"'#{HostsFile::MARKER}' manually (e.g. with `sudoedit #{hosts.path}`).", :yellow
|
|
264
|
+
end
|
|
265
|
+
else
|
|
266
|
+
case hosts.add
|
|
267
|
+
when :present
|
|
268
|
+
say "'#{HostsFile::HOSTNAME}' already resolves via #{hosts.path}. Nothing to do."
|
|
269
|
+
when :added
|
|
270
|
+
say "Added '#{HostsFile::ENTRY}' to #{hosts.path}.", :green
|
|
271
|
+
when :manual
|
|
272
|
+
say "Could not write #{hosts.path}. Add the entry manually:", :yellow
|
|
273
|
+
say " echo '#{HostsFile::ENTRY}' | sudo tee -a #{hosts.path}", :yellow
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
map "set-host" => :set_host
|
|
278
|
+
|
|
237
279
|
desc "start NAME", "Start a stopped box (and re-enable SSH)"
|
|
238
280
|
def start(name)
|
|
239
281
|
box = load_box!(name)
|
|
@@ -370,6 +412,28 @@ module GDKBox
|
|
|
370
412
|
end
|
|
371
413
|
end
|
|
372
414
|
|
|
415
|
+
# Ensure gdk.local resolves on the host so the box's web UI is reachable
|
|
416
|
+
# (GDK redirects to that hostname). Adding the entry needs sudo, so warn
|
|
417
|
+
# before any password prompt appears. Failures are non-fatal: the box is
|
|
418
|
+
# already up, and the user can run `gdkbox set-host` later. Skipped in
|
|
419
|
+
# --json mode, where prompting would break unattended orchestrators.
|
|
420
|
+
def setup_host
|
|
421
|
+
hosts = HostsFile.new
|
|
422
|
+
return if hosts.entry?
|
|
423
|
+
|
|
424
|
+
say "\nAdding '#{HostsFile::HOSTNAME}' to #{hosts.path} so the web UI resolves " \
|
|
425
|
+
"(may prompt for sudo)...", :green
|
|
426
|
+
case hosts.add
|
|
427
|
+
when :added
|
|
428
|
+
say "Added '#{HostsFile::ENTRY}' to #{hosts.path}.", :green
|
|
429
|
+
when :manual
|
|
430
|
+
say "Could not write #{hosts.path}. Run `gdkbox set-host`, or add it manually:", :yellow
|
|
431
|
+
say " echo '#{HostsFile::ENTRY}' | sudo tee -a #{hosts.path}", :yellow
|
|
432
|
+
end
|
|
433
|
+
rescue StandardError => e
|
|
434
|
+
say "Skipped #{hosts.path} setup: #{e.message}", :yellow
|
|
435
|
+
end
|
|
436
|
+
|
|
373
437
|
# Show the discovered skills and let the user pick by number (or "all").
|
|
374
438
|
def prompt_for_skills
|
|
375
439
|
found = Skills.discover
|
|
@@ -432,7 +496,7 @@ module GDKBox
|
|
|
432
496
|
say ""
|
|
433
497
|
say " SSH: ssh #{box.ssh_host_alias}", :cyan
|
|
434
498
|
say " VS Code: gdkbox code #{box.name}", :cyan
|
|
435
|
-
say " Web UI: #{box.web_url}", :cyan
|
|
499
|
+
say " Web UI: #{box.web_url} (http://#{HostsFile::HOSTNAME}:#{box.web_port})", :cyan
|
|
436
500
|
say " Agent: ssh #{box.ssh_host_alias} -t #{box.harness.bin}", :cyan
|
|
437
501
|
end
|
|
438
502
|
end
|
data/lib/gdkbox/config.rb
CHANGED
|
@@ -22,6 +22,12 @@ module GDKBox
|
|
|
22
22
|
DEFAULT_IMAGE =
|
|
23
23
|
"registry.gitlab.com/gitlab-org/gitlab-development-kit/gitlab-gdk-in-a-box:main"
|
|
24
24
|
|
|
25
|
+
# The hostname GDK is configured for. It is set as the container's own
|
|
26
|
+
# hostname (so services inside the box can resolve it) and mapped to
|
|
27
|
+
# loopback in the host's /etc/hosts by HostsFile (so the host browser can
|
|
28
|
+
# follow GDK's redirects to it).
|
|
29
|
+
GDK_HOSTNAME = "gdk.local"
|
|
30
|
+
|
|
25
31
|
CONTAINER_PREFIX = "gdkbox-"
|
|
26
32
|
HOST_ALIAS_PREFIX = "gdkbox-"
|
|
27
33
|
|
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/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.8
|
|
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
|