e2b 0.2.0 → 0.3.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 +6 -2
- data/lib/e2b/api/http_client.rb +30 -19
- data/lib/e2b/client.rb +79 -36
- data/lib/e2b/configuration.rb +12 -6
- data/lib/e2b/dockerfile_parser.rb +179 -0
- data/lib/e2b/errors.rb +24 -1
- data/lib/e2b/models/build_info.rb +29 -0
- data/lib/e2b/models/build_status_reason.rb +27 -0
- data/lib/e2b/models/sandbox_info.rb +19 -2
- data/lib/e2b/models/snapshot_info.rb +19 -0
- data/lib/e2b/models/template_build_status_response.rb +31 -0
- data/lib/e2b/models/template_log_entry.rb +54 -0
- data/lib/e2b/models/template_tag.rb +34 -0
- data/lib/e2b/models/template_tag_info.rb +21 -0
- data/lib/e2b/paginator.rb +97 -0
- data/lib/e2b/ready_cmd.rb +36 -0
- data/lib/e2b/sandbox.rb +217 -66
- data/lib/e2b/sandbox_helpers.rb +100 -0
- data/lib/e2b/services/base_service.rb +64 -15
- data/lib/e2b/services/command_handle.rb +189 -36
- data/lib/e2b/services/commands.rb +37 -50
- data/lib/e2b/services/filesystem.rb +70 -23
- data/lib/e2b/services/live_streamable.rb +94 -0
- data/lib/e2b/services/pty.rb +13 -64
- data/lib/e2b/services/watch_handle.rb +6 -3
- data/lib/e2b/template.rb +1089 -0
- data/lib/e2b/template_logger.rb +52 -0
- data/lib/e2b/version.rb +1 -1
- data/lib/e2b.rb +16 -0
- metadata +44 -2
|
@@ -31,6 +31,15 @@ module E2B
|
|
|
31
31
|
# @return [Hash] Metadata
|
|
32
32
|
attr_reader :metadata
|
|
33
33
|
|
|
34
|
+
# @return [String, nil] Current sandbox state
|
|
35
|
+
attr_reader :state
|
|
36
|
+
|
|
37
|
+
# @return [String, nil] Domain where the sandbox is hosted
|
|
38
|
+
attr_reader :sandbox_domain
|
|
39
|
+
|
|
40
|
+
# @return [String, nil] Envd version reported by the control plane
|
|
41
|
+
attr_reader :envd_version
|
|
42
|
+
|
|
34
43
|
# Create from API response hash
|
|
35
44
|
#
|
|
36
45
|
# @param data [Hash] API response data
|
|
@@ -45,12 +54,16 @@ module E2B
|
|
|
45
54
|
end_at: parse_time(data["endAt"] || data["end_at"] || data[:endAt]),
|
|
46
55
|
cpu_count: data["cpuCount"] || data["cpu_count"] || data[:cpuCount],
|
|
47
56
|
memory_mb: data["memoryMB"] || data["memory_mb"] || data[:memoryMB],
|
|
48
|
-
metadata: data["metadata"] || data[:metadata] || {}
|
|
57
|
+
metadata: data["metadata"] || data[:metadata] || {},
|
|
58
|
+
state: data["state"] || data[:state],
|
|
59
|
+
sandbox_domain: data["domain"] || data[:domain],
|
|
60
|
+
envd_version: data["envdVersion"] || data["envd_version"] || data[:envdVersion]
|
|
49
61
|
)
|
|
50
62
|
end
|
|
51
63
|
|
|
52
64
|
def initialize(sandbox_id:, template_id:, alias_name: nil, client_id: nil,
|
|
53
|
-
started_at: nil, end_at: nil, cpu_count: nil, memory_mb: nil, metadata: {}
|
|
65
|
+
started_at: nil, end_at: nil, cpu_count: nil, memory_mb: nil, metadata: {},
|
|
66
|
+
state: nil, sandbox_domain: nil, envd_version: nil)
|
|
54
67
|
@sandbox_id = sandbox_id
|
|
55
68
|
@template_id = template_id
|
|
56
69
|
@alias_name = alias_name
|
|
@@ -60,12 +73,16 @@ module E2B
|
|
|
60
73
|
@cpu_count = cpu_count
|
|
61
74
|
@memory_mb = memory_mb
|
|
62
75
|
@metadata = metadata || {}
|
|
76
|
+
@state = state
|
|
77
|
+
@sandbox_domain = sandbox_domain
|
|
78
|
+
@envd_version = envd_version
|
|
63
79
|
end
|
|
64
80
|
|
|
65
81
|
# Check if sandbox is still running (not past end_at)
|
|
66
82
|
#
|
|
67
83
|
# @return [Boolean]
|
|
68
84
|
def running?
|
|
85
|
+
return false if @state == "paused"
|
|
69
86
|
return true if @end_at.nil?
|
|
70
87
|
|
|
71
88
|
Time.now < @end_at
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E2B
|
|
4
|
+
module Models
|
|
5
|
+
class SnapshotInfo
|
|
6
|
+
attr_reader :snapshot_id
|
|
7
|
+
|
|
8
|
+
def self.from_hash(data)
|
|
9
|
+
new(
|
|
10
|
+
snapshot_id: data["snapshotID"] || data["snapshot_id"] || data[:snapshotID]
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(snapshot_id:)
|
|
15
|
+
@snapshot_id = snapshot_id
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E2B
|
|
4
|
+
module Models
|
|
5
|
+
class TemplateBuildStatusResponse
|
|
6
|
+
attr_reader :build_id, :template_id, :status, :log_entries, :logs, :reason
|
|
7
|
+
|
|
8
|
+
def self.from_hash(data)
|
|
9
|
+
new(
|
|
10
|
+
build_id: data["buildID"] || data["build_id"] || data[:buildID],
|
|
11
|
+
template_id: data["templateID"] || data["template_id"] || data[:templateID],
|
|
12
|
+
status: data["status"] || data[:status],
|
|
13
|
+
log_entries: Array(data["logEntries"] || data["log_entries"] || data[:logEntries]).map do |entry|
|
|
14
|
+
TemplateLogEntry.from_hash(entry)
|
|
15
|
+
end,
|
|
16
|
+
logs: data["logs"] || data[:logs] || [],
|
|
17
|
+
reason: BuildStatusReason.from_hash(data["reason"] || data[:reason])
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(build_id:, template_id:, status:, log_entries:, logs:, reason:)
|
|
22
|
+
@build_id = build_id
|
|
23
|
+
@template_id = template_id
|
|
24
|
+
@status = status
|
|
25
|
+
@log_entries = log_entries || []
|
|
26
|
+
@logs = logs || []
|
|
27
|
+
@reason = reason
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module E2B
|
|
6
|
+
module Models
|
|
7
|
+
class TemplateLogEntry
|
|
8
|
+
attr_reader :timestamp, :level, :message
|
|
9
|
+
|
|
10
|
+
def self.from_hash(data)
|
|
11
|
+
new(
|
|
12
|
+
timestamp: parse_time(data["timestamp"] || data[:timestamp]),
|
|
13
|
+
level: data["level"] || data[:level],
|
|
14
|
+
message: data["message"] || data[:message]
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(timestamp:, level:, message:)
|
|
19
|
+
@timestamp = timestamp
|
|
20
|
+
@level = level
|
|
21
|
+
@message = self.class.strip_ansi_escape_codes(message.to_s)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_s
|
|
25
|
+
"[#{@timestamp&.iso8601}] [#{@level}] #{@message}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.parse_time(value)
|
|
29
|
+
return nil if value.nil?
|
|
30
|
+
return value if value.is_a?(Time)
|
|
31
|
+
|
|
32
|
+
Time.parse(value)
|
|
33
|
+
rescue ArgumentError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.strip_ansi_escape_codes(message)
|
|
38
|
+
message.gsub(/\e\[[0-9;?]*[ -\/]*[@-~]/, "")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class TemplateLogEntryStart < TemplateLogEntry
|
|
43
|
+
def initialize(timestamp:, message:)
|
|
44
|
+
super(timestamp: timestamp, level: "debug", message: message)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class TemplateLogEntryEnd < TemplateLogEntry
|
|
49
|
+
def initialize(timestamp:, message:)
|
|
50
|
+
super(timestamp: timestamp, level: "debug", message: message)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module E2B
|
|
6
|
+
module Models
|
|
7
|
+
class TemplateTag
|
|
8
|
+
attr_reader :tag, :build_id, :created_at
|
|
9
|
+
|
|
10
|
+
def self.from_hash(data)
|
|
11
|
+
new(
|
|
12
|
+
tag: data["tag"] || data[:tag],
|
|
13
|
+
build_id: data["buildID"] || data["build_id"] || data[:buildID],
|
|
14
|
+
created_at: parse_time(data["createdAt"] || data["created_at"] || data[:createdAt])
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(tag:, build_id:, created_at:)
|
|
19
|
+
@tag = tag
|
|
20
|
+
@build_id = build_id
|
|
21
|
+
@created_at = created_at
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.parse_time(value)
|
|
25
|
+
return nil if value.nil?
|
|
26
|
+
return value if value.is_a?(Time)
|
|
27
|
+
|
|
28
|
+
Time.parse(value)
|
|
29
|
+
rescue ArgumentError
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E2B
|
|
4
|
+
module Models
|
|
5
|
+
class TemplateTagInfo
|
|
6
|
+
attr_reader :build_id, :tags
|
|
7
|
+
|
|
8
|
+
def self.from_hash(data)
|
|
9
|
+
new(
|
|
10
|
+
build_id: data["buildID"] || data["build_id"] || data[:buildID],
|
|
11
|
+
tags: data["tags"] || data[:tags] || []
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(build_id:, tags:)
|
|
16
|
+
@build_id = build_id
|
|
17
|
+
@tags = tags || []
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module E2B
|
|
6
|
+
class BasePaginator
|
|
7
|
+
attr_reader :next_token
|
|
8
|
+
|
|
9
|
+
def initialize(limit:, next_token: nil, &fetch_page)
|
|
10
|
+
@limit = limit
|
|
11
|
+
@next_token = next_token
|
|
12
|
+
@fetch_page = fetch_page
|
|
13
|
+
@has_next = true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def has_next?
|
|
17
|
+
@has_next
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def next_items
|
|
21
|
+
raise E2BError, "No more items to fetch" unless has_next?
|
|
22
|
+
|
|
23
|
+
items, token = @fetch_page.call(limit: @limit, next_token: @next_token)
|
|
24
|
+
@next_token = token
|
|
25
|
+
@has_next = !@next_token.nil? && !@next_token.empty?
|
|
26
|
+
items
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class SandboxPaginator < BasePaginator
|
|
31
|
+
def initialize(http_client:, query: nil, limit: 100, next_token: nil)
|
|
32
|
+
normalized_query = normalize_query(query)
|
|
33
|
+
|
|
34
|
+
super(limit: limit, next_token: next_token) do |limit:, next_token:|
|
|
35
|
+
params = { limit: limit }
|
|
36
|
+
params[:nextToken] = next_token if next_token
|
|
37
|
+
if normalized_query[:metadata]
|
|
38
|
+
params[:metadata] = self.class.encode_metadata(normalized_query[:metadata])
|
|
39
|
+
end
|
|
40
|
+
params[:state] = normalized_query[:state] if normalized_query[:state]
|
|
41
|
+
|
|
42
|
+
response = http_client.get("/v2/sandboxes", params: params, detailed: true)
|
|
43
|
+
sandboxes = extract_sandboxes(response.body)
|
|
44
|
+
|
|
45
|
+
[
|
|
46
|
+
Array(sandboxes).map { |sandbox_data| Models::SandboxInfo.from_hash(sandbox_data) },
|
|
47
|
+
response.headers["x-next-token"]
|
|
48
|
+
]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.encode_metadata(metadata)
|
|
53
|
+
encoded_pairs = metadata.to_h.each_with_object({}) do |(key, value), result|
|
|
54
|
+
result[URI.encode_www_form_component(key.to_s)] = URI.encode_www_form_component(value.to_s)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
URI.encode_www_form(encoded_pairs)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def normalize_query(query)
|
|
63
|
+
return {} unless query
|
|
64
|
+
|
|
65
|
+
state = query[:state] || query["state"]
|
|
66
|
+
{
|
|
67
|
+
metadata: query[:metadata] || query["metadata"],
|
|
68
|
+
state: state ? Array(state).map(&:to_s) : nil
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def extract_sandboxes(body)
|
|
73
|
+
return body if body.is_a?(Array)
|
|
74
|
+
return body["sandboxes"] || body[:sandboxes] || [] if body.is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
[]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class SnapshotPaginator < BasePaginator
|
|
81
|
+
def initialize(http_client:, sandbox_id: nil, limit: 100, next_token: nil)
|
|
82
|
+
super(limit: limit, next_token: next_token) do |limit:, next_token:|
|
|
83
|
+
params = { limit: limit }
|
|
84
|
+
params[:sandboxID] = sandbox_id if sandbox_id
|
|
85
|
+
params[:nextToken] = next_token if next_token
|
|
86
|
+
|
|
87
|
+
response = http_client.get("/snapshots", params: params, detailed: true)
|
|
88
|
+
snapshots = response.body.is_a?(Array) ? response.body : []
|
|
89
|
+
|
|
90
|
+
[
|
|
91
|
+
snapshots.map { |snapshot_data| Models::SnapshotInfo.from_hash(snapshot_data) },
|
|
92
|
+
response.headers["x-next-token"]
|
|
93
|
+
]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E2B
|
|
4
|
+
class ReadyCmd
|
|
5
|
+
def initialize(cmd)
|
|
6
|
+
@cmd = cmd
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def get_cmd
|
|
10
|
+
@cmd
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def wait_for_port(port)
|
|
16
|
+
ReadyCmd.new("ss -tuln | grep :#{port}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def wait_for_url(url, status_code = 200)
|
|
20
|
+
ReadyCmd.new(%(curl -s -o /dev/null -w "%{http_code}" #{url} | grep -q "#{status_code}"))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def wait_for_process(process_name)
|
|
24
|
+
ReadyCmd.new("pgrep #{process_name} > /dev/null")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def wait_for_file(filename)
|
|
28
|
+
ReadyCmd.new("[ -f #{filename} ]")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def wait_for_timeout(timeout)
|
|
32
|
+
seconds = [1, timeout.to_i / 1000].max
|
|
33
|
+
ReadyCmd.new("sleep #{seconds}")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|