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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 851b8b309a5e04c8e48d1090e45453b0f8464779a0d1cb30aaa69e5edf0829fe
4
- data.tar.gz: 01af5d8bd6d09cca936bf801f22a82ff143e4357b86ce47dc6478659d19d3f9e
3
+ metadata.gz: 75c5c8ed1f89cb877856f68e45c39f4b5460131ea5a312e5af10e7cb7de2688b
4
+ data.tar.gz: a0eb3697bb1b630665be5799d33ffb8bb4aec0999858e075861e3469bd312431
5
5
  SHA512:
6
- metadata.gz: 9a5201a873d0f54c7864f2e746e7077ea406f12f6ffce032280579ab1241d74fd743e6a4376204d89aa73bb75f30cdb697cc9bb99e3253e5f819d9e03d0adf17
7
- data.tar.gz: 569e92b7c9d527932912790e6a0e2b48be01920d5bd29cd80788673827adc17a2230fcd66f212f006b704f5e8b8b7fcee4154d0d7d502ff87e470d7b10afca31
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 as-is.
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
@@ -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 start stop rm add-skill].freeze
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
- def run_container(name:, image:, publish: [], labels: {}, env: {}, args: [])
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
@@ -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
@@ -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,
@@ -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 << ""
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GDKBox
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.10"
5
5
  end
data/lib/gdkbox.rb CHANGED
@@ -10,6 +10,8 @@ require "gdkbox/shell"
10
10
  require "gdkbox/progress_bar"
11
11
  require "gdkbox/config"
12
12
  require "gdkbox/docker"
13
+ require "gdkbox/hosts_file"
14
+ require "gdkbox/ports"
13
15
  require "gdkbox/store"
14
16
  require "gdkbox/ssh_key"
15
17
  require "gdkbox/ssh_config"
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.5
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