gdkbox 0.1.10 → 0.1.13
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 +18 -0
- data/lib/gdkbox/box.rb +74 -3
- data/lib/gdkbox/cli.rb +41 -4
- data/lib/gdkbox/completion.rb +2 -2
- data/lib/gdkbox/provisioner.rb +39 -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: 8df6faa5d2c2a7a0fe888195af92305774354b997ef5e0e2d1a7ee61469b60bf
|
|
4
|
+
data.tar.gz: 4c859a064721f68cdadc7a7140dc73b0d3a179a57fe3b538c63699c021115109
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ebd61583a5e01145206ba5407e2fbaad1cf456bbfbb040ecc58e66d0d471c3a5566fca4d194467f9dcc988f5540f57f54c73edf96ac5805248a3c34996f4095
|
|
7
|
+
data.tar.gz: fd01809df5f90d1eff5229918f424c311577f15a756ea32328157ebdcaf237e5d1c998a1db87d004a07dbc77be413c4260cc17baaa9db58248f5087cd3488467
|
data/README.md
CHANGED
|
@@ -92,6 +92,7 @@ Or run it straight from the checkout without installing:
|
|
|
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
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
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). |
|
|
95
96
|
| `gdkbox start NAME` | Start a stopped box (and re-enable SSH). |
|
|
96
97
|
| `gdkbox stop NAME` | Stop a running box. |
|
|
97
98
|
| `gdkbox rm NAME` | Remove a box: container, metadata, and SSH entry. |
|
|
@@ -136,6 +137,23 @@ gdkbox stop demo
|
|
|
136
137
|
gdkbox rm demo
|
|
137
138
|
```
|
|
138
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
|
+
|
|
139
157
|
## Orchestrating a fleet of agents
|
|
140
158
|
|
|
141
159
|
The end goal of `gdkbox` is to back an **orchestrator** that runs a pool of
|
data/lib/gdkbox/box.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "shellwords"
|
|
3
4
|
require "time"
|
|
4
5
|
|
|
5
6
|
module GDKBox
|
|
@@ -184,15 +185,85 @@ module GDKBox
|
|
|
184
185
|
"git@gitlab.com:#{token}.git"
|
|
185
186
|
end
|
|
186
187
|
|
|
188
|
+
# The host an SSH-style git URL connects to, or nil for non-SSH remotes
|
|
189
|
+
# (https needs no SSH client preparation).
|
|
190
|
+
def self.remote_ssh_host(url)
|
|
191
|
+
if (m = url.match(%r{\Assh://(?:[^@/]+@)?([^:/]+)}))
|
|
192
|
+
m[1]
|
|
193
|
+
elsif (m = url.match(/\A(?:[^@:\/]+@)([^:]+):/))
|
|
194
|
+
m[1]
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
187
198
|
# Point the box's GitLab checkout at a different remote (URL or
|
|
188
|
-
# "namespace/project").
|
|
199
|
+
# "namespace/project"). SSH remotes get the box's SSH client prepared
|
|
200
|
+
# first (host key acceptance + connection multiplexing) so the follow-up
|
|
201
|
+
# fetch — and later lazy fetches from the treeless clone — succeed.
|
|
202
|
+
# Returns the URL that origin now uses.
|
|
189
203
|
def set_gitlab_remote!(remote)
|
|
190
204
|
url = self.class.expand_remote(remote)
|
|
191
|
-
Provisioner.new(docker: @docker, config: @config)
|
|
192
|
-
|
|
205
|
+
provisioner = Provisioner.new(docker: @docker, config: @config)
|
|
206
|
+
if (host = self.class.remote_ssh_host(url))
|
|
207
|
+
provisioner.setup_git_ssh(container_name, host)
|
|
208
|
+
end
|
|
209
|
+
provisioner.setup_gitlab_remote(container_name, url)
|
|
193
210
|
url
|
|
194
211
|
end
|
|
195
212
|
|
|
213
|
+
# The hydrate fetch, run inside the box. The filter is passed to `git
|
|
214
|
+
# fetch` directly and only persisted to config after the fetch succeeds
|
|
215
|
+
# (and, for a full hydrate, the previous filter is put back on failure),
|
|
216
|
+
# so an interrupted hydrate cannot leave the config claiming objects the
|
|
217
|
+
# store does not have (issue #8).
|
|
218
|
+
HYDRATE_SCRIPT = <<~'BASH'
|
|
219
|
+
set -e
|
|
220
|
+
if [ -n "$GDKBOX_HYDRATE_FILTER" ]; then
|
|
221
|
+
git fetch --refetch --progress --filter="$GDKBOX_HYDRATE_FILTER" origin
|
|
222
|
+
git config remote.origin.partialclonefilter "$GDKBOX_HYDRATE_FILTER"
|
|
223
|
+
else
|
|
224
|
+
prev=$(git config --get remote.origin.partialclonefilter || true)
|
|
225
|
+
git config --unset-all remote.origin.partialclonefilter || true
|
|
226
|
+
if ! git fetch --refetch --progress origin; then
|
|
227
|
+
if [ -n "$prev" ]; then git config remote.origin.partialclonefilter "$prev"; fi
|
|
228
|
+
echo "hydrate: fetch failed; previous filter config restored" >&2
|
|
229
|
+
exit 1
|
|
230
|
+
fi
|
|
231
|
+
git config --unset remote.origin.promisor || true
|
|
232
|
+
fi
|
|
233
|
+
BASH
|
|
234
|
+
|
|
235
|
+
# Backfill the box's treeless GitLab clone so deep-history operations
|
|
236
|
+
# (old rebases, blame, bisect) work. trees: true fetches only the missing
|
|
237
|
+
# trees (much smaller; file contents stay lazy).
|
|
238
|
+
#
|
|
239
|
+
# The fetch runs over SSH (`ssh -F <generated config> <alias>`), not
|
|
240
|
+
# `docker exec`: the remote is typically git@gitlab.com, and only an SSH
|
|
241
|
+
# session carries the user's forwarded agent — docker exec has no
|
|
242
|
+
# SSH_AUTH_SOCK, so it cannot authenticate at all (issue #8). The SSH
|
|
243
|
+
# client stanza (host key + multiplexing) is seeded first for SSH
|
|
244
|
+
# origins. Streams git's progress lines to the block; raises
|
|
245
|
+
# CommandError when the fetch fails.
|
|
246
|
+
def hydrate!(trees: false, &progress)
|
|
247
|
+
origin = @docker.exec(
|
|
248
|
+
container_name, "git remote get-url origin",
|
|
249
|
+
user: @config.ssh_user, workdir: @config.gitlab_checkout_path
|
|
250
|
+
).stdout.strip
|
|
251
|
+
if (host = self.class.remote_ssh_host(origin))
|
|
252
|
+
Provisioner.new(docker: @docker, config: @config)
|
|
253
|
+
.setup_git_ssh(container_name, host)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
filter = trees ? "blob:none" : ""
|
|
257
|
+
remote_cmd = "cd #{Shellwords.escape(@config.gitlab_checkout_path)} && " \
|
|
258
|
+
"GDKBOX_HYDRATE_FILTER=#{Shellwords.escape(filter)} " \
|
|
259
|
+
"bash -c #{Shellwords.escape(HYDRATE_SCRIPT)}"
|
|
260
|
+
argv = ["ssh", "-F", @config.ssh_config_path, ssh_host_alias, remote_cmd]
|
|
261
|
+
result = @shell.stream_tty(*argv, &progress)
|
|
262
|
+
raise CommandError.new(argv, result.status, "") unless result.success?
|
|
263
|
+
|
|
264
|
+
result
|
|
265
|
+
end
|
|
266
|
+
|
|
196
267
|
# Seed a git identity into the box so `git commit` works there. Falls back
|
|
197
268
|
# to the host's `git config` when name/email are not given; raises when
|
|
198
269
|
# neither source has anything to seed. Returns the [name, email] seeded.
|
data/lib/gdkbox/cli.rb
CHANGED
|
@@ -288,6 +288,42 @@ module GDKBox
|
|
|
288
288
|
end
|
|
289
289
|
map "set-remote" => :set_remote
|
|
290
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
|
+
|
|
291
327
|
desc "set-host", "Add (default) or remove the gdk.local entry in /etc/hosts"
|
|
292
328
|
long_desc <<~DESC
|
|
293
329
|
GDK generates URLs and redirects that use the `gdk.local` hostname, so
|
|
@@ -475,10 +511,11 @@ module GDKBox
|
|
|
475
511
|
url = box.set_gitlab_remote!(remote)
|
|
476
512
|
say "Pointed the GitLab checkout at #{url}.", :green unless options[:json]
|
|
477
513
|
rescue StandardError => e
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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}"
|
|
482
519
|
end
|
|
483
520
|
|
|
484
521
|
# Ensure gdk.local resolves on the host so the box's web UI is reachable
|
data/lib/gdkbox/completion.rb
CHANGED
|
@@ -11,8 +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 set-git set-remote
|
|
15
|
-
stop rm add-skill].freeze
|
|
14
|
+
BOX_COMMANDS = %w[status dispatch ssh code install-agent set-key set-git set-remote hydrate
|
|
15
|
+
start stop rm add-skill].freeze
|
|
16
16
|
|
|
17
17
|
def initialize(cli_class = CLI)
|
|
18
18
|
@cli = cli_class
|
data/lib/gdkbox/provisioner.rb
CHANGED
|
@@ -141,6 +141,37 @@ module GDKBox
|
|
|
141
141
|
true
|
|
142
142
|
BASH
|
|
143
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
|
+
|
|
144
175
|
# Points the GitLab checkout's origin at a different repository. The image
|
|
145
176
|
# hardcodes the community mirror (gitlab-community/gitlab-org/gitlab);
|
|
146
177
|
# team members typically want the canonical gitlab-org/gitlab. The mirror
|
|
@@ -200,6 +231,14 @@ module GDKBox
|
|
|
200
231
|
)
|
|
201
232
|
end
|
|
202
233
|
|
|
234
|
+
def setup_git_ssh(container_name, host)
|
|
235
|
+
@docker.exec(
|
|
236
|
+
container_name, GIT_SSH_SETUP,
|
|
237
|
+
user: @config.ssh_user,
|
|
238
|
+
env: { "GDKBOX_GIT_HOST" => host }
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
|
|
203
242
|
def setup_gitlab_remote(container_name, url)
|
|
204
243
|
@docker.exec(
|
|
205
244
|
container_name, GITLAB_REMOTE_SETUP,
|
data/lib/gdkbox/version.rb
CHANGED