gdkbox 0.1.8 → 0.1.12
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 +21 -0
- data/lib/gdkbox/box.rb +87 -0
- data/lib/gdkbox/cli.rb +106 -0
- data/lib/gdkbox/completion.rb +2 -1
- data/lib/gdkbox/config.rb +14 -0
- data/lib/gdkbox/docker.rb +15 -0
- data/lib/gdkbox/provisioner.rb +113 -0
- data/lib/gdkbox/ssh_config.rb +5 -0
- data/lib/gdkbox/version.rb +1 -1
- 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: b71b4f7e72836332b32feca0a6e3e0c485f8255fcfb0f1dc93c7792c3f6fc5ab
|
|
4
|
+
data.tar.gz: 66e0331f211fd1df2b67694e42e65d8971502615b42c61093f86a3a465e06425
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b70d9d914b6ce31f0a644cae9260a98adb3540b0d76555a1ed4e72fc423b9602deb84b338b1b91cf29bf455546b248e875dbfa2e7fdc5dafc9b37322cb2d58be
|
|
7
|
+
data.tar.gz: 73ebbfbfb11732b3d5fb9f40ea0d795a846c61ccb24d513c51684542424d2d78a7eef919e03d5d2007f64df3c3f0704cb75f07dbc3e7b494f90f9ae892daec43
|
data/README.md
CHANGED
|
@@ -90,6 +90,9 @@ Or run it straight from the checkout without installing:
|
|
|
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
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. |
|
|
95
|
+
| `gdkbox hydrate NAME` | Backfill the box's treeless GitLab clone so deep rebases/blame/bisect work (`--trees` for a much smaller trees-only fetch). |
|
|
93
96
|
| `gdkbox start NAME` | Start a stopped box (and re-enable SSH). |
|
|
94
97
|
| `gdkbox stop NAME` | Stop a running box. |
|
|
95
98
|
| `gdkbox rm NAME` | Remove a box: container, metadata, and SSH entry. |
|
|
@@ -110,6 +113,7 @@ Or run it straight from the checkout without installing:
|
|
|
110
113
|
| `--harness ID` | `claude` (or config.yml) | Agent harness to install: claude, codex, opencode, pi. |
|
|
111
114
|
| `--no-agent` | (agent installed) | Skip installing the agent harness. |
|
|
112
115
|
| `--skill NAME [NAME...]` | (none) | Seed skill(s) into the box at creation for dispatched agents. |
|
|
116
|
+
| `--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. |
|
|
113
117
|
|
|
114
118
|
## Typical workflow
|
|
115
119
|
|
|
@@ -133,6 +137,23 @@ gdkbox stop demo
|
|
|
133
137
|
gdkbox rm demo
|
|
134
138
|
```
|
|
135
139
|
|
|
140
|
+
### Working with git history in a box
|
|
141
|
+
|
|
142
|
+
The image's GitLab checkout is a **treeless clone** (`--filter=tree:0`, ~550 MB
|
|
143
|
+
instead of many GB). Day-to-day work near `master` is fine, but operations that
|
|
144
|
+
span a large history gap — rebasing a branch hundreds of commits behind,
|
|
145
|
+
`git blame` or `git bisect` across old ranges — trigger storms of lazy object
|
|
146
|
+
fetches and fail with errors like `could not fetch <sha> from promisor remote`
|
|
147
|
+
(not merge conflicts, even when a rebase reports failure). Either keep branches
|
|
148
|
+
current and do deep-history work elsewhere, or hydrate the box once:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
gdkbox hydrate demo --trees # fetch missing trees only: much smaller, usually
|
|
152
|
+
# enough for deep rebases (blobs stay lazy)
|
|
153
|
+
gdkbox hydrate demo # full backfill: several GB, then the clone
|
|
154
|
+
# behaves like a normal full clone
|
|
155
|
+
```
|
|
156
|
+
|
|
136
157
|
## Orchestrating a fleet of agents
|
|
137
158
|
|
|
138
159
|
The end goal of `gdkbox` is to back an **orchestrator** that runs a pool of
|
data/lib/gdkbox/box.rb
CHANGED
|
@@ -137,6 +137,15 @@ module GDKBox
|
|
|
137
137
|
)
|
|
138
138
|
@store.save(@data)
|
|
139
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
|
+
|
|
140
149
|
provisioner.setup_agent(cname, harness) if install_agent
|
|
141
150
|
@data["agent_installed"] = install_agent
|
|
142
151
|
|
|
@@ -166,6 +175,75 @@ module GDKBox
|
|
|
166
175
|
@store.delete(name)
|
|
167
176
|
end
|
|
168
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
|
+
# The host an SSH-style git URL connects to, or nil for non-SSH remotes
|
|
188
|
+
# (https needs no SSH client preparation).
|
|
189
|
+
def self.remote_ssh_host(url)
|
|
190
|
+
if (m = url.match(%r{\Assh://(?:[^@/]+@)?([^:/]+)}))
|
|
191
|
+
m[1]
|
|
192
|
+
elsif (m = url.match(/\A(?:[^@:\/]+@)([^:]+):/))
|
|
193
|
+
m[1]
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Point the box's GitLab checkout at a different remote (URL or
|
|
198
|
+
# "namespace/project"). SSH remotes get the box's SSH client prepared
|
|
199
|
+
# first (host key acceptance + connection multiplexing) so the follow-up
|
|
200
|
+
# fetch — and later lazy fetches from the treeless clone — succeed.
|
|
201
|
+
# Returns the URL that origin now uses.
|
|
202
|
+
def set_gitlab_remote!(remote)
|
|
203
|
+
url = self.class.expand_remote(remote)
|
|
204
|
+
provisioner = Provisioner.new(docker: @docker, config: @config)
|
|
205
|
+
if (host = self.class.remote_ssh_host(url))
|
|
206
|
+
provisioner.setup_git_ssh(container_name, host)
|
|
207
|
+
end
|
|
208
|
+
provisioner.setup_gitlab_remote(container_name, url)
|
|
209
|
+
url
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Backfill the box's treeless GitLab clone so deep-history operations
|
|
213
|
+
# (old rebases, blame, bisect) work. trees: true fetches only the missing
|
|
214
|
+
# trees (much smaller; file contents stay lazy). Prepares the SSH client
|
|
215
|
+
# first when origin is an SSH remote — a refetch without multiplexing
|
|
216
|
+
# would hit the same throttling the lazy fetches do. Streams git's
|
|
217
|
+
# progress lines to the block.
|
|
218
|
+
def hydrate!(trees: false, &progress)
|
|
219
|
+
provisioner = Provisioner.new(docker: @docker, config: @config)
|
|
220
|
+
origin = @docker.exec(
|
|
221
|
+
container_name, "git remote get-url origin",
|
|
222
|
+
user: @config.ssh_user, workdir: @config.gitlab_checkout_path
|
|
223
|
+
).stdout.strip
|
|
224
|
+
if (host = self.class.remote_ssh_host(origin))
|
|
225
|
+
provisioner.setup_git_ssh(container_name, host)
|
|
226
|
+
end
|
|
227
|
+
provisioner.hydrate(container_name, trees: trees, &progress)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Seed a git identity into the box so `git commit` works there. Falls back
|
|
231
|
+
# to the host's `git config` when name/email are not given; raises when
|
|
232
|
+
# neither source has anything to seed. Returns the [name, email] seeded.
|
|
233
|
+
def set_git_identity!(git_name: nil, git_email: nil)
|
|
234
|
+
host_name, host_email = host_git_identity
|
|
235
|
+
git_name ||= host_name
|
|
236
|
+
git_email ||= host_email
|
|
237
|
+
if git_name.nil? && git_email.nil?
|
|
238
|
+
raise Error, "No git identity found. Pass --name/--email or set " \
|
|
239
|
+
"`git config --global user.name/user.email` on the host."
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
Provisioner.new(docker: @docker, config: @config)
|
|
243
|
+
.setup_git_identity(container_name, name: git_name, email: git_email)
|
|
244
|
+
[git_name, git_email]
|
|
245
|
+
end
|
|
246
|
+
|
|
169
247
|
# (Re)install this box's agent harness inside the container.
|
|
170
248
|
def install_agent!
|
|
171
249
|
Provisioner.new(docker: @docker, config: @config).setup_agent(container_name, harness)
|
|
@@ -267,6 +345,15 @@ module GDKBox
|
|
|
267
345
|
|
|
268
346
|
private
|
|
269
347
|
|
|
348
|
+
# The host's git identity as [name, email], each nil when unset.
|
|
349
|
+
def host_git_identity
|
|
350
|
+
read = lambda do |key|
|
|
351
|
+
value = @shell.run("git", "config", "--get", key).stdout.strip
|
|
352
|
+
value.empty? ? nil : value
|
|
353
|
+
end
|
|
354
|
+
[read.call("user.name"), read.call("user.email")]
|
|
355
|
+
end
|
|
356
|
+
|
|
270
357
|
# Choose free host ports and persist a preliminary record claiming them,
|
|
271
358
|
# all while holding an exclusive lock so concurrent `create!` calls in
|
|
272
359
|
# separate processes serialize and cannot pick the same ports. Returns the
|
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,6 +58,7 @@ 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)
|
|
@@ -236,6 +240,90 @@ module GDKBox
|
|
|
236
240
|
end
|
|
237
241
|
map "set-key" => :set_key
|
|
238
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 "hydrate NAME", "Backfill the box's treeless GitLab clone (deep rebases, blame, bisect)"
|
|
292
|
+
long_desc <<~DESC
|
|
293
|
+
The GDK-in-a-box image clones GitLab treeless (--filter=tree:0), so
|
|
294
|
+
operations spanning a large history gap — rebasing a branch far behind
|
|
295
|
+
master, blame or bisect across old ranges — storm the network with lazy
|
|
296
|
+
fetches and fail. Hydrating backfills the missing objects once.
|
|
297
|
+
|
|
298
|
+
By default everything is fetched and the clone becomes a normal full
|
|
299
|
+
clone (a large one-time download, likely several GB). With --trees only
|
|
300
|
+
the missing trees are fetched — a fraction of the size, and usually
|
|
301
|
+
enough for deep rebases; file contents are still fetched lazily as
|
|
302
|
+
needed. Progress is streamed live.
|
|
303
|
+
DESC
|
|
304
|
+
option :trees, type: :boolean, default: false,
|
|
305
|
+
desc: "Fetch missing trees only (much smaller; blobs stay lazy)"
|
|
306
|
+
option :force, type: :boolean, default: false, desc: "Skip confirmation"
|
|
307
|
+
def hydrate(name)
|
|
308
|
+
box = load_box!(name)
|
|
309
|
+
unless options[:force]
|
|
310
|
+
estimate = options[:trees] ? "a sizeable download" : "a large download, likely several GB"
|
|
311
|
+
return unless yes?("Hydrate '#{name}' (#{estimate})? [y/N]")
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
say "Hydrating '#{name}' (#{options[:trees] ? 'trees only' : 'full'})...", :green
|
|
315
|
+
tty = $stderr.tty?
|
|
316
|
+
box.hydrate!(trees: options[:trees]) do |line|
|
|
317
|
+
if tty
|
|
318
|
+
$stderr.print("\r\e[K#{line}")
|
|
319
|
+
elsif !line.include?("%") # skip per-percent spam when piped
|
|
320
|
+
$stderr.puts(line)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
$stderr.print("\n") if tty
|
|
324
|
+
say "Done. '#{name}' can now work across the full history.", :green
|
|
325
|
+
end
|
|
326
|
+
|
|
239
327
|
desc "set-host", "Add (default) or remove the gdk.local entry in /etc/hosts"
|
|
240
328
|
long_desc <<~DESC
|
|
241
329
|
GDK generates URLs and redirects that use the `gdk.local` hostname, so
|
|
@@ -412,6 +500,24 @@ module GDKBox
|
|
|
412
500
|
end
|
|
413
501
|
end
|
|
414
502
|
|
|
503
|
+
# Point the new box's GitLab checkout at the configured remote, when one
|
|
504
|
+
# is set (--gitlab-remote wins over config.yml; no setting keeps the
|
|
505
|
+
# image's community mirror). Best-effort like seed_skills: the box is
|
|
506
|
+
# already up, so a bad remote should warn, not abort. Muted in JSON mode.
|
|
507
|
+
def repoint_gitlab_remote(box)
|
|
508
|
+
remote = options[:gitlab_remote] || config.default_gitlab_remote
|
|
509
|
+
return unless remote
|
|
510
|
+
|
|
511
|
+
url = box.set_gitlab_remote!(remote)
|
|
512
|
+
say "Pointed the GitLab checkout at #{url}.", :green unless options[:json]
|
|
513
|
+
rescue StandardError => e
|
|
514
|
+
# Warn on stderr even in --json mode: stdout stays clean JSON, but a
|
|
515
|
+
# silently failed repoint costs the user a debugging session later
|
|
516
|
+
# (the box looks fine until the first fetch).
|
|
517
|
+
warn "gdkbox: could not repoint the GitLab checkout: #{e.message}"
|
|
518
|
+
warn "gdkbox: retry later with: gdkbox set-remote #{box.name} #{remote}"
|
|
519
|
+
end
|
|
520
|
+
|
|
415
521
|
# Ensure gdk.local resolves on the host so the box's web UI is reachable
|
|
416
522
|
# (GDK redirects to that hostname). Adding the entry needs sudo, so warn
|
|
417
523
|
# before any password prompt appears. Failures are non-fatal: the box is
|
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 hydrate
|
|
15
|
+
start 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
|
|
@@ -102,6 +103,19 @@ module GDKBox
|
|
|
102
103
|
key.empty? ? Harness.default : key
|
|
103
104
|
end
|
|
104
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
|
+
|
|
105
119
|
# An API key read from config.yml under `config_key` (e.g.
|
|
106
120
|
# "anthropic_api_key", "openai_api_key"), or nil. This is a plaintext
|
|
107
121
|
# secret on disk, so it is the lowest-priority source (a flag or the
|
data/lib/gdkbox/docker.rb
CHANGED
|
@@ -223,6 +223,21 @@ module GDKBox
|
|
|
223
223
|
check ? @shell.run!(*cmd, input: input) : @shell.run(*cmd, input: input)
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
+
# Like #exec, but for long-running scripts whose output should be seen
|
|
227
|
+
# live (e.g. a multi-GB git fetch): runs under a PTY and yields each
|
|
228
|
+
# output line as it appears. Raises CommandError on a non-zero exit.
|
|
229
|
+
def exec_stream(name, script, user: nil, workdir: nil, env: {}, &block)
|
|
230
|
+
cmd = ["docker", "exec"]
|
|
231
|
+
cmd.push("-u", user) if user
|
|
232
|
+
cmd.push("-w", workdir) if workdir
|
|
233
|
+
env.each { |key, value| cmd.push("-e", "#{key}=#{value}") }
|
|
234
|
+
cmd.push(name, "bash", "-lc", script)
|
|
235
|
+
result = @shell.stream_tty(*cmd, &block)
|
|
236
|
+
raise CommandError.new(cmd, result.status, "") unless result.success?
|
|
237
|
+
|
|
238
|
+
result
|
|
239
|
+
end
|
|
240
|
+
|
|
226
241
|
# Copy a host path into a container at dest (a full destination path).
|
|
227
242
|
def cp_into(name, src, dest)
|
|
228
243
|
@shell.run!("docker", "cp", src, "#{name}:#{dest}")
|
data/lib/gdkbox/provisioner.rb
CHANGED
|
@@ -131,6 +131,79 @@ 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
|
+
# Prepares the box's SSH client for real git traffic against a remote
|
|
145
|
+
# host (issue #6, hit when a gitlab_remote is configured):
|
|
146
|
+
#
|
|
147
|
+
# - `StrictHostKeyChecking accept-new`: a fresh box has an empty
|
|
148
|
+
# known_hosts, so the first fetch would die on host key verification.
|
|
149
|
+
# - Connection multiplexing: lazy fetches from the treeless clone open
|
|
150
|
+
# SSH connections in bursts; gitlab.com throttles that, which surfaces
|
|
151
|
+
# as a misleading "Permission denied (publickey)". One shared control
|
|
152
|
+
# connection absorbs the burst.
|
|
153
|
+
#
|
|
154
|
+
# The stanza is tagged per host, so re-runs are no-ops and different
|
|
155
|
+
# remotes each get their own block.
|
|
156
|
+
GIT_SSH_SETUP = <<~'BASH'
|
|
157
|
+
set -e
|
|
158
|
+
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
|
|
159
|
+
config="$HOME/.ssh/config"
|
|
160
|
+
touch "$config" && chmod 600 "$config"
|
|
161
|
+
if ! grep -qF "gdkbox:git-ssh $GDKBOX_GIT_HOST" "$config"; then
|
|
162
|
+
cat >> "$config" <<EOF
|
|
163
|
+
|
|
164
|
+
# gdkbox:git-ssh $GDKBOX_GIT_HOST
|
|
165
|
+
Host $GDKBOX_GIT_HOST
|
|
166
|
+
StrictHostKeyChecking accept-new
|
|
167
|
+
ControlMaster auto
|
|
168
|
+
ControlPath ~/.ssh/cm-%r@%h-%p
|
|
169
|
+
ControlPersist 20m
|
|
170
|
+
ServerAliveInterval 30
|
|
171
|
+
EOF
|
|
172
|
+
fi
|
|
173
|
+
BASH
|
|
174
|
+
|
|
175
|
+
# Points the GitLab checkout's origin at a different repository. The image
|
|
176
|
+
# hardcodes the community mirror (gitlab-community/gitlab-org/gitlab);
|
|
177
|
+
# team members typically want the canonical gitlab-org/gitlab. The mirror
|
|
178
|
+
# shares its history, so a set-url plus fetch is all it takes (the
|
|
179
|
+
# checkout is treeless, keeping the fetch cheap).
|
|
180
|
+
GITLAB_REMOTE_SETUP = <<~'BASH'
|
|
181
|
+
set -e
|
|
182
|
+
git remote set-url origin "$GDKBOX_GITLAB_REMOTE"
|
|
183
|
+
git fetch --quiet origin
|
|
184
|
+
BASH
|
|
185
|
+
|
|
186
|
+
# Backfills the treeless GitLab clone (the image clones with
|
|
187
|
+
# --filter=tree:0, so operations spanning a large history gap storm the
|
|
188
|
+
# network with lazy fetches and fail; see #7). Two levels:
|
|
189
|
+
#
|
|
190
|
+
# - GDKBOX_HYDRATE_FILTER=blob:none — fetch all missing trees, keep file
|
|
191
|
+
# contents lazy. A fraction of the download; enough for deep rebases,
|
|
192
|
+
# which mostly storm tree fetches.
|
|
193
|
+
# - no filter (full) — fetch everything and drop the promisor config;
|
|
194
|
+
# the repository behaves like a normal full clone afterwards.
|
|
195
|
+
HYDRATE = <<~'BASH'
|
|
196
|
+
set -e
|
|
197
|
+
if [ -n "$GDKBOX_HYDRATE_FILTER" ]; then
|
|
198
|
+
git config remote.origin.partialclonefilter "$GDKBOX_HYDRATE_FILTER"
|
|
199
|
+
git fetch --refetch --progress origin
|
|
200
|
+
else
|
|
201
|
+
git config --unset-all remote.origin.partialclonefilter || true
|
|
202
|
+
git fetch --refetch --progress origin
|
|
203
|
+
git config --unset remote.origin.promisor || true
|
|
204
|
+
fi
|
|
205
|
+
BASH
|
|
206
|
+
|
|
134
207
|
def initialize(docker:, config:)
|
|
135
208
|
@docker = docker
|
|
136
209
|
@config = config
|
|
@@ -168,6 +241,46 @@ module GDKBox
|
|
|
168
241
|
)
|
|
169
242
|
end
|
|
170
243
|
|
|
244
|
+
def setup_git_identity(container_name, name:, email:)
|
|
245
|
+
@docker.exec(
|
|
246
|
+
container_name, GIT_IDENTITY_SETUP,
|
|
247
|
+
user: @config.ssh_user,
|
|
248
|
+
env: {
|
|
249
|
+
"GDKBOX_GIT_NAME" => name.to_s,
|
|
250
|
+
"GDKBOX_GIT_EMAIL" => email.to_s
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Hydrate the GitLab checkout, streaming git's progress lines to the
|
|
256
|
+
# block. trees: true fetches trees only (blobs stay lazy).
|
|
257
|
+
def hydrate(container_name, trees: false, &block)
|
|
258
|
+
@docker.exec_stream(
|
|
259
|
+
container_name, HYDRATE,
|
|
260
|
+
user: @config.ssh_user,
|
|
261
|
+
workdir: @config.gitlab_checkout_path,
|
|
262
|
+
env: { "GDKBOX_HYDRATE_FILTER" => (trees ? "blob:none" : "") },
|
|
263
|
+
&block
|
|
264
|
+
)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def setup_git_ssh(container_name, host)
|
|
268
|
+
@docker.exec(
|
|
269
|
+
container_name, GIT_SSH_SETUP,
|
|
270
|
+
user: @config.ssh_user,
|
|
271
|
+
env: { "GDKBOX_GIT_HOST" => host }
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def setup_gitlab_remote(container_name, url)
|
|
276
|
+
@docker.exec(
|
|
277
|
+
container_name, GITLAB_REMOTE_SETUP,
|
|
278
|
+
user: @config.ssh_user,
|
|
279
|
+
workdir: @config.gitlab_checkout_path,
|
|
280
|
+
env: { "GDKBOX_GITLAB_REMOTE" => url }
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
171
284
|
def setup_api_key(container_name, api_key, key_env)
|
|
172
285
|
@docker.exec(
|
|
173
286
|
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