bsdkrun 0.3.2 → 0.4.0
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 +93 -21
- data/lib/bsdkrun/args.rb +13 -0
- data/lib/bsdkrun/cache.rb +112 -0
- data/lib/bsdkrun/client.rb +1 -0
- data/lib/bsdkrun/errors.rb +13 -0
- data/lib/bsdkrun/filesystem.rb +105 -0
- data/lib/bsdkrun/process.rb +10 -2
- data/lib/bsdkrun/sandbox.rb +15 -3
- data/lib/bsdkrun/version.rb +1 -1
- data/lib/bsdkrun.rb +5 -3
- 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: e78b89f68872f94abe500dba9bc0d69bd2b8ff0488745bb1f7e07ef2658f8a28
|
|
4
|
+
data.tar.gz: 1effefa03711b90030219d9456edbb6df361591184aa9bb7f01c06e87ad582e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce3f123068f4c08775e7308513b727182ece1ce14737de2cc874ed050be8e19979e82c902703202f72a162222d0b12363a6aefc5b9441eeab34038976ed28a1c
|
|
7
|
+
data.tar.gz: 8eb660e5db46bf1fc617b250f76c55f41f75943f2bc71026994790e812c000ae90aa63384956938e7a5bda406d8f59912ab36ac7f5a5029a615738a1ccb4dca3
|
data/README.md
CHANGED
|
@@ -9,14 +9,14 @@ dependencies** — just the Ruby standard library (`open3`, `json`, `pathname`).
|
|
|
9
9
|
```ruby
|
|
10
10
|
require "bsdkrun"
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
sbx = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
|
|
13
13
|
|
|
14
14
|
# exec argv directly, with env / stdin / a PTY / a working dir:
|
|
15
|
-
puts
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
puts sbx.exec(["uname", "-a"]).text
|
|
16
|
+
sbx.exec(["apk", "add", "curl"], throw_on_error: true)
|
|
17
|
+
sbx.run_command("curl", ["-fsSL", "https://example.com"])
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
sbx.stop
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Install
|
|
@@ -77,15 +77,34 @@ Bsdkrun::Sandbox.create(os: "kernel", kernel: "netbsd", format: "elf", disk: "ro
|
|
|
77
77
|
|
|
78
78
|
Every `create` runs the machine **detached** and returns a `Sandbox` handle.
|
|
79
79
|
|
|
80
|
+
### Environment variables
|
|
81
|
+
|
|
82
|
+
`env:` sets the guest environment for the machine's entrypoint. It is merged
|
|
83
|
+
over the image's own config, so a key the image already defines is replaced
|
|
84
|
+
rather than duplicated.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
sbx = Bsdkrun::Sandbox.create(
|
|
88
|
+
os: "linux",
|
|
89
|
+
image: "node:22",
|
|
90
|
+
env: { "NODE_ENV" => "production", "PORT" => "3000" },
|
|
91
|
+
command: ["node", "server.js"]
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Linux guests only — BSD guests boot their own init, so there is no generated
|
|
96
|
+
init to export into; set those from `exec` after boot. For a single command
|
|
97
|
+
rather than the whole machine, `exec` takes its own `env:`.
|
|
98
|
+
|
|
80
99
|
## Running commands
|
|
81
100
|
|
|
82
101
|
`exec` is the primary programmatic entrypoint. No shell parsing — pass an argv
|
|
83
102
|
array (or a program name plus `args:`).
|
|
84
103
|
|
|
85
104
|
```ruby
|
|
86
|
-
|
|
105
|
+
sbx.exec(["ls", "-la", "/etc"])
|
|
87
106
|
|
|
88
|
-
|
|
107
|
+
sbx.exec("ruby",
|
|
89
108
|
args: ["-e", "puts ENV['X']"],
|
|
90
109
|
env: { "X" => "hi" },
|
|
91
110
|
cwd: "/app",
|
|
@@ -96,7 +115,7 @@ box.exec("ruby",
|
|
|
96
115
|
throw_on_error: true) # raise on non-zero exit (default: false)
|
|
97
116
|
|
|
98
117
|
# Vercel-Sandbox-style alias:
|
|
99
|
-
result =
|
|
118
|
+
result = sbx.run_command("uname", ["-a"])
|
|
100
119
|
result.stdout # raw stdout
|
|
101
120
|
result.text # stdout, trailing newlines trimmed
|
|
102
121
|
result.exit_code
|
|
@@ -111,21 +130,74 @@ The callbacks run as chunks arrive, and the same bytes remain buffered in the
|
|
|
111
130
|
returned result. They do not require `tty`; a PTY changes command semantics and
|
|
112
131
|
may merge stderr into stdout.
|
|
113
132
|
|
|
133
|
+
## Caching
|
|
134
|
+
|
|
135
|
+
`sbx.cache` saves a guest directory under a key and restores it later, so a
|
|
136
|
+
rebuild can pick up where the last one left off. **A miss is not an error** —
|
|
137
|
+
check `restored` rather than rescuing.
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
key = "deps-#{lock_hash}"
|
|
141
|
+
hit = sbx.cache.restore(key: key, restore_keys: ["deps-"])
|
|
142
|
+
unless hit.restored
|
|
143
|
+
sbx.exec(["npm", "ci"])
|
|
144
|
+
sbx.cache.save("/app/node_modules", key: key, compression: "zstd")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
Bsdkrun::Caches.ls # every stored entry, newest first
|
|
148
|
+
Bsdkrun::Caches.rm([key]) # or Bsdkrun::Caches.rm(all: true)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`restore_keys` are prefixes tried in order when the exact key misses; within a
|
|
152
|
+
prefix the newest matching entry wins, and `hit.key` says which one was used.
|
|
153
|
+
Formats are `gzip` (default), `zstd`, `estargz` and `none`.
|
|
154
|
+
|
|
155
|
+
Where entries live is host configuration, not an SDK concern: the default is
|
|
156
|
+
this host's disk, and `BSDKRUN_CACHE_BACKEND=s3` + `BSDKRUN_CACHE_S3_*` (or
|
|
157
|
+
`~/.config/bsdkrun/cache.toml`) points them at a bucket instead.
|
|
158
|
+
|
|
159
|
+
## Files
|
|
160
|
+
|
|
161
|
+
`sbx.fs` reads and writes files in the guest. Parent directories are created
|
|
162
|
+
for you, and everything is byte-exact — `read_file` returns a binary string.
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
sbx.fs.write_file("/app/main.py", "print('hi')")
|
|
166
|
+
sbx.fs.write_file("/app/logo.png", png_bytes)
|
|
167
|
+
|
|
168
|
+
text = sbx.fs.read_text("/app/out.json")
|
|
169
|
+
bytes = sbx.fs.read_file("/app/logo.png")
|
|
170
|
+
|
|
171
|
+
sbx.fs.upload("./src", "/app/src") # file or directory
|
|
172
|
+
sbx.fs.download("/app/dist", "./dist", recursive: true)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`upload` looks at the local path to decide whether to recurse; `download` cannot
|
|
176
|
+
(the path is in the guest), so say so for a directory. A directory's *contents*
|
|
177
|
+
land in the destination: uploading `./src` to `/app/src` leaves the guest's
|
|
178
|
+
`/app/src` holding what `./src` holds.
|
|
179
|
+
|
|
180
|
+
Failures raise `Bsdkrun::FileTransferFailed`, which carries the offending `path`.
|
|
181
|
+
|
|
182
|
+
> Transfers ride the same in-guest agent as `exec`, so the sandbox must be
|
|
183
|
+
> running. A directory copy also needs `tar` in the guest; single files need
|
|
184
|
+
> only the shell every bootable image already has.
|
|
185
|
+
|
|
114
186
|
## Lifecycle & inventory
|
|
115
187
|
|
|
116
188
|
```ruby
|
|
117
|
-
|
|
118
|
-
same = Bsdkrun::Sandbox.get(
|
|
189
|
+
sbx = Bsdkrun::Sandbox.create(os: "linux", image: "alpine", command: ["sleep", "300"])
|
|
190
|
+
same = Bsdkrun::Sandbox.get(sbx.id) # reconnect (prefix ok)
|
|
119
191
|
all = Bsdkrun::Sandbox.list(all: true) # Array<SandboxInfo>
|
|
120
192
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
193
|
+
sbx.status # SandboxInfo | nil
|
|
194
|
+
sbx.running? # true / false
|
|
195
|
+
sbx.logs # console log (String)
|
|
196
|
+
sbx.shell # interactive shell (inherits the terminal)
|
|
197
|
+
sbx.stop # BSD guests clean-poweroff; Linux SIGTERM
|
|
198
|
+
sbx.start # restart in place — resumes its own disk/rootfs (data persists)
|
|
199
|
+
sbx.update(cpus: 4, mem: 2048) # applies on next start
|
|
200
|
+
sbx.remove(force: true)
|
|
129
201
|
```
|
|
130
202
|
|
|
131
203
|
Host-level namespaces:
|
|
@@ -147,11 +219,11 @@ Bsdkrun::System.versions("netbsd") # Array<String>
|
|
|
147
219
|
Bsdkrun::Sandbox.create(os: "linux", image: "alpine", net: { ports: ["2222:22"] })
|
|
148
220
|
|
|
149
221
|
# agent-managed key-based SSH
|
|
150
|
-
|
|
151
|
-
|
|
222
|
+
sbx.ssh_setup # install local ~/.ssh/*.pub keys
|
|
223
|
+
sbx.ssh_setup(user: "tsiry", key: "~/.ssh/work.pub")
|
|
152
224
|
|
|
153
225
|
# put a guest on your tailnet
|
|
154
|
-
|
|
226
|
+
sbx.tailscale_up(authkey: "tskey-auth-...", hostname: "web")
|
|
155
227
|
```
|
|
156
228
|
|
|
157
229
|
### Global networks — reach machines by name
|
data/lib/bsdkrun/args.rb
CHANGED
|
@@ -28,6 +28,17 @@ module Bsdkrun
|
|
|
28
28
|
# +--name+ flag if a name is set.
|
|
29
29
|
# @param o [Hash]
|
|
30
30
|
# @return [Array<String>]
|
|
31
|
+
# +-e K=V+ per entry, sorted by key.
|
|
32
|
+
#
|
|
33
|
+
# A hash has no argv order of its own, so sorting is what makes the command
|
|
34
|
+
# line — and the tests that assert on it — deterministic. The guest sees the
|
|
35
|
+
# same environment either way.
|
|
36
|
+
def env_args(env)
|
|
37
|
+
return [] if env.nil? || env.empty?
|
|
38
|
+
|
|
39
|
+
env.to_h.sort_by { |k, _| k.to_s }.flat_map { |k, v| ["-e", "#{k}=#{v}"] }
|
|
40
|
+
end
|
|
41
|
+
|
|
31
42
|
def name_args(o)
|
|
32
43
|
o[:name] ? ["--name", o[:name].to_s] : []
|
|
33
44
|
end
|
|
@@ -81,7 +92,9 @@ module Bsdkrun
|
|
|
81
92
|
a.push("--initramfs") if opts[:initramfs]
|
|
82
93
|
a.push("-v", opts[:volume]) if opts[:volume]
|
|
83
94
|
Array(opts[:mounts]).each { |m| a.push("--mount", m) }
|
|
95
|
+
Array(opts[:attach_disk]).each { |d| a.push("--attach-disk", d) }
|
|
84
96
|
a.push("--entrypoint", opts[:entrypoint]) if opts[:entrypoint]
|
|
97
|
+
a.concat(env_args(opts[:env]))
|
|
85
98
|
a.push("--console", opts[:console]) if opts[:console]
|
|
86
99
|
a.concat(net_args(opts[:net])).concat(name_args(opts)).concat(vm_args(opts))
|
|
87
100
|
cmd = opts[:command]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Bsdkrun
|
|
6
|
+
# A stored cache entry, as +cache ls+ reports it.
|
|
7
|
+
CacheEntry = Struct.new(:key, :path, :compression, :size, :created, :digest, keyword_init: true) do
|
|
8
|
+
def self.from(row)
|
|
9
|
+
new(
|
|
10
|
+
key: row["key"].to_s,
|
|
11
|
+
path: row["path"].to_s,
|
|
12
|
+
compression: row["compression"].to_s,
|
|
13
|
+
size: row["size"].to_i,
|
|
14
|
+
created: row["created"].to_i,
|
|
15
|
+
digest: row["digest"].to_s
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# What a restore did. A miss is not an error — check +restored+.
|
|
21
|
+
RestoreResult = Struct.new(
|
|
22
|
+
:restored, :requested_key, :key, :path, :size, :compression, :created,
|
|
23
|
+
keyword_init: true
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Save and restore guest directories under a key, reached as {Sandbox#cache}.
|
|
27
|
+
#
|
|
28
|
+
# Entries are keyed, so a rebuild can pick up where the last one left off:
|
|
29
|
+
#
|
|
30
|
+
# hit = sbx.cache.restore(key: key, restore_keys: ["deps-"])
|
|
31
|
+
# unless hit.restored
|
|
32
|
+
# sbx.exec(["npm", "ci"])
|
|
33
|
+
# sbx.cache.save("/app/node_modules", key: key)
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# Where entries live — host disk or S3 — is host configuration, not an SDK
|
|
37
|
+
# concern: set +BSDKRUN_CACHE_BACKEND+ / +BSDKRUN_CACHE_S3_*+, or write
|
|
38
|
+
# +~/.config/bsdkrun/cache.toml+.
|
|
39
|
+
class Cache
|
|
40
|
+
# @param id [String] the machine's id.
|
|
41
|
+
def initialize(id)
|
|
42
|
+
@id = id
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Archive the guest directory at +path+ under +key+.
|
|
46
|
+
#
|
|
47
|
+
# @param path [String] absolute path in the guest.
|
|
48
|
+
# @param key [String] key to store under.
|
|
49
|
+
# @param compression [String] gzip (default), zstd, estargz or none.
|
|
50
|
+
# @param force [Boolean] replace an entry that already has this key.
|
|
51
|
+
# @return [CacheEntry]
|
|
52
|
+
def save(path, key:, compression: "gzip", force: false)
|
|
53
|
+
args = ["cache", "save", "#{@id}:#{path}", "--key", key, "--json"]
|
|
54
|
+
args += ["--compression", compression] unless compression == "gzip"
|
|
55
|
+
args << "--force" if force
|
|
56
|
+
CacheEntry.from(json(args, "bsdkrun cache save"))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Restore a stored tree.
|
|
60
|
+
#
|
|
61
|
+
# @param key [String]
|
|
62
|
+
# @param path [String, nil] defaults to where the entry was saved from.
|
|
63
|
+
# @param restore_keys [Array<String>] prefixes tried in order on a miss.
|
|
64
|
+
# @return [RestoreResult]
|
|
65
|
+
def restore(key:, path: nil, restore_keys: [])
|
|
66
|
+
target = path ? "#{@id}:#{path}" : @id
|
|
67
|
+
args = ["cache", "restore", target, "--key", key, "--json"]
|
|
68
|
+
args += ["--restore-keys", *restore_keys] unless restore_keys.empty?
|
|
69
|
+
row = json(args, "bsdkrun cache restore")
|
|
70
|
+
RestoreResult.new(
|
|
71
|
+
restored: !!row["restored"],
|
|
72
|
+
requested_key: row["requested_key"].to_s,
|
|
73
|
+
key: row["key"],
|
|
74
|
+
path: row["path"],
|
|
75
|
+
size: row["size"],
|
|
76
|
+
compression: row["compression"],
|
|
77
|
+
created: row["created"]
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def json(args, label)
|
|
84
|
+
out = Process.run!(args, label: label).stdout
|
|
85
|
+
JSON.parse(out.strip.empty? ? "{}" : out)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Host-level cache operations, mirroring {Bsdkrun.volumes}.
|
|
90
|
+
module Caches
|
|
91
|
+
module_function
|
|
92
|
+
|
|
93
|
+
# @return [Array<CacheEntry>] every stored entry, newest first.
|
|
94
|
+
def ls
|
|
95
|
+
out = Process.run!(["cache", "ls", "--json"], label: "bsdkrun cache ls").stdout
|
|
96
|
+
JSON.parse(out.strip.empty? ? "[]" : out).map { |row| CacheEntry.from(row) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Remove entries by key, or every one with <tt>all: true</tt>.
|
|
100
|
+
# @return [void]
|
|
101
|
+
def rm(keys = [], all: false)
|
|
102
|
+
args = ["cache", "rm"]
|
|
103
|
+
if all
|
|
104
|
+
args << "--all"
|
|
105
|
+
else
|
|
106
|
+
args += Array(keys)
|
|
107
|
+
end
|
|
108
|
+
Process.run!(args, label: "bsdkrun cache rm")
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/lib/bsdkrun/client.rb
CHANGED
data/lib/bsdkrun/errors.rb
CHANGED
|
@@ -50,6 +50,19 @@ module Bsdkrun
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# A guest filesystem operation was refused (see {FileSystem}).
|
|
54
|
+
class FileTransferFailed < Error
|
|
55
|
+
# @return [String] the path that could not be transferred.
|
|
56
|
+
attr_reader :path
|
|
57
|
+
|
|
58
|
+
# @param message [String]
|
|
59
|
+
# @param path [String]
|
|
60
|
+
def initialize(message, path)
|
|
61
|
+
@path = path
|
|
62
|
+
super(message)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
53
66
|
# A GraphQL request to a remote +bsdkrund+ daemon failed — a transport
|
|
54
67
|
# failure, a non-JSON response, or a +body["errors"]+ entry that was not an
|
|
55
68
|
# auth failure. Mirrors +web/src/lib/graphql.ts+'s +GraphQLError+.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bsdkrun
|
|
4
|
+
# Files in a running sandbox, reached as {Sandbox#fs}.
|
|
5
|
+
#
|
|
6
|
+
# Every call goes through the guest's exec agent, so the sandbox has to be
|
|
7
|
+
# running — there is no offline write.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# sbx.fs.write_file("/app/main.py", "print('hi')")
|
|
11
|
+
# sbx.fs.read_text("/app/out.json")
|
|
12
|
+
# sbx.fs.upload("./src", "/app/src")
|
|
13
|
+
# sbx.fs.download("/app/dist", "./dist", recursive: true)
|
|
14
|
+
class FileSystem
|
|
15
|
+
# @param id [String] the machine's id.
|
|
16
|
+
def initialize(id)
|
|
17
|
+
@id = id
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Write +data+ to +path+ in the guest, creating parent directories.
|
|
21
|
+
#
|
|
22
|
+
# @param path [String] absolute path in the guest.
|
|
23
|
+
# @param data [String] text or binary content.
|
|
24
|
+
# @return [void]
|
|
25
|
+
# @raise [FileTransferFailed]
|
|
26
|
+
def write_file(path, data)
|
|
27
|
+
res = Process.run(["cp", "-", "#{@id}:#{path}"], stdin: data, binary: true)
|
|
28
|
+
check!(res, path)
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Read +path+ from the guest as bytes (ASCII-8BIT).
|
|
33
|
+
#
|
|
34
|
+
# @param path [String]
|
|
35
|
+
# @return [String] binary string.
|
|
36
|
+
# @raise [FileTransferFailed]
|
|
37
|
+
def read_file(path)
|
|
38
|
+
res = Process.run(["cp", "#{@id}:#{path}", "-"], binary: true)
|
|
39
|
+
check!(res, path)
|
|
40
|
+
res.stdout
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Read +path+ from the guest and tag it with +encoding+.
|
|
44
|
+
#
|
|
45
|
+
# @param path [String]
|
|
46
|
+
# @param encoding [String]
|
|
47
|
+
# @return [String]
|
|
48
|
+
def read_text(path, encoding: "UTF-8")
|
|
49
|
+
read_file(path).force_encoding(encoding)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Copy a host file or directory into the guest.
|
|
53
|
+
#
|
|
54
|
+
# A directory's *contents* land in +remote_path+, so
|
|
55
|
+
# <tt>upload("./src", "/app/src")</tt> leaves the guest's +/app/src+ holding
|
|
56
|
+
# what +./src+ holds. Whether it recurses is decided by looking at the local
|
|
57
|
+
# path, so callers do not have to say which kind of thing it is.
|
|
58
|
+
#
|
|
59
|
+
# @param local_path [String]
|
|
60
|
+
# @param remote_path [String]
|
|
61
|
+
# @return [void]
|
|
62
|
+
# @raise [FileTransferFailed]
|
|
63
|
+
def upload(local_path, remote_path)
|
|
64
|
+
unless File.exist?(local_path)
|
|
65
|
+
raise FileTransferFailed.new("cannot upload #{local_path}: no such file or directory",
|
|
66
|
+
local_path)
|
|
67
|
+
end
|
|
68
|
+
args = ["cp"]
|
|
69
|
+
args << "-r" if File.directory?(local_path)
|
|
70
|
+
args += [local_path.to_s, "#{@id}:#{remote_path}"]
|
|
71
|
+
check!(Process.run(args), local_path)
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Copy a file or directory out of the guest onto the host.
|
|
76
|
+
#
|
|
77
|
+
# Pass <tt>recursive: true</tt> for a directory; unlike {#upload} it cannot
|
|
78
|
+
# be detected here, because the path lives in the guest and answering would
|
|
79
|
+
# cost an extra round trip.
|
|
80
|
+
#
|
|
81
|
+
# @param remote_path [String]
|
|
82
|
+
# @param local_path [String]
|
|
83
|
+
# @param recursive [Boolean]
|
|
84
|
+
# @return [void]
|
|
85
|
+
# @raise [FileTransferFailed]
|
|
86
|
+
def download(remote_path, local_path, recursive: false)
|
|
87
|
+
args = ["cp"]
|
|
88
|
+
args << "-r" if recursive
|
|
89
|
+
args += ["#{@id}:#{remote_path}", local_path.to_s]
|
|
90
|
+
check!(Process.run(args), remote_path)
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def check!(res, path)
|
|
97
|
+
return if res.exit_code.zero?
|
|
98
|
+
|
|
99
|
+
# The CLI already explains these well; strip its "Error: " prefix.
|
|
100
|
+
text = res.stderr.to_s.strip.sub(/\AError:\s*/, "")
|
|
101
|
+
text = "file transfer failed for #{path}" if text.empty?
|
|
102
|
+
raise FileTransferFailed.new(text, path)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/bsdkrun/process.rb
CHANGED
|
@@ -26,15 +26,23 @@ module Bsdkrun
|
|
|
26
26
|
# @param env [Hash] extra environment merged onto the process env.
|
|
27
27
|
# @param stdin [String, nil] data piped to the child's stdin.
|
|
28
28
|
# @param log_level [Integer] bsdkrun global log level (0=off .. 5=trace).
|
|
29
|
+
# @param binary [Boolean] keep stdout as bytes (ASCII-8BIT) instead of text.
|
|
30
|
+
# Needed by {FileSystem#read_file}: appending a chunk of arbitrary bytes to
|
|
31
|
+
# a UTF-8 buffer raises Encoding::CompatibilityError, so a PNG read back
|
|
32
|
+
# out of a guest would blow up mid-transfer rather than return.
|
|
29
33
|
# @return [RawResult]
|
|
30
|
-
def run(args, env: {}, stdin: nil, log_level: 0, on_stdout: nil, on_stderr: nil)
|
|
34
|
+
def run(args, env: {}, stdin: nil, log_level: 0, on_stdout: nil, on_stderr: nil, binary: false)
|
|
31
35
|
bin = Binary.resolve
|
|
32
36
|
full = ["--log-level", log_level.to_s, *args]
|
|
33
37
|
merged_env = env.to_h.transform_keys(&:to_s).transform_values(&:to_s)
|
|
34
|
-
out = +""
|
|
38
|
+
out = binary ? (+"").b : +""
|
|
35
39
|
err = +""
|
|
36
40
|
status = nil
|
|
37
41
|
Open3.popen3(merged_env, bin, *full) do |child_in, child_out, child_err, wait|
|
|
42
|
+
if binary
|
|
43
|
+
child_in.binmode
|
|
44
|
+
child_out.binmode
|
|
45
|
+
end
|
|
38
46
|
writer = Thread.new { child_in.write(stdin) if stdin; child_in.close }
|
|
39
47
|
stdout_reader = Thread.new do
|
|
40
48
|
while (chunk = child_out.readpartial(8192) rescue nil)
|
data/lib/bsdkrun/sandbox.rb
CHANGED
|
@@ -9,9 +9,9 @@ module Bsdkrun
|
|
|
9
9
|
# with {Sandbox.list}.
|
|
10
10
|
#
|
|
11
11
|
# @example
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# sbx = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
|
|
13
|
+
# sbx.exec(["uname", "-a"]).text
|
|
14
|
+
# sbx.stop
|
|
15
15
|
class Sandbox
|
|
16
16
|
ID_RE = /\A[0-9a-f]{6,}\z/
|
|
17
17
|
SSH_PORT_RE = /ssh -p (\d+)/
|
|
@@ -32,6 +32,18 @@ module Bsdkrun
|
|
|
32
32
|
@ssh_port = ssh_port
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Read and write files in the guest.
|
|
36
|
+
# @return [FileSystem]
|
|
37
|
+
def fs
|
|
38
|
+
@fs ||= FileSystem.new(@id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Save and restore guest directories under a key.
|
|
42
|
+
# @return [Cache]
|
|
43
|
+
def cache
|
|
44
|
+
@cache ||= Cache.new(@id)
|
|
45
|
+
end
|
|
46
|
+
|
|
35
47
|
class << self
|
|
36
48
|
# Boot a new microVM and return a handle to it.
|
|
37
49
|
#
|
data/lib/bsdkrun/version.rb
CHANGED
data/lib/bsdkrun.rb
CHANGED
|
@@ -5,6 +5,8 @@ require_relative "bsdkrun/errors"
|
|
|
5
5
|
require_relative "bsdkrun/binary"
|
|
6
6
|
require_relative "bsdkrun/process"
|
|
7
7
|
require_relative "bsdkrun/args"
|
|
8
|
+
require_relative "bsdkrun/cache"
|
|
9
|
+
require_relative "bsdkrun/filesystem"
|
|
8
10
|
require_relative "bsdkrun/types"
|
|
9
11
|
require_relative "bsdkrun/sandbox"
|
|
10
12
|
require_relative "bsdkrun/images"
|
|
@@ -23,9 +25,9 @@ require_relative "bsdkrun/client"
|
|
|
23
25
|
# @example
|
|
24
26
|
# require "bsdkrun"
|
|
25
27
|
#
|
|
26
|
-
#
|
|
27
|
-
# puts
|
|
28
|
-
#
|
|
28
|
+
# sbx = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
|
|
29
|
+
# puts sbx.exec(["uname", "-a"]).text
|
|
30
|
+
# sbx.stop
|
|
29
31
|
module Bsdkrun
|
|
30
32
|
class << self
|
|
31
33
|
# Force the SDK to use a specific +bsdkrun+ binary, bypassing discovery.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bsdkrun
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tsiry Sandratraina
|
|
@@ -51,8 +51,10 @@ files:
|
|
|
51
51
|
- lib/bsdkrun.rb
|
|
52
52
|
- lib/bsdkrun/args.rb
|
|
53
53
|
- lib/bsdkrun/binary.rb
|
|
54
|
+
- lib/bsdkrun/cache.rb
|
|
54
55
|
- lib/bsdkrun/client.rb
|
|
55
56
|
- lib/bsdkrun/errors.rb
|
|
57
|
+
- lib/bsdkrun/filesystem.rb
|
|
56
58
|
- lib/bsdkrun/images.rb
|
|
57
59
|
- lib/bsdkrun/networks.rb
|
|
58
60
|
- lib/bsdkrun/process.rb
|