gdkbox 0.1.10 → 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 +18 -0
- data/lib/gdkbox/box.rb +37 -3
- data/lib/gdkbox/cli.rb +41 -4
- data/lib/gdkbox/completion.rb +2 -2
- data/lib/gdkbox/docker.rb +15 -0
- data/lib/gdkbox/provisioner.rb +72 -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
|
@@ -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
|
@@ -184,15 +184,49 @@ module GDKBox
|
|
|
184
184
|
"git@gitlab.com:#{token}.git"
|
|
185
185
|
end
|
|
186
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
|
+
|
|
187
197
|
# Point the box's GitLab checkout at a different remote (URL or
|
|
188
|
-
# "namespace/project").
|
|
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.
|
|
189
202
|
def set_gitlab_remote!(remote)
|
|
190
203
|
url = self.class.expand_remote(remote)
|
|
191
|
-
Provisioner.new(docker: @docker, config: @config)
|
|
192
|
-
|
|
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)
|
|
193
209
|
url
|
|
194
210
|
end
|
|
195
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
|
+
|
|
196
230
|
# Seed a git identity into the box so `git commit` works there. Falls back
|
|
197
231
|
# to the host's `git config` when name/email are not given; raises when
|
|
198
232
|
# 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/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
|
@@ -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
|
|
@@ -152,6 +183,27 @@ module GDKBox
|
|
|
152
183
|
git fetch --quiet origin
|
|
153
184
|
BASH
|
|
154
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
|
+
|
|
155
207
|
def initialize(docker:, config:)
|
|
156
208
|
@docker = docker
|
|
157
209
|
@config = config
|
|
@@ -200,6 +252,26 @@ module GDKBox
|
|
|
200
252
|
)
|
|
201
253
|
end
|
|
202
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
|
+
|
|
203
275
|
def setup_gitlab_remote(container_name, url)
|
|
204
276
|
@docker.exec(
|
|
205
277
|
container_name, GITLAB_REMOTE_SETUP,
|
data/lib/gdkbox/version.rb
CHANGED