microsandbox-rb 0.9.2 → 0.10.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/CHANGELOG.md +81 -0
- data/Cargo.lock +82 -76
- data/README.md +53 -2
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/src/error.rs +6 -0
- data/ext/microsandbox/src/sandbox.rs +197 -4
- data/lib/microsandbox/modification.rb +108 -0
- data/lib/microsandbox/sandbox.rb +177 -1
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox.rb +1 -0
- data/sig/microsandbox.rbs +43 -1
- metadata +2 -1
data/lib/microsandbox/sandbox.rb
CHANGED
|
@@ -116,6 +116,30 @@ module Microsandbox
|
|
|
116
116
|
JSON.parse(@native.config_json)
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
# Health-check the guest agent without refreshing the idle timer. Connects
|
|
120
|
+
# to the running sandbox and sends `core.ping`; a stopped sandbox raises
|
|
121
|
+
# {SandboxNotRunningError} (it is not started implicitly). Mirrors the
|
|
122
|
+
# official SDKs' `ping`.
|
|
123
|
+
# @return [PingResult]
|
|
124
|
+
def ping
|
|
125
|
+
PingResult.new(@native.ping)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Explicitly refresh this sandbox's idle-activity timer. A stopped sandbox
|
|
129
|
+
# raises {SandboxNotRunningError}. Mirrors the official SDKs' `touch`.
|
|
130
|
+
# @return [TouchResult]
|
|
131
|
+
def touch
|
|
132
|
+
TouchResult.new(@native.touch)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Plan or apply a live modification of this sandbox. See {Sandbox#modify} for
|
|
136
|
+
# the full option set.
|
|
137
|
+
# @return [ModificationPlan]
|
|
138
|
+
def modify(**kwargs)
|
|
139
|
+
opts = Sandbox.send(:build_modify_opts, **kwargs)
|
|
140
|
+
ModificationPlan.new(JSON.parse(@native.modify(opts)))
|
|
141
|
+
end
|
|
142
|
+
|
|
119
143
|
# Snapshot this (stopped) sandbox under a bare name, resolved under the
|
|
120
144
|
# snapshots dir. Convenience equivalent of
|
|
121
145
|
# `Snapshot.create(name, name: <snapshot-name>)` addressed by this handle.
|
|
@@ -213,7 +237,12 @@ module Microsandbox
|
|
|
213
237
|
# @param name [String] sandbox name (max 128 UTF-8 bytes)
|
|
214
238
|
# @param image [String, nil] OCI image reference (e.g. "python")
|
|
215
239
|
# @param cpus [Integer, nil] number of vCPUs
|
|
240
|
+
# @param max_cpus [Integer, nil] boot-time maximum vCPU ceiling a later live
|
|
241
|
+
# {Sandbox#modify} can grow up to (defaults to `cpus`; must be >= `cpus`)
|
|
216
242
|
# @param memory [Integer, nil] memory in MiB
|
|
243
|
+
# @param max_memory [Integer, nil] boot-time maximum memory ceiling (MiB) a
|
|
244
|
+
# later live {Sandbox#modify} can grow up to (defaults to `memory`; must
|
|
245
|
+
# be >= `memory`)
|
|
217
246
|
# @param env [Hash, nil] environment variables
|
|
218
247
|
# @param workdir [String, nil] working directory inside the guest
|
|
219
248
|
# @param shell [String, nil] default shell (for {#shell})
|
|
@@ -334,7 +363,8 @@ module Microsandbox
|
|
|
334
363
|
|
|
335
364
|
# @api private
|
|
336
365
|
# Shared keyword-option builder for {create}/{create_with_progress}.
|
|
337
|
-
def build_create_opts(image: nil, cpus: nil,
|
|
366
|
+
def build_create_opts(image: nil, cpus: nil, max_cpus: nil, memory: nil, max_memory: nil,
|
|
367
|
+
env: nil, workdir: nil,
|
|
338
368
|
shell: nil, user: nil, hostname: nil, labels: nil, scripts: nil,
|
|
339
369
|
entrypoint: nil, ports: nil, ports_udp: nil, volumes: nil, network: nil,
|
|
340
370
|
dns: nil, tls: nil, ipv4_pool: nil, ipv6_pool: nil,
|
|
@@ -370,7 +400,9 @@ module Microsandbox
|
|
|
370
400
|
opts["from_snapshot"] = from_snapshot.to_s if from_snapshot
|
|
371
401
|
opts["fstype"] = fstype.to_s if fstype
|
|
372
402
|
opts["cpus"] = Integer(cpus) if cpus
|
|
403
|
+
opts["max_cpus"] = Integer(max_cpus) if max_cpus
|
|
373
404
|
opts["memory"] = Integer(memory) if memory
|
|
405
|
+
opts["max_memory"] = Integer(max_memory) if max_memory
|
|
374
406
|
opts["workdir"] = workdir.to_s if workdir
|
|
375
407
|
opts["shell"] = shell.to_s if shell
|
|
376
408
|
opts["user"] = user.to_s if user
|
|
@@ -522,6 +554,99 @@ module Microsandbox
|
|
|
522
554
|
Array(secrets).map { |spec| normalize_secret(spec) }
|
|
523
555
|
end
|
|
524
556
|
|
|
557
|
+
# @api private
|
|
558
|
+
# Shared keyword-option builder for {Sandbox#modify}/{SandboxHandle#modify}.
|
|
559
|
+
# Produces the string-keyed Hash the native `modify` reads. `policy` is
|
|
560
|
+
# always set (defaulting to "no_restart"); everything else is included only
|
|
561
|
+
# when provided so an unset option means "leave unchanged".
|
|
562
|
+
def build_modify_opts(cpus: nil, max_cpus: nil, memory: nil, max_memory: nil,
|
|
563
|
+
env: nil, remove_env: nil, labels: nil, remove_labels: nil, workdir: nil,
|
|
564
|
+
secrets: nil, remove_secrets: nil, policy: nil, dry_run: false)
|
|
565
|
+
opts = {}
|
|
566
|
+
opts["cpus"] = Integer(cpus) if cpus
|
|
567
|
+
opts["max_cpus"] = Integer(max_cpus) if max_cpus
|
|
568
|
+
opts["memory"] = Integer(memory) if memory
|
|
569
|
+
opts["max_memory"] = Integer(max_memory) if max_memory
|
|
570
|
+
opts["env"] = stringify(env) if env
|
|
571
|
+
opts["remove_env"] = Array(remove_env).map(&:to_s) if remove_env
|
|
572
|
+
opts["labels"] = stringify(labels) if labels
|
|
573
|
+
opts["remove_labels"] = Array(remove_labels).map(&:to_s) if remove_labels
|
|
574
|
+
opts["workdir"] = workdir.to_s if workdir
|
|
575
|
+
opts["secrets"] = normalize_modify_secrets(secrets) if secrets
|
|
576
|
+
opts["remove_secrets"] = Array(remove_secrets).map(&:to_s) if remove_secrets
|
|
577
|
+
opts["policy"] = normalize_modify_policy(policy)
|
|
578
|
+
opts["dry_run"] = true if dry_run
|
|
579
|
+
opts
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# Normalize the `secrets:` modify option — a Hash of `{ name => spec }` —
|
|
583
|
+
# into the native array of specs, sorted by name for deterministic patch
|
|
584
|
+
# (and plan) ordering. The modify secret vocabulary differs from create's
|
|
585
|
+
# (source-or-value with `env:`/`store:`/`value:` mutually exclusive, keyed
|
|
586
|
+
# by secret name), mirroring the upstream `SecretModifySpec`.
|
|
587
|
+
def normalize_modify_secrets(secrets)
|
|
588
|
+
unless secrets.is_a?(Hash)
|
|
589
|
+
raise ArgumentError, "secrets: must be a Hash of { name => spec } (got #{secrets.class})"
|
|
590
|
+
end
|
|
591
|
+
out = secrets.map { |name, spec| normalize_modify_secret(name, spec) }
|
|
592
|
+
.sort_by { |h| h["name"] }
|
|
593
|
+
# A Symbol and a String key stringify to the same secret name, yielding
|
|
594
|
+
# two same-name specs. Upstream's fluent API dedupes those; the patch
|
|
595
|
+
# path we drive does not, and duplicates make the live value and the
|
|
596
|
+
# persisted config diverge nondeterministically — reject them loudly.
|
|
597
|
+
dup = out.group_by { |h| h["name"] }.find { |_, specs| specs.size > 1 }
|
|
598
|
+
if dup
|
|
599
|
+
raise ArgumentError,
|
|
600
|
+
"secrets: duplicate entries for #{dup.first.inspect} (a Symbol and a String key stringify to the same name)"
|
|
601
|
+
end
|
|
602
|
+
out
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def normalize_modify_secret(name, spec)
|
|
606
|
+
spec ||= {}
|
|
607
|
+
unless spec.is_a?(Hash)
|
|
608
|
+
# Reject before any spec access: a non-Hash spec (e.g. the tempting
|
|
609
|
+
# `{ name => raw_value }` shorthand) must never reach a method call
|
|
610
|
+
# whose NoMethodError would embed the receiver — cleartext — in the
|
|
611
|
+
# message on Ruby < 3.3.
|
|
612
|
+
raise ArgumentError,
|
|
613
|
+
"secret #{name.to_s.inspect}: spec must be a Hash (e.g. { value: ... } or { env: ... }), got #{spec.class}"
|
|
614
|
+
end
|
|
615
|
+
env = fetch_opt(spec, :env)
|
|
616
|
+
value = fetch_opt(spec, :value)
|
|
617
|
+
store = fetch_opt(spec, :store)
|
|
618
|
+
present = {"env" => env, "value" => value, "store" => store}.reject { |_, v| v.nil? }
|
|
619
|
+
if present.size > 1
|
|
620
|
+
# Report only the conflicting KEYS, never the values — a secret spec's
|
|
621
|
+
# :value is cleartext and error messages routinely land in logs and
|
|
622
|
+
# error trackers. Mirrors normalize_secret above.
|
|
623
|
+
keys = present.keys.map(&:inspect).join(" and ")
|
|
624
|
+
raise ArgumentError,
|
|
625
|
+
"secret #{name.to_s.inspect}: #{keys} are mutually exclusive; set at most one"
|
|
626
|
+
end
|
|
627
|
+
out = {"name" => name.to_s}
|
|
628
|
+
out["env"] = env.to_s if env
|
|
629
|
+
out["value"] = value.to_s if value
|
|
630
|
+
out["store"] = store.to_s if store
|
|
631
|
+
pl = fetch_opt(spec, :placeholder)
|
|
632
|
+
out["placeholder"] = pl.to_s if pl
|
|
633
|
+
hosts = spec[:allowed_hosts] || spec["allowed_hosts"]
|
|
634
|
+
out["allowed_hosts"] = Array(hosts).map(&:to_s) if hosts
|
|
635
|
+
out
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# Normalize the `policy:` modify option (Symbol or String) to the native
|
|
639
|
+
# string, defaulting to "no_restart".
|
|
640
|
+
def normalize_modify_policy(policy)
|
|
641
|
+
return "no_restart" if policy.nil?
|
|
642
|
+
s = policy.to_s
|
|
643
|
+
unless %w[no_restart next_start restart].include?(s)
|
|
644
|
+
raise ArgumentError,
|
|
645
|
+
"policy: must be :no_restart, :next_start, or :restart (got #{policy.inspect})"
|
|
646
|
+
end
|
|
647
|
+
s
|
|
648
|
+
end
|
|
649
|
+
|
|
525
650
|
def normalize_secret(spec)
|
|
526
651
|
env = spec[:env] || spec["env"]
|
|
527
652
|
value = spec[:value] || spec["value"]
|
|
@@ -941,6 +1066,57 @@ module Microsandbox
|
|
|
941
1066
|
Metrics.new(@native.metrics)
|
|
942
1067
|
end
|
|
943
1068
|
|
|
1069
|
+
# Health-check the guest agent without refreshing the idle timer. Sends
|
|
1070
|
+
# `core.ping` to the running sandbox and reports the round-trip latency.
|
|
1071
|
+
# Mirrors the official SDKs' `ping`.
|
|
1072
|
+
# @return [PingResult]
|
|
1073
|
+
def ping
|
|
1074
|
+
PingResult.new(@native.ping)
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
# Explicitly refresh this sandbox's idle-activity timer (resetting any
|
|
1078
|
+
# `idle_timeout:` countdown). Mirrors the official SDKs' `touch`.
|
|
1079
|
+
# @return [TouchResult]
|
|
1080
|
+
def touch
|
|
1081
|
+
TouchResult.new(@native.touch)
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
# Plan or apply a live modification of this sandbox: resize CPU/memory,
|
|
1085
|
+
# adjust env/labels/workdir, and rotate/remove secrets, without recreating
|
|
1086
|
+
# the sandbox. The apply is all-or-nothing under the chosen `policy:`: the
|
|
1087
|
+
# default `:no_restart` applies only changes that are live-capable *right
|
|
1088
|
+
# now* and raises if any requested change needs a restart (on a running
|
|
1089
|
+
# sandbox that includes env/labels/workdir and *adding* a secret — rotating
|
|
1090
|
+
# or removing an existing one is live). Pass `policy: :next_start` to
|
|
1091
|
+
# persist restart-required changes for the next start, or `:restart` to
|
|
1092
|
+
# restart and apply them now; use `dry_run: true` to preview each change's
|
|
1093
|
+
# disposition without applying anything. Mirrors the official SDKs'
|
|
1094
|
+
# `modify`. Returns a {ModificationPlan} classifying each change.
|
|
1095
|
+
#
|
|
1096
|
+
# @param cpus [Integer, nil] desired effective vCPU count
|
|
1097
|
+
# @param max_cpus [Integer, nil] desired boot-time maximum vCPU ceiling
|
|
1098
|
+
# @param memory [Integer, nil] desired effective guest memory in MiB
|
|
1099
|
+
# @param max_memory [Integer, nil] desired boot-time maximum memory (MiB)
|
|
1100
|
+
# @param env [Hash, nil] environment variables to set for future execs
|
|
1101
|
+
# @param remove_env [Array<String>, nil] environment variable names to remove
|
|
1102
|
+
# @param labels [Hash, nil] labels to set
|
|
1103
|
+
# @param remove_labels [Array<String>, nil] label keys to remove
|
|
1104
|
+
# @param workdir [String, nil] desired working directory for future execs
|
|
1105
|
+
# @param secrets [Hash, nil] desired secret specs keyed by secret name. Each
|
|
1106
|
+
# spec is a Hash with at most one of `env:` (host env var to read),
|
|
1107
|
+
# `store:` (host secret-store reference), or `value:` (raw material), plus
|
|
1108
|
+
# optional `placeholder:` and `allowed_hosts:` (Array of host patterns).
|
|
1109
|
+
# @param remove_secrets [Array<String>, nil] secret names to remove
|
|
1110
|
+
# @param policy [Symbol, String, nil] `:no_restart` (default — apply only
|
|
1111
|
+
# changes that need no restart), `:next_start` (persist for the next
|
|
1112
|
+
# start), or `:restart` (restart to apply restart-required changes)
|
|
1113
|
+
# @param dry_run [Boolean] compute the plan without applying anything
|
|
1114
|
+
# @return [ModificationPlan]
|
|
1115
|
+
def modify(**kwargs)
|
|
1116
|
+
opts = Sandbox.send(:build_modify_opts, **kwargs)
|
|
1117
|
+
ModificationPlan.new(JSON.parse(@native.modify(opts)))
|
|
1118
|
+
end
|
|
1119
|
+
|
|
944
1120
|
# Read captured logs.
|
|
945
1121
|
#
|
|
946
1122
|
# @param tail [Integer, nil] only the last N entries
|
data/lib/microsandbox/version.rb
CHANGED
|
@@ -8,12 +8,12 @@ module Microsandbox
|
|
|
8
8
|
# Versioning section of the README for the full gem-to-runtime map. Must equal
|
|
9
9
|
# the native ext's Cargo crate version (`Native.version`), enforced by
|
|
10
10
|
# spec/unit/version_spec.rb.
|
|
11
|
-
VERSION = "0.
|
|
11
|
+
VERSION = "0.10.0"
|
|
12
12
|
|
|
13
13
|
# The upstream microsandbox runtime release this gem build embeds — the `tag`
|
|
14
14
|
# pinned on the `microsandbox`/`microsandbox-network` git deps in
|
|
15
15
|
# ext/microsandbox/Cargo.toml. Exposed at runtime as
|
|
16
16
|
# {Microsandbox.runtime_version}. spec/unit/version_spec.rb asserts it stays in
|
|
17
17
|
# sync with the Cargo tag so it can't silently drift out of date.
|
|
18
|
-
RUNTIME_VERSION = "v0.6.
|
|
18
|
+
RUNTIME_VERSION = "v0.6.6"
|
|
19
19
|
end
|
data/lib/microsandbox.rb
CHANGED
|
@@ -30,6 +30,7 @@ require_relative "microsandbox/patch"
|
|
|
30
30
|
require_relative "microsandbox/network"
|
|
31
31
|
require_relative "microsandbox/agent"
|
|
32
32
|
require_relative "microsandbox/ssh"
|
|
33
|
+
require_relative "microsandbox/modification"
|
|
33
34
|
require_relative "microsandbox/sandbox"
|
|
34
35
|
|
|
35
36
|
# Microsandbox — lightweight microVM sandboxes for Ruby.
|
data/sig/microsandbox.rbs
CHANGED
|
@@ -174,6 +174,14 @@ module Microsandbox
|
|
|
174
174
|
def config: () -> Hash[String, untyped]
|
|
175
175
|
def snapshot: (String name) -> SnapshotInfo
|
|
176
176
|
def snapshot_to: (String path) -> SnapshotInfo
|
|
177
|
+
def ping: () -> PingResult
|
|
178
|
+
def touch: () -> TouchResult
|
|
179
|
+
def modify: (?cpus: Integer?, ?max_cpus: Integer?, ?memory: Integer?, ?max_memory: Integer?,
|
|
180
|
+
?env: Hash[untyped, untyped]?, ?remove_env: Array[String]?,
|
|
181
|
+
?labels: Hash[untyped, untyped]?, ?remove_labels: Array[String]?,
|
|
182
|
+
?workdir: String?, ?secrets: Hash[untyped, untyped]?,
|
|
183
|
+
?remove_secrets: Array[String]?, ?policy: (Symbol | String)?,
|
|
184
|
+
?dry_run: bool) -> ModificationPlan
|
|
177
185
|
end
|
|
178
186
|
|
|
179
187
|
# Deprecated alias for SandboxHandle (was a read-only metadata type pre-v0.5.8).
|
|
@@ -190,8 +198,34 @@ module Microsandbox
|
|
|
190
198
|
def observed_at: () -> Time
|
|
191
199
|
end
|
|
192
200
|
|
|
201
|
+
class PingResult
|
|
202
|
+
def initialize: (Hash[String, untyped] data) -> void
|
|
203
|
+
def name: () -> String
|
|
204
|
+
def latency: () -> Float
|
|
205
|
+
def latency_ms: () -> Float
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
class TouchResult
|
|
209
|
+
def initialize: (Hash[String, untyped] data) -> void
|
|
210
|
+
def name: () -> String
|
|
211
|
+
def activity_seq: () -> Integer
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
class ModificationPlan
|
|
215
|
+
def initialize: (Hash[String, untyped] data) -> void
|
|
216
|
+
def sandbox: () -> String
|
|
217
|
+
def status: () -> String
|
|
218
|
+
def policy: () -> Symbol
|
|
219
|
+
def applied?: () -> bool
|
|
220
|
+
def changes: () -> Array[Hash[Symbol, untyped]]
|
|
221
|
+
def conflicts: () -> Array[Hash[Symbol, untyped]]
|
|
222
|
+
def warnings: () -> Array[Hash[Symbol, untyped]]
|
|
223
|
+
def resize_status: () -> Array[Hash[Symbol, untyped]]
|
|
224
|
+
end
|
|
225
|
+
|
|
193
226
|
class Sandbox
|
|
194
|
-
def self.create: (String name, ?image: String?, ?cpus: Integer?, ?
|
|
227
|
+
def self.create: (String name, ?image: String?, ?cpus: Integer?, ?max_cpus: Integer?,
|
|
228
|
+
?memory: Integer?, ?max_memory: Integer?,
|
|
195
229
|
?env: Hash[untyped, untyped]?, ?workdir: String?, ?shell: String?,
|
|
196
230
|
?user: String?, ?hostname: String?, ?labels: Hash[untyped, untyped]?,
|
|
197
231
|
?scripts: Hash[untyped, untyped]?, ?entrypoint: Array[String]?,
|
|
@@ -238,6 +272,14 @@ module Microsandbox
|
|
|
238
272
|
def fs: () -> FS
|
|
239
273
|
def ssh: () -> SshOps
|
|
240
274
|
def metrics: () -> Metrics
|
|
275
|
+
def ping: () -> PingResult
|
|
276
|
+
def touch: () -> TouchResult
|
|
277
|
+
def modify: (?cpus: Integer?, ?max_cpus: Integer?, ?memory: Integer?, ?max_memory: Integer?,
|
|
278
|
+
?env: Hash[untyped, untyped]?, ?remove_env: Array[String]?,
|
|
279
|
+
?labels: Hash[untyped, untyped]?, ?remove_labels: Array[String]?,
|
|
280
|
+
?workdir: String?, ?secrets: Hash[untyped, untyped]?,
|
|
281
|
+
?remove_secrets: Array[String]?, ?policy: (Symbol | String)?,
|
|
282
|
+
?dry_run: bool) -> ModificationPlan
|
|
241
283
|
def logs: (?tail: Integer?, ?since_ms: Numeric?, ?until_ms: Numeric?,
|
|
242
284
|
?sources: Array[String | Symbol]?) -> Array[LogEntry]
|
|
243
285
|
def metrics_stream: (?interval: Numeric) -> MetricsStream
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: microsandbox-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ya-luotao
|
|
@@ -68,6 +68,7 @@ files:
|
|
|
68
68
|
- lib/microsandbox/image.rb
|
|
69
69
|
- lib/microsandbox/log_entry.rb
|
|
70
70
|
- lib/microsandbox/metrics.rb
|
|
71
|
+
- lib/microsandbox/modification.rb
|
|
71
72
|
- lib/microsandbox/network.rb
|
|
72
73
|
- lib/microsandbox/patch.rb
|
|
73
74
|
- lib/microsandbox/sandbox.rb
|