daytona-sdk 0.125.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 +7 -0
- data/.rubocop.yml +16 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.md +39 -0
- data/Rakefile +12 -0
- data/lib/daytona/code_toolbox/sandbox_python_code_toolbox.rb +439 -0
- data/lib/daytona/code_toolbox/sandbox_ts_code_toolbox.rb +23 -0
- data/lib/daytona/common/charts.rb +298 -0
- data/lib/daytona/common/code_language.rb +11 -0
- data/lib/daytona/common/daytona.rb +206 -0
- data/lib/daytona/common/file_system.rb +23 -0
- data/lib/daytona/common/git.rb +16 -0
- data/lib/daytona/common/image.rb +493 -0
- data/lib/daytona/common/process.rb +141 -0
- data/lib/daytona/common/pty.rb +306 -0
- data/lib/daytona/common/resources.rb +31 -0
- data/lib/daytona/common/response.rb +28 -0
- data/lib/daytona/common/snapshot.rb +110 -0
- data/lib/daytona/computer_use.rb +549 -0
- data/lib/daytona/config.rb +53 -0
- data/lib/daytona/daytona.rb +278 -0
- data/lib/daytona/file_system.rb +359 -0
- data/lib/daytona/git.rb +287 -0
- data/lib/daytona/lsp_server.rb +130 -0
- data/lib/daytona/object_storage.rb +169 -0
- data/lib/daytona/process.rb +484 -0
- data/lib/daytona/sandbox.rb +376 -0
- data/lib/daytona/sdk/version.rb +7 -0
- data/lib/daytona/sdk.rb +45 -0
- data/lib/daytona/snapshot_service.rb +198 -0
- data/lib/daytona/util.rb +56 -0
- data/lib/daytona/volume.rb +43 -0
- data/lib/daytona/volume_service.rb +49 -0
- data/sig/daytona/sdk.rbs +6 -0
- metadata +149 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'timeout'
|
|
4
|
+
|
|
5
|
+
module Daytona
|
|
6
|
+
class Sandbox # rubocop:disable Metrics/ClassLength
|
|
7
|
+
DEFAULT_TIMEOUT = 60
|
|
8
|
+
|
|
9
|
+
# @return [String] The ID of the sandbox
|
|
10
|
+
attr_reader :id
|
|
11
|
+
|
|
12
|
+
# @return [String] The organization ID of the sandbox
|
|
13
|
+
attr_reader :organization_id
|
|
14
|
+
|
|
15
|
+
# @return [String] The snapshot used for the sandbox
|
|
16
|
+
attr_reader :snapshot
|
|
17
|
+
|
|
18
|
+
# @return [String] The user associated with the project
|
|
19
|
+
attr_reader :user
|
|
20
|
+
|
|
21
|
+
# @return [Hash<String, String>] Environment variables for the sandbox
|
|
22
|
+
attr_reader :env
|
|
23
|
+
|
|
24
|
+
# @return [Hash<String, String>] Labels for the sandbox
|
|
25
|
+
attr_reader :labels
|
|
26
|
+
|
|
27
|
+
# @return [Boolean] Whether the sandbox http preview is public
|
|
28
|
+
attr_reader :public
|
|
29
|
+
|
|
30
|
+
# @return [Boolean] Whether to block all network access for the sandbox
|
|
31
|
+
attr_reader :network_block_all
|
|
32
|
+
|
|
33
|
+
# @return [String] Comma-separated list of allowed CIDR network addresses for the sandbox
|
|
34
|
+
attr_reader :network_allow_list
|
|
35
|
+
|
|
36
|
+
# @return [String] The target environment for the sandbox
|
|
37
|
+
attr_reader :target
|
|
38
|
+
|
|
39
|
+
# @return [Float] The CPU quota for the sandbox
|
|
40
|
+
attr_reader :cpu
|
|
41
|
+
|
|
42
|
+
# @return [Float] The GPU quota for the sandbox
|
|
43
|
+
attr_reader :gpu
|
|
44
|
+
|
|
45
|
+
# @return [Float] The memory quota for the sandbox
|
|
46
|
+
attr_reader :memory
|
|
47
|
+
|
|
48
|
+
# @return [Float] The disk quota for the sandbox
|
|
49
|
+
attr_reader :disk
|
|
50
|
+
|
|
51
|
+
# @return [DaytonaApiClient::SandboxState] The state of the sandbox
|
|
52
|
+
attr_reader :state
|
|
53
|
+
|
|
54
|
+
# @return [DaytonaApiClient::SandboxDesiredState] The desired state of the sandbox
|
|
55
|
+
attr_reader :desired_state
|
|
56
|
+
|
|
57
|
+
# @return [String] The error reason of the sandbox
|
|
58
|
+
attr_reader :error_reason
|
|
59
|
+
|
|
60
|
+
# @return [String] The state of the backup
|
|
61
|
+
attr_reader :backup_state
|
|
62
|
+
|
|
63
|
+
# @return [String] The creation timestamp of the last backup
|
|
64
|
+
attr_reader :backup_created_at
|
|
65
|
+
|
|
66
|
+
# @return [Float] Auto-stop interval in minutes (0 means disabled)
|
|
67
|
+
attr_reader :auto_stop_interval
|
|
68
|
+
|
|
69
|
+
# @return [Float] Auto-archive interval in minutes
|
|
70
|
+
attr_reader :auto_archive_interval
|
|
71
|
+
|
|
72
|
+
# @return [Float] Auto-delete interval in minutes
|
|
73
|
+
# (negative value means disabled, 0 means delete immediately upon stopping)
|
|
74
|
+
attr_reader :auto_delete_interval
|
|
75
|
+
|
|
76
|
+
# @return [String] The domain name of the runner
|
|
77
|
+
attr_reader :runner_domain
|
|
78
|
+
|
|
79
|
+
# @return [Array<DaytonaApiClient::SandboxVolume>] Array of volumes attached to the sandbox
|
|
80
|
+
attr_reader :volumes
|
|
81
|
+
|
|
82
|
+
# @return [DaytonaApiClient::BuildInfo] Build information for the sandbox
|
|
83
|
+
attr_reader :build_info
|
|
84
|
+
|
|
85
|
+
# @return [String] The creation timestamp of the sandbox
|
|
86
|
+
attr_reader :created_at
|
|
87
|
+
|
|
88
|
+
# @return [String] The last update timestamp of the sandbox
|
|
89
|
+
attr_reader :updated_at
|
|
90
|
+
|
|
91
|
+
# @return [String] The version of the daemon running in the sandbox
|
|
92
|
+
attr_reader :daemon_version
|
|
93
|
+
|
|
94
|
+
# @return [Daytona::SandboxPythonCodeToolbox, Daytona::SandboxTsCodeToolbox]
|
|
95
|
+
attr_reader :code_toolbox
|
|
96
|
+
|
|
97
|
+
# @return [Daytona::Config]
|
|
98
|
+
attr_reader :config
|
|
99
|
+
|
|
100
|
+
# @return [DaytonaApiClient::SandboxApi]
|
|
101
|
+
attr_reader :sandbox_api
|
|
102
|
+
|
|
103
|
+
# @return [DaytonaApiClient::ToolboxApi]
|
|
104
|
+
attr_reader :toolbox_api
|
|
105
|
+
|
|
106
|
+
# @return [Daytona::Process]
|
|
107
|
+
attr_reader :process
|
|
108
|
+
|
|
109
|
+
# @return [Daytona::FileSystem]
|
|
110
|
+
attr_reader :fs
|
|
111
|
+
|
|
112
|
+
# @return [Daytona::Git]
|
|
113
|
+
attr_reader :git
|
|
114
|
+
|
|
115
|
+
# @return [Daytona::ComputerUse]
|
|
116
|
+
attr_reader :computer_use
|
|
117
|
+
|
|
118
|
+
# @params code_toolbox [Daytona::SandboxPythonCodeToolbox, Daytona::SandboxTsCodeToolbox]
|
|
119
|
+
# @params config [Daytona::Config]
|
|
120
|
+
# @params sandbox_api [DaytonaApiClient::SandboxApi]
|
|
121
|
+
# @params sandbox_dto [DaytonaApiClient::Sandbox]
|
|
122
|
+
# @params toolbox_api [DaytonaApiClient::ToolboxApi]
|
|
123
|
+
def initialize(code_toolbox:, sandbox_dto:, config:, sandbox_api:, toolbox_api:) # rubocop:disable Metrics/MethodLength
|
|
124
|
+
process_response(sandbox_dto)
|
|
125
|
+
@code_toolbox = code_toolbox
|
|
126
|
+
@config = config
|
|
127
|
+
@sandbox_api = sandbox_api
|
|
128
|
+
@toolbox_api = toolbox_api
|
|
129
|
+
@process = Process.new(
|
|
130
|
+
sandbox_id: id,
|
|
131
|
+
code_toolbox:,
|
|
132
|
+
toolbox_api:,
|
|
133
|
+
get_preview_link: proc { |port| preview_url(port) }
|
|
134
|
+
)
|
|
135
|
+
@fs = FileSystem.new(sandbox_id: id, toolbox_api:)
|
|
136
|
+
@git = Git.new(sandbox_id: id, toolbox_api:)
|
|
137
|
+
@computer_use = ComputerUse.new(sandbox_id: id, toolbox_api:)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Archives the sandbox, making it inactive and preserving its state. When sandboxes are
|
|
141
|
+
# archived, the entire filesystem state is moved to cost-effective object storage, making it
|
|
142
|
+
# possible to keep sandboxes available for an extended period. The tradeoff between archived
|
|
143
|
+
# and stopped states is that starting an archived sandbox takes more time, depending on its size.
|
|
144
|
+
# Sandbox must be stopped before archiving.
|
|
145
|
+
#
|
|
146
|
+
# @return [void]
|
|
147
|
+
def archive
|
|
148
|
+
sandbox_api.archive_sandbox(id)
|
|
149
|
+
refresh
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Sets the auto-archive interval for the Sandbox.
|
|
153
|
+
# The Sandbox will automatically archive after being continuously stopped for the specified interval.
|
|
154
|
+
#
|
|
155
|
+
# @param interval [Integer]
|
|
156
|
+
# @return [Integer]
|
|
157
|
+
# @raise [Daytona:Sdk::Error]
|
|
158
|
+
def auto_archive_interval=(interval)
|
|
159
|
+
raise Sdk::Error, 'Auto-archive interval must be a non-negative integer' if interval.negative?
|
|
160
|
+
|
|
161
|
+
sandbox_api.set_auto_archive_interval(id, interval)
|
|
162
|
+
@auto_archive_interval = interval
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Sets the auto-delete interval for the Sandbox.
|
|
166
|
+
# The Sandbox will automatically delete after being continuously stopped for the specified interval.
|
|
167
|
+
#
|
|
168
|
+
# @param interval [Integer]
|
|
169
|
+
# @return [Integer]
|
|
170
|
+
# @raise [Daytona:Sdk::Error]
|
|
171
|
+
def auto_delete_interval=(interval)
|
|
172
|
+
sandbox_api.set_auto_delete_interval(id, interval)
|
|
173
|
+
@auto_delete_interval = interval
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Sets the auto-stop interval for the Sandbox.
|
|
177
|
+
# The Sandbox will automatically stop after being idle (no new events) for the specified interval.
|
|
178
|
+
# Events include any state changes or interactions with the Sandbox through the SDK.
|
|
179
|
+
# Interactions using Sandbox Previews are not included.
|
|
180
|
+
#
|
|
181
|
+
# @param interval [Integer]
|
|
182
|
+
# @return [Integer]
|
|
183
|
+
# @raise [Daytona:Sdk::Error]
|
|
184
|
+
def auto_stop_interval=(interval)
|
|
185
|
+
raise Sdk::Error, 'Auto-stop interval must be a non-negative integer' if interval.negative?
|
|
186
|
+
|
|
187
|
+
sandbox_api.set_autostop_interval(id, interval)
|
|
188
|
+
@auto_stop_interval = interval
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Creates an SSH access token for the sandbox.
|
|
192
|
+
#
|
|
193
|
+
# @param expires_in_minutes [Integer] TThe number of minutes the SSH access token will be valid for
|
|
194
|
+
# @return [DaytonaApiClient::SshAccessDto]
|
|
195
|
+
def create_ssh_access(expires_in_minutes) = sandbox_api.create_ssh_access(id, { expires_in_minutes: })
|
|
196
|
+
|
|
197
|
+
# @return [void]
|
|
198
|
+
def delete
|
|
199
|
+
sandbox_api.delete_sandbox(id)
|
|
200
|
+
refresh
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Sets labels for the Sandbox.
|
|
204
|
+
#
|
|
205
|
+
# @param labels [Hash<String, String>]
|
|
206
|
+
# @return [Hash<String, String>]
|
|
207
|
+
def labels=(labels)
|
|
208
|
+
@labels = sandbox_api.replace_labels(id, DaytonaApiClient::SandboxLabels.build_from_hash(labels:)).labels
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Retrieves the preview link for the sandbox at the specified port. If the port is closed,
|
|
212
|
+
# it will be opened automatically. For private sandboxes, a token is included to grant access
|
|
213
|
+
# to the URL.
|
|
214
|
+
#
|
|
215
|
+
# @param port [Integer]
|
|
216
|
+
# @return [DaytonaApiClient::PortPreviewUrl]
|
|
217
|
+
def preview_url(port) = sandbox_api.get_port_preview_url(id, port)
|
|
218
|
+
|
|
219
|
+
# Refresh the Sandbox data from the API.
|
|
220
|
+
#
|
|
221
|
+
# @return [void]
|
|
222
|
+
def refresh = process_response(sandbox_api.get_sandbox(id))
|
|
223
|
+
|
|
224
|
+
# Revokes an SSH access token for the sandbox.
|
|
225
|
+
#
|
|
226
|
+
# @param token [String]
|
|
227
|
+
# @return [void]
|
|
228
|
+
def revoke_ssh_access(token) = sandbox_api.revoke_ssh_access(id, token:)
|
|
229
|
+
|
|
230
|
+
# Starts the Sandbox and waits for it to be ready.
|
|
231
|
+
#
|
|
232
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
233
|
+
# @return [void]
|
|
234
|
+
def start(timeout = DEFAULT_TIMEOUT)
|
|
235
|
+
with_timeout(
|
|
236
|
+
timeout:,
|
|
237
|
+
message: "Sandbox #{id} failed to become ready within the #{timeout} seconds timeout period",
|
|
238
|
+
setup: proc { process_response(sandbox_api.start_sandbox(id)) }
|
|
239
|
+
) { wait_for_states(operation: OPERATION_START, target_states: [DaytonaApiClient::SandboxState::STARTED]) }
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Stops the Sandbox and waits for it to be stopped.
|
|
243
|
+
#
|
|
244
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
245
|
+
# @return [void]
|
|
246
|
+
def stop(timeout = DEFAULT_TIMEOUT) # rubocop:disable Metrics/MethodLength
|
|
247
|
+
with_timeout(
|
|
248
|
+
timeout:,
|
|
249
|
+
message: "Sandbox #{id} failed to become stopped within the #{timeout} seconds timeout period",
|
|
250
|
+
setup: proc {
|
|
251
|
+
sandbox_api.stop_sandbox(id)
|
|
252
|
+
refresh
|
|
253
|
+
}
|
|
254
|
+
) do
|
|
255
|
+
wait_for_states(
|
|
256
|
+
operation: OPERATION_STOP,
|
|
257
|
+
target_states: [DaytonaApiClient::SandboxState::STOPPED, DaytonaApiClient::SandboxState::DESTROYED]
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Creates a new Language Server Protocol (LSP) server instance.
|
|
263
|
+
# The LSP server provides language-specific features like code completion,
|
|
264
|
+
# diagnostics, and more.
|
|
265
|
+
#
|
|
266
|
+
# @param language_id [Symbol] The language server type (e.g., Daytona::LspServer::Language::PYTHON)
|
|
267
|
+
# @param path_to_project [String] Path to the project root directory. Relative paths are resolved
|
|
268
|
+
# based on the sandbox working directory.
|
|
269
|
+
# @return [Daytona::LspServer]
|
|
270
|
+
def create_lsp_server(language_id:, path_to_project:)
|
|
271
|
+
LspServer.new(language_id:, path_to_project:, toolbox_api:, sandbox_id: id)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Validates an SSH access token for the sandbox.
|
|
275
|
+
#
|
|
276
|
+
# @param token [String]
|
|
277
|
+
# @return [DaytonaApiClient::SshAccessValidationDto]
|
|
278
|
+
def validate_ssh_access(token) = sandbox_api.validate_ssh_access(token)
|
|
279
|
+
|
|
280
|
+
# Waits for the Sandbox to reach the 'started' state. Polls the Sandbox status until it
|
|
281
|
+
# reaches the 'started' state or encounters an error.
|
|
282
|
+
#
|
|
283
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
284
|
+
# @return [void]
|
|
285
|
+
def wait_for_sandbox_start(_timeout = DEFAULT_TIMEOUT)
|
|
286
|
+
wait_for_states(operation: OPERATION_START, target_states: [DaytonaApiClient::SandboxState::STARTED])
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
# @params sandbox_dto [DaytonaApiClient::Sandbox]
|
|
292
|
+
# @return [void]
|
|
293
|
+
def process_response(sandbox_dto) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
294
|
+
@id = sandbox_dto.id
|
|
295
|
+
@organization_id = sandbox_dto.organization_id
|
|
296
|
+
@snapshot = sandbox_dto.snapshot
|
|
297
|
+
@user = sandbox_dto.user
|
|
298
|
+
@env = sandbox_dto.env
|
|
299
|
+
@labels = sandbox_dto.labels
|
|
300
|
+
@public = sandbox_dto.public
|
|
301
|
+
@target = sandbox_dto.target
|
|
302
|
+
@cpu = sandbox_dto.cpu
|
|
303
|
+
@gpu = sandbox_dto.gpu
|
|
304
|
+
@memory = sandbox_dto.memory
|
|
305
|
+
@disk = sandbox_dto.disk
|
|
306
|
+
@state = sandbox_dto.state
|
|
307
|
+
@desired_state = sandbox_dto.desired_state
|
|
308
|
+
@error_reason = sandbox_dto.error_reason
|
|
309
|
+
@backup_state = sandbox_dto.backup_state
|
|
310
|
+
@backup_created_at = sandbox_dto.backup_created_at
|
|
311
|
+
@auto_stop_interval = sandbox_dto.auto_stop_interval
|
|
312
|
+
@auto_archive_interval = sandbox_dto.auto_archive_interval
|
|
313
|
+
@auto_delete_interval = sandbox_dto.auto_delete_interval
|
|
314
|
+
@runner_domain = sandbox_dto.runner_domain
|
|
315
|
+
@volumes = sandbox_dto.volumes
|
|
316
|
+
@build_info = sandbox_dto.build_info
|
|
317
|
+
@created_at = sandbox_dto.created_at
|
|
318
|
+
@updated_at = sandbox_dto.updated_at
|
|
319
|
+
@daemon_version = sandbox_dto.daemon_version
|
|
320
|
+
@network_block_all = sandbox_dto.network_block_all
|
|
321
|
+
@network_allow_list = sandbox_dto.network_allow_list
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Monitors block not to exceed max execution time.
|
|
325
|
+
#
|
|
326
|
+
# @param setup [#call, Nil] Optional setup block
|
|
327
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
|
|
328
|
+
# @param message [String] Error message
|
|
329
|
+
# @return [void]
|
|
330
|
+
# @raise [Daytona::Sdk::Error]
|
|
331
|
+
def with_timeout(message:, setup:, timeout: DEFAULT_TIMEOUT, &)
|
|
332
|
+
start_at = Time.now
|
|
333
|
+
setup&.call
|
|
334
|
+
|
|
335
|
+
Timeout.timeout(
|
|
336
|
+
setup ? [NO_TIMEOUT, timeout - (Time.now - start_at)].max : timeout,
|
|
337
|
+
Sdk::Error,
|
|
338
|
+
message,
|
|
339
|
+
&
|
|
340
|
+
)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Waits for the Sandbox to reach the one of the target states. Polls the Sandbox status until it
|
|
344
|
+
# reaches the one of the target states or encounters an error. It will wait up to 60 seconds
|
|
345
|
+
# for the Sandbox to reach one of the target states.
|
|
346
|
+
#
|
|
347
|
+
# @param operation [#to_s] Operation name for error message
|
|
348
|
+
# @param target_states [Array<DaytonaApiClient::SandboxState>] List of the target states
|
|
349
|
+
# @return [void]
|
|
350
|
+
# @raise [Daytona::Sdk::Error]
|
|
351
|
+
def wait_for_states(operation:, target_states:)
|
|
352
|
+
loop do
|
|
353
|
+
case state
|
|
354
|
+
when *target_states then return
|
|
355
|
+
when DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED
|
|
356
|
+
raise Sdk::Error, "Sandbox #{id} failed to #{operation} with state: #{state}, error reason: #{error_reason}"
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
sleep(IDLE_DURATION)
|
|
360
|
+
refresh
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
IDLE_DURATION = 0.1
|
|
365
|
+
private_constant :IDLE_DURATION
|
|
366
|
+
|
|
367
|
+
NO_TIMEOUT = 0
|
|
368
|
+
private_constant :NO_TIMEOUT
|
|
369
|
+
|
|
370
|
+
OPERATION_START = :start
|
|
371
|
+
private_constant :OPERATION_START
|
|
372
|
+
|
|
373
|
+
OPERATION_STOP = :stop
|
|
374
|
+
private_constant :OPERATION_STOP
|
|
375
|
+
end
|
|
376
|
+
end
|
data/lib/daytona/sdk.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
require 'dotenv'
|
|
6
|
+
Dotenv.load('.env.local', '.env')
|
|
7
|
+
require 'daytona_api_client'
|
|
8
|
+
require 'toml'
|
|
9
|
+
require 'websocket-client-simple'
|
|
10
|
+
|
|
11
|
+
require_relative 'sdk/version'
|
|
12
|
+
require_relative 'config'
|
|
13
|
+
require_relative 'common/charts'
|
|
14
|
+
require_relative 'common/code_language'
|
|
15
|
+
require_relative 'common/daytona'
|
|
16
|
+
require_relative 'common/file_system'
|
|
17
|
+
require_relative 'common/image'
|
|
18
|
+
require_relative 'common/git'
|
|
19
|
+
require_relative 'common/process'
|
|
20
|
+
require_relative 'common/pty'
|
|
21
|
+
require_relative 'common/resources'
|
|
22
|
+
require_relative 'common/response'
|
|
23
|
+
require_relative 'common/snapshot'
|
|
24
|
+
require_relative 'computer_use'
|
|
25
|
+
require_relative 'code_toolbox/sandbox_python_code_toolbox'
|
|
26
|
+
require_relative 'code_toolbox/sandbox_ts_code_toolbox'
|
|
27
|
+
require_relative 'daytona'
|
|
28
|
+
require_relative 'file_system'
|
|
29
|
+
require_relative 'git'
|
|
30
|
+
require_relative 'lsp_server'
|
|
31
|
+
require_relative 'object_storage'
|
|
32
|
+
require_relative 'sandbox'
|
|
33
|
+
require_relative 'snapshot_service'
|
|
34
|
+
require_relative 'util'
|
|
35
|
+
require_relative 'volume'
|
|
36
|
+
require_relative 'volume_service'
|
|
37
|
+
require_relative 'process'
|
|
38
|
+
|
|
39
|
+
module Daytona
|
|
40
|
+
module Sdk
|
|
41
|
+
class Error < StandardError; end
|
|
42
|
+
|
|
43
|
+
def self.logger = @logger ||= Logger.new($stdout, level: Logger::INFO)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module Daytona
|
|
6
|
+
class SnapshotService
|
|
7
|
+
SNAPSHOTS_FETCH_LIMIT = 200
|
|
8
|
+
|
|
9
|
+
# @param snapshots_api [DaytonaApiClient::SnapshotsApi] The snapshots API client
|
|
10
|
+
# @param object_storage_api [DaytonaApiClient::ObjectStorageApi] The object storage API client
|
|
11
|
+
def initialize(snapshots_api:, object_storage_api:)
|
|
12
|
+
@snapshots_api = snapshots_api
|
|
13
|
+
@object_storage_api = object_storage_api
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# List all Snapshots.
|
|
17
|
+
#
|
|
18
|
+
# @param page [Integer, Nil]
|
|
19
|
+
# @param limit [Integer, Nil]
|
|
20
|
+
# @return [Daytona::PaginatedResource] Paginated list of all Snapshots
|
|
21
|
+
# @raise [Daytona::Sdk::Error]
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# daytona = Daytona::Daytona.new
|
|
25
|
+
# response = daytona.snapshot.list(page: 1, limit: 10)
|
|
26
|
+
# snapshots.items.each { |snapshot| puts "#{snapshot.name} (#{snapshot.image_name})" }
|
|
27
|
+
def list(page: nil, limit: nil)
|
|
28
|
+
raise Sdk::Error, 'page must be positive integer' if page && page < 1
|
|
29
|
+
|
|
30
|
+
raise Sdk::Error, 'limit must be positive integer' if limit && limit < 1
|
|
31
|
+
|
|
32
|
+
response = snapshots_api.get_all_snapshots(page:, limit:)
|
|
33
|
+
PaginatedResource.new(
|
|
34
|
+
total: response.total,
|
|
35
|
+
page: response.page,
|
|
36
|
+
total_pages: response.total_pages,
|
|
37
|
+
items: response.items.map { |snapshot_dto| Snapshot.from_dto(snapshot_dto) }
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Delete a Snapshot.
|
|
42
|
+
#
|
|
43
|
+
# @param snapshot [Daytona::Snapshot] Snapshot to delete
|
|
44
|
+
# @return [void]
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# daytona = Daytona::Daytona.new
|
|
48
|
+
# snapshot = daytona.snapshot.get("demo")
|
|
49
|
+
# daytona.snapshot.delete(snapshot)
|
|
50
|
+
# puts "Snapshot deleted"
|
|
51
|
+
def delete(snapshot) = snapshots_api.remove_snapshot(snapshot.id)
|
|
52
|
+
|
|
53
|
+
# Get a Snapshot by name.
|
|
54
|
+
#
|
|
55
|
+
# @param name [String] Name of the Snapshot to get
|
|
56
|
+
# @return [Daytona::Snapshot] The Snapshot object
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# daytona = Daytona::Daytona.new
|
|
60
|
+
# snapshot = daytona.snapshot.get("demo")
|
|
61
|
+
# puts "#{snapshot.name} (#{snapshot.image_name})"
|
|
62
|
+
def get(name) = Snapshot.from_dto(snapshots_api.get_snapshot(name))
|
|
63
|
+
|
|
64
|
+
# Creates and registers a new snapshot from the given Image definition.
|
|
65
|
+
#
|
|
66
|
+
# @param params [Daytona::CreateSnapshotParams] Parameters for snapshot creation
|
|
67
|
+
# @param on_logs [Proc, Nil] Callback proc handling snapshot creation logs
|
|
68
|
+
# @return [Daytona::Snapshot] The created snapshot
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
# image = Image.debianSlim('3.12').pipInstall('numpy')
|
|
72
|
+
# params = CreateSnapshotParams.new(name: 'my-snapshot', image: image)
|
|
73
|
+
# snapshot = daytona.snapshot.create(params) do |chunk|
|
|
74
|
+
# print chunk
|
|
75
|
+
# end
|
|
76
|
+
def create(params, on_logs: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
77
|
+
create_snapshot_req = DaytonaApiClient::CreateSnapshot.new(name: params.name)
|
|
78
|
+
|
|
79
|
+
if params.image.is_a?(String)
|
|
80
|
+
create_snapshot_req.image_name = params.image
|
|
81
|
+
create_snapshot_req.entrypoint = params.entrypoint
|
|
82
|
+
else
|
|
83
|
+
create_snapshot_req.build_info = DaytonaApiClient::CreateBuildInfo.new(
|
|
84
|
+
context_hashes: self.class.process_image_context(object_storage_api, params.image),
|
|
85
|
+
dockerfile_content: if params.entrypoint
|
|
86
|
+
params.image.entrypoint(params.entrypoint).dockerfile
|
|
87
|
+
else
|
|
88
|
+
params.image.dockerfile
|
|
89
|
+
end
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if params.resources
|
|
94
|
+
create_snapshot_req.cpu = params.resources.cpu
|
|
95
|
+
create_snapshot_req.gpu = params.resources.gpu
|
|
96
|
+
create_snapshot_req.memory = params.resources.memory
|
|
97
|
+
create_snapshot_req.disk = params.resources.disk
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
snapshot = snapshots_api.create_snapshot(create_snapshot_req)
|
|
101
|
+
|
|
102
|
+
snapshot = stream_logs(snapshot, on_logs:) if on_logs
|
|
103
|
+
|
|
104
|
+
if [DaytonaApiClient::SnapshotState::ERROR, DaytonaApiClient::SnapshotState::BUILD_FAILED].include?(snapshot.state)
|
|
105
|
+
raise Sdk::Error, "Failed to create snapshot #{snapshot.name}, reason: #{snapshot.error_reason}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
Snapshot.from_dto(snapshot)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Activate a snapshot
|
|
112
|
+
#
|
|
113
|
+
# @param snapshot [Daytona::Snapshot] The snapshot instance
|
|
114
|
+
# @return [Daytona::Snapshot]
|
|
115
|
+
def activate(snapshot) = Snapshot.from_dto(snapshots_api.activate_snapshot(snapshot.id))
|
|
116
|
+
|
|
117
|
+
# Processes the image context by uploading it to object storage
|
|
118
|
+
#
|
|
119
|
+
# @param image [Daytona::Image] The Image instance
|
|
120
|
+
# @return [Array<String>] List of context hashes stored in object storage
|
|
121
|
+
def self.process_image_context(object_storage_api, image) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
122
|
+
return [] unless image.context_list && !image.context_list.empty?
|
|
123
|
+
|
|
124
|
+
push_access_creds = object_storage_api.get_push_access
|
|
125
|
+
|
|
126
|
+
object_storage = ObjectStorage.new(
|
|
127
|
+
endpoint_url: push_access_creds.storage_url,
|
|
128
|
+
aws_access_key_id: push_access_creds.access_key,
|
|
129
|
+
aws_secret_access_key: push_access_creds.secret,
|
|
130
|
+
aws_session_token: push_access_creds.session_token,
|
|
131
|
+
bucket_name: push_access_creds.bucket
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
image.context_list.map do |context|
|
|
135
|
+
object_storage.upload(
|
|
136
|
+
context.source_path,
|
|
137
|
+
push_access_creds.organization_id,
|
|
138
|
+
context.archive_path
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
# @return [DaytonaApiClient::SnapshotsApi] The snapshots API client
|
|
146
|
+
attr_reader :snapshots_api
|
|
147
|
+
|
|
148
|
+
# @return [DaytonaApiClient::ObjectStorageApi, nil] The object storage API client
|
|
149
|
+
attr_reader :object_storage_api
|
|
150
|
+
|
|
151
|
+
# @param snapshot [DaytonaApiClient::SnapshotDto]
|
|
152
|
+
# @param on_logs [Proc]
|
|
153
|
+
# @return [DaytonaApiClient::SnapshotDto]
|
|
154
|
+
def stream_logs(snapshot, on_logs:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
155
|
+
terminal_states = [
|
|
156
|
+
DaytonaApiClient::SnapshotState::ACTIVE,
|
|
157
|
+
DaytonaApiClient::SnapshotState::ERROR,
|
|
158
|
+
DaytonaApiClient::SnapshotState::BUILD_FAILED
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
thread = nil
|
|
162
|
+
previous_state = snapshot.state
|
|
163
|
+
until terminal_states.include?(snapshot.state)
|
|
164
|
+
Sdk.logger.debug("Waiting for snapshot to be created: #{snapshot.state}")
|
|
165
|
+
if thread.nil? && snapshot.state != DaytonaApiClient::SnapshotState::BUILD_PENDING
|
|
166
|
+
thread = start_log_streaming(snapshot, on_logs:)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
on_logs.call("Creating snapshot #{snapshot.name} (#{snapshot.state})") if previous_state != snapshot.state
|
|
170
|
+
|
|
171
|
+
sleep(1)
|
|
172
|
+
previous_state = snapshot.state
|
|
173
|
+
snapshot = snapshots_api.get_snapshot(snapshot.id)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
thread&.join
|
|
177
|
+
|
|
178
|
+
if snapshot.state == DaytonaApiClient::SnapshotState::ACTIVE
|
|
179
|
+
on_logs.call("Created snapshot #{snapshot.name} (#{snapshot.state})")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
snapshot
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# @param snapshot [DaytonaApiClient::SnapshotDto]
|
|
186
|
+
# @param on_logs [Proc]
|
|
187
|
+
# @return [Thread]
|
|
188
|
+
def start_log_streaming(snapshot, on_logs:)
|
|
189
|
+
uri = URI.parse(snapshots_api.api_client.config.base_url)
|
|
190
|
+
uri.path = "/api/snapshots/#{snapshot.id}/build-logs"
|
|
191
|
+
uri.query = 'follow=true'
|
|
192
|
+
|
|
193
|
+
headers = {}
|
|
194
|
+
snapshots_api.api_client.update_params_for_auth!(headers, nil, ['bearer'])
|
|
195
|
+
Util.stream_async(uri:, headers:, on_chunk: on_logs)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
data/lib/daytona/util.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
|
|
5
|
+
module Daytona
|
|
6
|
+
module Util
|
|
7
|
+
def self.demux(line) # rubocop:disable Metrics/MethodLength
|
|
8
|
+
stdout = ''.dup
|
|
9
|
+
stderr = ''.dup
|
|
10
|
+
|
|
11
|
+
until line.empty?
|
|
12
|
+
buff = line.start_with?(STDOUT_PREFIX) ? stdout : stderr
|
|
13
|
+
line = line[3..]
|
|
14
|
+
|
|
15
|
+
end_index = [
|
|
16
|
+
line.index(STDOUT_PREFIX),
|
|
17
|
+
line.index(STDERR_PREFIX)
|
|
18
|
+
].compact.min || line.length
|
|
19
|
+
data = line[...end_index]
|
|
20
|
+
buff << data
|
|
21
|
+
|
|
22
|
+
line = line[end_index..]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
[stdout, stderr]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param uri [URI]
|
|
29
|
+
# @param on_chunk [Proc]
|
|
30
|
+
# @param headers [Hash<String, String>]
|
|
31
|
+
# @return [Thread]
|
|
32
|
+
def self.stream_async(uri:, on_chunk:, headers: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
33
|
+
Sdk.logger.debug("Starting async stream: #{uri}")
|
|
34
|
+
Thread.new do
|
|
35
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
36
|
+
request = Net::HTTP::Get.new(uri, headers)
|
|
37
|
+
|
|
38
|
+
http.request(request) do |response|
|
|
39
|
+
response.read_body do |chunk|
|
|
40
|
+
Sdk.logger.debug("Chunked response received: #{chunk.inspect}")
|
|
41
|
+
on_chunk.call(chunk)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
rescue Net::ReadTimeout => e
|
|
46
|
+
Sdk.logger.debug("Async stream (#{uri}) timeout: #{e.inspect}")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
STDOUT_PREFIX = "\x01\x01\01"
|
|
51
|
+
private_constant :STDOUT_PREFIX
|
|
52
|
+
|
|
53
|
+
STDERR_PREFIX = "\x02\x02\02"
|
|
54
|
+
private_constant :STDERR_PREFIX
|
|
55
|
+
end
|
|
56
|
+
end
|