daytona 0.196.0 → 0.197.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/lib/daytona/common/daytona.rb +19 -5
- data/lib/daytona/common/pty.rb +22 -7
- data/lib/daytona/daytona.rb +33 -3
- data/lib/daytona/process.rb +34 -18
- data/lib/daytona/sandbox.rb +159 -4
- data/lib/daytona/sdk/version.rb +1 -1
- data/lib/daytona/sdk.rb +1 -0
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e838c81b167b038d7935b94479648df5ebecb486e34465986baaeed2da7a0a5c
|
|
4
|
+
data.tar.gz: 1dce18d22149936bf16af4beda9dcf52bb1de63a0b358c7e965f2dc663bd5933
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e55a677df194a0dd60144888407f1186a03b26de14d9c3b4faf6d8611859c5d7ad059b73a1a3ce9cb41f22300c69b029139659817a27a7622e351073e00cb432
|
|
7
|
+
data.tar.gz: e62f6b6366d05fa9bd5343752064c1e50cdd4e0a93e101d7ec09791773e81feda4df1a856646c9961b09bdef2d96b2b7c7320afdca1e1f5189434f83556036a4
|
|
@@ -28,6 +28,11 @@ module Daytona
|
|
|
28
28
|
# @return [Integer, nil] Auto-stop interval in minutes
|
|
29
29
|
attr_accessor :auto_stop_interval
|
|
30
30
|
|
|
31
|
+
# @return [Integer, nil] Auto-pause interval in minutes (nil = server default: 60 for
|
|
32
|
+
# non-ephemeral pause-supporting sandbox classes with auto-stop disabled; 0 = disabled).
|
|
33
|
+
# Not allowed for ephemeral sandboxes
|
|
34
|
+
attr_accessor :auto_pause_interval
|
|
35
|
+
|
|
31
36
|
# @return [Integer, nil] Auto-archive interval in minutes
|
|
32
37
|
attr_accessor :auto_archive_interval
|
|
33
38
|
|
|
@@ -69,6 +74,7 @@ module Daytona
|
|
|
69
74
|
# @param public [Boolean, nil] Whether the Sandbox should be public
|
|
70
75
|
# @param timeout [Float, nil] Timeout in seconds for Sandbox to be created and started
|
|
71
76
|
# @param auto_stop_interval [Integer, nil] Auto-stop interval in minutes
|
|
77
|
+
# @param auto_pause_interval [Integer, nil] Auto-pause interval in minutes
|
|
72
78
|
# @param auto_archive_interval [Integer, nil] Auto-archive interval in minutes
|
|
73
79
|
# @param auto_delete_interval [Integer, nil] Auto-delete interval in minutes
|
|
74
80
|
# @param volumes [Array<DaytonaApiClient::SandboxVolume>, nil] List of volumes mounts to attach to the Sandbox
|
|
@@ -87,6 +93,7 @@ module Daytona
|
|
|
87
93
|
public: nil,
|
|
88
94
|
timeout: nil,
|
|
89
95
|
auto_stop_interval: nil,
|
|
96
|
+
auto_pause_interval: nil,
|
|
90
97
|
auto_archive_interval: nil,
|
|
91
98
|
auto_delete_interval: nil,
|
|
92
99
|
volumes: nil,
|
|
@@ -104,6 +111,7 @@ module Daytona
|
|
|
104
111
|
@public = public
|
|
105
112
|
@timeout = timeout
|
|
106
113
|
@auto_stop_interval = auto_stop_interval
|
|
114
|
+
@auto_pause_interval = auto_pause_interval
|
|
107
115
|
@auto_archive_interval = auto_archive_interval
|
|
108
116
|
@auto_delete_interval = auto_delete_interval
|
|
109
117
|
@volumes = volumes
|
|
@@ -130,6 +138,7 @@ module Daytona
|
|
|
130
138
|
public:,
|
|
131
139
|
timeout:,
|
|
132
140
|
auto_stop_interval:,
|
|
141
|
+
auto_pause_interval:,
|
|
133
142
|
auto_archive_interval:,
|
|
134
143
|
auto_delete_interval:,
|
|
135
144
|
volumes:,
|
|
@@ -148,12 +157,15 @@ module Daytona
|
|
|
148
157
|
#
|
|
149
158
|
# @return [void]
|
|
150
159
|
def handle_ephemeral_auto_delete_conflict
|
|
151
|
-
return unless ephemeral
|
|
160
|
+
return unless ephemeral
|
|
161
|
+
|
|
162
|
+
if auto_delete_interval && !auto_delete_interval.zero?
|
|
163
|
+
warn(
|
|
164
|
+
"'ephemeral' and 'auto_delete_interval' cannot be used together. " \
|
|
165
|
+
'If ephemeral is true, auto_delete_interval will be ignored and set to 0.'
|
|
166
|
+
)
|
|
167
|
+
end
|
|
152
168
|
|
|
153
|
-
warn(
|
|
154
|
-
"'ephemeral' and 'auto_delete_interval' cannot be used together. " \
|
|
155
|
-
'If ephemeral is true, auto_delete_interval will be ignored and set to 0.'
|
|
156
|
-
)
|
|
157
169
|
@auto_delete_interval = 0
|
|
158
170
|
end
|
|
159
171
|
end
|
|
@@ -178,6 +190,7 @@ module Daytona
|
|
|
178
190
|
# @param public [Boolean, nil] Whether the Sandbox should be public
|
|
179
191
|
# @param timeout [Float, nil] Timeout in seconds for Sandbox to be created and started
|
|
180
192
|
# @param auto_stop_interval [Integer, nil] Auto-stop interval in minutes
|
|
193
|
+
# @param auto_pause_interval [Integer, nil] Auto-pause interval in minutes
|
|
181
194
|
# @param auto_archive_interval [Integer, nil] Auto-archive interval in minutes
|
|
182
195
|
# @param auto_delete_interval [Integer, nil] Auto-delete interval in minutes
|
|
183
196
|
# @param volumes [Array<DaytonaApiClient::SandboxVolume>, nil] List of volumes mounts to attach to the Sandbox
|
|
@@ -219,6 +232,7 @@ module Daytona
|
|
|
219
232
|
# @param public [Boolean, nil] Whether the Sandbox should be public
|
|
220
233
|
# @param timeout [Float, nil] Timeout in seconds for Sandbox to be created and started
|
|
221
234
|
# @param auto_stop_interval [Integer, nil] Auto-stop interval in minutes
|
|
235
|
+
# @param auto_pause_interval [Integer, nil] Auto-pause interval in minutes
|
|
222
236
|
# @param auto_archive_interval [Integer, nil] Auto-archive interval in minutes
|
|
223
237
|
# @param auto_delete_interval [Integer, nil] Auto-delete interval in minutes
|
|
224
238
|
# @param volumes [Array<DaytonaApiClient::SandboxVolume>, nil] List of volumes mounts to attach to the Sandbox
|
data/lib/daytona/common/pty.rb
CHANGED
|
@@ -7,6 +7,10 @@ require 'json'
|
|
|
7
7
|
require 'observer'
|
|
8
8
|
|
|
9
9
|
module Daytona
|
|
10
|
+
# Capability token advertised on PTY WebSocket connects so the daemon sends the
|
|
11
|
+
# "exited" control message; clients that don't send it only get the close frame.
|
|
12
|
+
PTY_EXIT_CONTROL_SUBPROTOCOL = 'X-Daytona-Pty-Exit-Control'
|
|
13
|
+
|
|
10
14
|
class PtySize
|
|
11
15
|
# @return [Integer] Number of terminal rows (height)
|
|
12
16
|
attr_reader :rows
|
|
@@ -68,6 +72,7 @@ module Daytona
|
|
|
68
72
|
@handle_kill = handle_kill
|
|
69
73
|
@exit_code = nil
|
|
70
74
|
@error = nil
|
|
75
|
+
@exited = false
|
|
71
76
|
@logger = Sdk.logger
|
|
72
77
|
|
|
73
78
|
@status = Status::INIT
|
|
@@ -77,21 +82,23 @@ module Daytona
|
|
|
77
82
|
# Check if connected to the PTY session
|
|
78
83
|
#
|
|
79
84
|
# @return [Boolean] true if connected, false otherwise
|
|
80
|
-
|
|
85
|
+
# A PTY that has exited is not usable even while the socket briefly stays
|
|
86
|
+
# open before the close frame, so treat it as disconnected.
|
|
87
|
+
def connected? = websocket.open? && !@exited
|
|
81
88
|
|
|
82
89
|
# Wait for the PTY connection to be established
|
|
83
90
|
#
|
|
84
91
|
# @param timeout [Float] Maximum time in seconds to wait for connection. Defaults to 10.0
|
|
85
92
|
# @return [void]
|
|
86
93
|
# @raise [Daytona::Sdk::Error] If connection timeout is exceeded
|
|
87
|
-
def wait_for_connection(timeout: DEFAULT_TIMEOUT)
|
|
88
|
-
return if status == Status::CONNECTED
|
|
94
|
+
def wait_for_connection(timeout: DEFAULT_TIMEOUT) # rubocop:disable Metrics/CyclomaticComplexity
|
|
95
|
+
return if status == Status::CONNECTED || @exited
|
|
89
96
|
|
|
90
97
|
start_time = Time.now
|
|
91
98
|
|
|
92
|
-
sleep(SLEEP_INTERVAL) until status == Status::CONNECTED || (Time.now - start_time) > timeout
|
|
99
|
+
sleep(SLEEP_INTERVAL) until status == Status::CONNECTED || @exited || (Time.now - start_time) > timeout
|
|
93
100
|
|
|
94
|
-
raise Sdk::Error, 'PTY connection timeout' unless status == Status::CONNECTED
|
|
101
|
+
raise Sdk::Error, 'PTY connection timeout' unless status == Status::CONNECTED || @exited
|
|
95
102
|
end
|
|
96
103
|
|
|
97
104
|
# Send input to the PTY session
|
|
@@ -127,8 +134,9 @@ module Daytona
|
|
|
127
134
|
#
|
|
128
135
|
# @param on_data [Proc, nil] Optional callback to handle output data
|
|
129
136
|
# @return [Daytona::PtyResult] Result containing exit code and error information
|
|
130
|
-
def wait(timeout: nil, &on_data)
|
|
137
|
+
def wait(timeout: nil, &on_data) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/AbcSize
|
|
131
138
|
timeout ||= Float::INFINITY
|
|
139
|
+
return PtyResult.new(exit_code:, error:) if @exited
|
|
132
140
|
return unless status == Status::CONNECTED
|
|
133
141
|
|
|
134
142
|
start_time = Time.now
|
|
@@ -239,11 +247,17 @@ module Daytona
|
|
|
239
247
|
|
|
240
248
|
# @param data [WebSocket::Frame::Data]
|
|
241
249
|
# @return [void]
|
|
242
|
-
def process_websocket_control_message(data) # rubocop:disable Metrics/MethodLength
|
|
250
|
+
def process_websocket_control_message(data) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
243
251
|
case data[:status]
|
|
244
252
|
when WebSocketControlStatus::CONNECTED
|
|
245
253
|
logger.debug('[control] connected')
|
|
246
254
|
@status = Status::CONNECTED
|
|
255
|
+
when WebSocketControlStatus::EXITED
|
|
256
|
+
logger.debug("[control] exited: code=#{data[:exitCode]}")
|
|
257
|
+
@exit_code = data[:exitCode]
|
|
258
|
+
@error = data[:exitReason] if data[:exitReason]
|
|
259
|
+
@exited = true
|
|
260
|
+
@status = Status::CLOSED
|
|
247
261
|
when WebSocketControlStatus::ERROR
|
|
248
262
|
logger.debug("[control] error: #{error.inspect}")
|
|
249
263
|
@status = Status::ERROR
|
|
@@ -302,6 +316,7 @@ module Daytona
|
|
|
302
316
|
module WebSocketControlStatus
|
|
303
317
|
ALL = [
|
|
304
318
|
CONNECTED = 'connected',
|
|
319
|
+
EXITED = 'exited',
|
|
305
320
|
ERROR = 'error'
|
|
306
321
|
].freeze
|
|
307
322
|
end
|
data/lib/daytona/daytona.rb
CHANGED
|
@@ -48,6 +48,9 @@ module Daytona
|
|
|
48
48
|
@api_client = build_api_client
|
|
49
49
|
@sandbox_api = DaytonaApiClient::SandboxApi.new(api_client)
|
|
50
50
|
@config_api = DaytonaApiClient::ConfigApi.new(api_client)
|
|
51
|
+
@analytics_api_url = nil
|
|
52
|
+
@analytics_api_url_fetched = false
|
|
53
|
+
@analytics_api_url_mutex = Mutex.new
|
|
51
54
|
@volume = VolumeService.new(DaytonaApiClient::VolumesApi.new(api_client), otel_state:)
|
|
52
55
|
@secret = SecretService.new(DaytonaApiClient::SecretApi.new(api_client), otel_state:)
|
|
53
56
|
@object_storage_api = DaytonaApiClient::ObjectStorageApi.new(api_client)
|
|
@@ -68,7 +71,8 @@ module Daytona
|
|
|
68
71
|
#
|
|
69
72
|
# @param params [Daytona::CreateSandboxFromSnapshotParams, Daytona::CreateSandboxFromImageParams, Nil] Sandbox creation parameters
|
|
70
73
|
# @return [Daytona::Sandbox] The created sandbox
|
|
71
|
-
# @raise [Daytona::Sdk::Error] If auto_stop_interval or auto_archive_interval is negative
|
|
74
|
+
# @raise [Daytona::Sdk::Error] If auto_stop_interval, auto_pause_interval, or auto_archive_interval is negative,
|
|
75
|
+
# or if auto_stop_interval and auto_pause_interval are both non-zero
|
|
72
76
|
def create(params = nil, on_snapshot_create_logs: nil)
|
|
73
77
|
if params.nil?
|
|
74
78
|
params = CreateSandboxFromSnapshotParams.new(language: CodeLanguage::PYTHON)
|
|
@@ -148,6 +152,18 @@ module Daytona
|
|
|
148
152
|
# @return [Daytona::OtelState, nil]
|
|
149
153
|
attr_reader :otel_state
|
|
150
154
|
|
|
155
|
+
# Resolves the deployment's Analytics API URL via /config, cached (including a nil
|
|
156
|
+
# result) for the client's lifetime. Errors are not cached, so failed lookups retry.
|
|
157
|
+
def analytics_api_url
|
|
158
|
+
@analytics_api_url_mutex.synchronize do
|
|
159
|
+
unless @analytics_api_url_fetched
|
|
160
|
+
@analytics_api_url = @config_api.config_controller_get_config.analytics_api_url
|
|
161
|
+
@analytics_api_url_fetched = true
|
|
162
|
+
end
|
|
163
|
+
@analytics_api_url
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
151
167
|
# Fetches a single page of sandboxes. Each call produces one OTel span
|
|
152
168
|
# ("Daytona.list_fetch_page") so that paginated iteration emits N spans
|
|
153
169
|
# for N pages.
|
|
@@ -192,7 +208,8 @@ module Daytona
|
|
|
192
208
|
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
193
209
|
# @param on_snapshot_create_logs [Proc]
|
|
194
210
|
# @return [Daytona::Sandbox] The created sandbox
|
|
195
|
-
# @raise [Daytona::Sdk::Error] If auto_stop_interval or auto_archive_interval is negative
|
|
211
|
+
# @raise [Daytona::Sdk::Error] If auto_stop_interval, auto_pause_interval, or auto_archive_interval is negative,
|
|
212
|
+
# or if auto_stop_interval and auto_pause_interval are both non-zero
|
|
196
213
|
def _create(params, timeout: 60, on_snapshot_create_logs: nil)
|
|
197
214
|
raise Sdk::Error, 'Timeout must be a non-negative number' if timeout.negative?
|
|
198
215
|
|
|
@@ -200,6 +217,17 @@ module Daytona
|
|
|
200
217
|
|
|
201
218
|
raise Sdk::Error, 'auto_stop_interval must be a non-negative integer' if params.auto_stop_interval&.negative?
|
|
202
219
|
|
|
220
|
+
raise Sdk::Error, 'auto_pause_interval must be a non-negative integer' if params.auto_pause_interval&.negative?
|
|
221
|
+
|
|
222
|
+
if params.auto_stop_interval&.positive? && params.auto_pause_interval&.positive?
|
|
223
|
+
raise Sdk::Error,
|
|
224
|
+
'auto_stop_interval and auto_pause_interval are mutually exclusive. Set at most one of them to a non-zero value'
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
if params.auto_pause_interval&.positive? && params.auto_delete_interval&.zero?
|
|
228
|
+
raise Sdk::Error, 'Ephemeral sandboxes cannot have auto-pause enabled. Set auto_pause_interval to 0'
|
|
229
|
+
end
|
|
230
|
+
|
|
203
231
|
if params.auto_archive_interval&.negative?
|
|
204
232
|
raise Sdk::Error, 'auto_archive_interval must be a non-negative integer'
|
|
205
233
|
end
|
|
@@ -214,6 +242,7 @@ module Daytona
|
|
|
214
242
|
public: params.public,
|
|
215
243
|
target: config.target,
|
|
216
244
|
auto_stop_interval: params.auto_stop_interval,
|
|
245
|
+
auto_pause_interval: params.auto_pause_interval,
|
|
217
246
|
auto_archive_interval: params.auto_archive_interval,
|
|
218
247
|
auto_delete_interval: params.auto_delete_interval,
|
|
219
248
|
volumes: params.volumes,
|
|
@@ -322,7 +351,8 @@ module Daytona
|
|
|
322
351
|
sandbox_dto:,
|
|
323
352
|
config:,
|
|
324
353
|
sandbox_api:,
|
|
325
|
-
otel_state: @otel_state
|
|
354
|
+
otel_state: @otel_state,
|
|
355
|
+
analytics_api_url_provider: method(:analytics_api_url)
|
|
326
356
|
)
|
|
327
357
|
end
|
|
328
358
|
|
data/lib/daytona/process.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
# frozen_string_literal: true
|
|
5
5
|
|
|
6
|
+
require 'base64'
|
|
7
|
+
require 'json'
|
|
6
8
|
require 'uri'
|
|
7
9
|
|
|
8
10
|
module Daytona
|
|
@@ -410,19 +412,32 @@ module Daytona
|
|
|
410
412
|
# pty_handle.disconnect
|
|
411
413
|
#
|
|
412
414
|
# @raise [Daytona::Sdk::Error] If the PTY session creation fails or the session ID is already in use.
|
|
413
|
-
def create_pty_session(id:, cwd: nil, envs: nil, pty_size: nil) # rubocop:disable Metrics/MethodLength
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
lazy_start: true
|
|
422
|
-
)
|
|
423
|
-
)
|
|
415
|
+
def create_pty_session(id:, cwd: nil, envs: nil, pty_size: nil) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
416
|
+
cols = pty_size&.cols || 80
|
|
417
|
+
rows = pty_size&.rows || 24
|
|
418
|
+
|
|
419
|
+
preview_link = get_preview_link.call(WS_PORT)
|
|
420
|
+
url = URI.parse(preview_link.url)
|
|
421
|
+
url.scheme = url.scheme == 'https' ? 'wss' : 'ws'
|
|
422
|
+
url.path = '/process/pty/create-connect'
|
|
424
423
|
|
|
425
|
-
|
|
424
|
+
params = { id:, cols:, rows: }
|
|
425
|
+
params[:cwd] = cwd if cwd
|
|
426
|
+
url.query = URI.encode_www_form(params)
|
|
427
|
+
|
|
428
|
+
headers = toolbox_api.api_client.default_headers.dup.merge(
|
|
429
|
+
'X-Daytona-Preview-Token' => preview_link.token
|
|
430
|
+
)
|
|
431
|
+
protocols = [headers['Sec-WebSocket-Protocol'], PTY_EXIT_CONTROL_SUBPROTOCOL].compact
|
|
432
|
+
protocols << "X-Daytona-Pty-Envs~#{Base64.urlsafe_encode64(envs.to_json, padding: false)}" if envs
|
|
433
|
+
headers['Sec-WebSocket-Protocol'] = protocols.join(', ')
|
|
434
|
+
|
|
435
|
+
PtyHandle.new(
|
|
436
|
+
WebSocket::Client::Simple.connect(url.to_s, headers:),
|
|
437
|
+
session_id: id,
|
|
438
|
+
handle_resize: ->(pty_size_arg) { resize_pty_session(id, pty_size_arg) },
|
|
439
|
+
handle_kill: -> { delete_pty_session(id) }
|
|
440
|
+
).tap(&:wait_for_connection)
|
|
426
441
|
end
|
|
427
442
|
|
|
428
443
|
# Connects to an existing PTY session in the Sandbox.
|
|
@@ -448,13 +463,14 @@ module Daytona
|
|
|
448
463
|
url.scheme = url.scheme == 'https' ? 'wss' : 'ws'
|
|
449
464
|
url.path = "/process/pty/#{session_id}/connect"
|
|
450
465
|
|
|
466
|
+
headers = toolbox_api.api_client.default_headers.dup.merge(
|
|
467
|
+
'X-Daytona-Preview-Token' => preview_link.token
|
|
468
|
+
)
|
|
469
|
+
headers['Sec-WebSocket-Protocol'] =
|
|
470
|
+
[headers['Sec-WebSocket-Protocol'], PTY_EXIT_CONTROL_SUBPROTOCOL].compact.join(', ')
|
|
471
|
+
|
|
451
472
|
handle = nil
|
|
452
|
-
WebSocket::Client::Simple.connect(
|
|
453
|
-
url.to_s,
|
|
454
|
-
headers: toolbox_api.api_client.default_headers.dup.merge(
|
|
455
|
-
'X-Daytona-Preview-Token' => preview_link.token
|
|
456
|
-
)
|
|
457
|
-
) do |client|
|
|
473
|
+
WebSocket::Client::Simple.connect(url.to_s, headers:) do |client|
|
|
458
474
|
handle = PtyHandle.new(
|
|
459
475
|
client,
|
|
460
476
|
session_id:,
|
data/lib/daytona/sandbox.rb
CHANGED
|
@@ -3,14 +3,33 @@
|
|
|
3
3
|
|
|
4
4
|
# frozen_string_literal: true
|
|
5
5
|
|
|
6
|
+
require 'time'
|
|
6
7
|
require 'timeout'
|
|
7
8
|
|
|
8
9
|
module Daytona
|
|
10
|
+
# A single point-in-time sample of historical Sandbox resource usage.
|
|
11
|
+
SandboxMetrics = Data.define(
|
|
12
|
+
:cpu_count, :cpu_used_pct, :disk_total, :disk_used,
|
|
13
|
+
:mem_total, :mem_used, :mem_cache, :timestamp
|
|
14
|
+
)
|
|
15
|
+
|
|
9
16
|
class Sandbox # rubocop:disable Metrics/ClassLength
|
|
10
17
|
include Instrumentation
|
|
11
18
|
|
|
12
19
|
DEFAULT_TIMEOUT = 60
|
|
13
20
|
|
|
21
|
+
SANDBOX_METRIC_FIELD_BY_NAME = {
|
|
22
|
+
'daytona.sandbox.cpu.utilization' => :cpu_used_pct,
|
|
23
|
+
'daytona.sandbox.cpu.limit' => :cpu_count,
|
|
24
|
+
'daytona.sandbox.memory.usage' => :mem_used,
|
|
25
|
+
'daytona.sandbox.memory.limit' => :mem_total,
|
|
26
|
+
'daytona.sandbox.memory.cache' => :mem_cache,
|
|
27
|
+
'daytona.sandbox.filesystem.usage' => :disk_used,
|
|
28
|
+
'daytona.sandbox.filesystem.total' => :disk_total
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
SANDBOX_METRIC_NAMES = SANDBOX_METRIC_FIELD_BY_NAME.keys.freeze
|
|
32
|
+
|
|
14
33
|
# @return [String] The ID of the sandbox
|
|
15
34
|
attr_reader :id
|
|
16
35
|
|
|
@@ -79,6 +98,9 @@ module Daytona
|
|
|
79
98
|
# @return [Float] Auto-stop interval in minutes (0 means disabled)
|
|
80
99
|
attr_reader :auto_stop_interval
|
|
81
100
|
|
|
101
|
+
# @return [Float] Auto-pause interval in minutes (0 means disabled)
|
|
102
|
+
attr_reader :auto_pause_interval
|
|
103
|
+
|
|
82
104
|
# @return [Float] Auto-archive interval in minutes
|
|
83
105
|
attr_reader :auto_archive_interval
|
|
84
106
|
|
|
@@ -132,11 +154,12 @@ module Daytona
|
|
|
132
154
|
# @params sandbox_api [DaytonaApiClient::SandboxApi]
|
|
133
155
|
# @params sandbox_dto [DaytonaApiClient::Sandbox, DaytonaApiClient::SandboxListItem]
|
|
134
156
|
# @params otel_state [Daytona::OtelState, nil]
|
|
135
|
-
def initialize(sandbox_dto:, config:, sandbox_api:, otel_state: nil) # rubocop:disable Metrics/MethodLength
|
|
157
|
+
def initialize(sandbox_dto:, config:, sandbox_api:, otel_state: nil, analytics_api_url_provider: nil) # rubocop:disable Metrics/MethodLength
|
|
136
158
|
process_response(sandbox_dto)
|
|
137
159
|
@config = config
|
|
138
160
|
@sandbox_api = sandbox_api
|
|
139
161
|
@otel_state = otel_state
|
|
162
|
+
@analytics_api_url_provider = analytics_api_url_provider
|
|
140
163
|
|
|
141
164
|
# Create toolbox API clients with dynamic configuration
|
|
142
165
|
toolbox_api_config = build_toolbox_api_config
|
|
@@ -160,6 +183,7 @@ module Daytona
|
|
|
160
183
|
interpreter_api = DaytonaToolboxApiClient::InterpreterApi.new(create_authenticated_client.call)
|
|
161
184
|
info_api = DaytonaToolboxApiClient::InfoApi.new(create_authenticated_client.call)
|
|
162
185
|
server_api = DaytonaToolboxApiClient::ServerApi.new(create_authenticated_client.call)
|
|
186
|
+
system_api = DaytonaToolboxApiClient::SystemApi.new(create_authenticated_client.call)
|
|
163
187
|
|
|
164
188
|
@process = Process.new(
|
|
165
189
|
sandbox_id: id,
|
|
@@ -180,6 +204,7 @@ module Daytona
|
|
|
180
204
|
@lsp_api = lsp_api
|
|
181
205
|
@info_api = info_api
|
|
182
206
|
@server_api = server_api
|
|
207
|
+
@system_api = system_api
|
|
183
208
|
end
|
|
184
209
|
|
|
185
210
|
# Archives the sandbox, making it inactive and preserving its state. When sandboxes are
|
|
@@ -278,6 +303,21 @@ module Daytona
|
|
|
278
303
|
@auto_stop_interval = interval
|
|
279
304
|
end
|
|
280
305
|
|
|
306
|
+
# Sets the auto-pause interval for the Sandbox.
|
|
307
|
+
# The Sandbox will automatically pause after being idle (no new events) for the specified interval.
|
|
308
|
+
# Events include any state changes or interactions with the Sandbox through the SDK.
|
|
309
|
+
# Interactions using Sandbox Previews are not included.
|
|
310
|
+
#
|
|
311
|
+
# @param interval [Integer]
|
|
312
|
+
# @return [Integer]
|
|
313
|
+
# @raise [Daytona:Sdk::Error]
|
|
314
|
+
def auto_pause_interval=(interval)
|
|
315
|
+
raise Sdk::Error, 'Auto-pause interval must be a non-negative integer' if interval.negative?
|
|
316
|
+
|
|
317
|
+
sandbox_api.set_auto_pause_interval(id, interval)
|
|
318
|
+
@auto_pause_interval = interval
|
|
319
|
+
end
|
|
320
|
+
|
|
281
321
|
# Creates an SSH access token for the sandbox.
|
|
282
322
|
#
|
|
283
323
|
# @param expires_in_minutes [Integer] TThe number of minutes the SSH access token will be valid for
|
|
@@ -338,6 +378,42 @@ module Daytona
|
|
|
338
378
|
raise Sdk::Error, "Failed to update environment: #{e.message}"
|
|
339
379
|
end
|
|
340
380
|
|
|
381
|
+
# Gets the most recent resource usage sample directly from the Sandbox daemon.
|
|
382
|
+
#
|
|
383
|
+
# Unlike #get_metrics, which returns aggregated historical samples, this returns the
|
|
384
|
+
# single current reading without going through the telemetry backend.
|
|
385
|
+
#
|
|
386
|
+
# @return [Daytona::SandboxMetrics]
|
|
387
|
+
#
|
|
388
|
+
# @example
|
|
389
|
+
# m = sandbox.get_metrics_latest
|
|
390
|
+
# puts "CPU used: #{m.cpu_used_pct}%"
|
|
391
|
+
def get_metrics_latest
|
|
392
|
+
sandbox_metrics_from_system_metrics(@system_api.get_system_metrics)
|
|
393
|
+
rescue StandardError => e
|
|
394
|
+
raise Sdk::Error, "Failed to get system metrics: #{e.message}"
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Gets historical time-series resource usage metrics for the Sandbox.
|
|
398
|
+
#
|
|
399
|
+
# @param start_time [Time, nil] Start of the range. Defaults to the Sandbox creation time.
|
|
400
|
+
# @param end_time [Time, nil] End of the range. Defaults to the current time.
|
|
401
|
+
# @return [Array<Daytona::SandboxMetrics>] Time-ordered usage samples.
|
|
402
|
+
#
|
|
403
|
+
# @example
|
|
404
|
+
# sandbox.get_metrics.each { |m| puts "#{m.timestamp}: #{m.cpu_used_pct}%" }
|
|
405
|
+
def get_metrics(start_time = nil, end_time = nil)
|
|
406
|
+
from, to = metrics_range(start_time, end_time)
|
|
407
|
+
analytics_api_url = fetch_analytics_api_url
|
|
408
|
+
if analytics_api_url && !analytics_api_url.empty?
|
|
409
|
+
pivot_sandbox_metric_points(fetch_analytics_metrics(analytics_api_url, from, to))
|
|
410
|
+
else
|
|
411
|
+
pivot_sandbox_metrics(fetch_proxy_metrics(from, to))
|
|
412
|
+
end
|
|
413
|
+
rescue StandardError => e
|
|
414
|
+
raise Sdk::Error, "Failed to get sandbox metrics: #{e.message}"
|
|
415
|
+
end
|
|
416
|
+
|
|
341
417
|
# Sets labels for the Sandbox.
|
|
342
418
|
#
|
|
343
419
|
# @param labels [Hash<String, String>]
|
|
@@ -568,7 +644,8 @@ module Daytona
|
|
|
568
644
|
config:,
|
|
569
645
|
sandbox_api:,
|
|
570
646
|
code_toolbox:,
|
|
571
|
-
otel_state
|
|
647
|
+
otel_state:,
|
|
648
|
+
analytics_api_url_provider: @analytics_api_url_provider
|
|
572
649
|
)
|
|
573
650
|
forked.send(:wait_for_states, operation: OPERATION_START,
|
|
574
651
|
target_states: [DaytonaApiClient::SandboxState::STARTED])
|
|
@@ -609,9 +686,10 @@ module Daytona
|
|
|
609
686
|
) { wait_for_pause_complete }
|
|
610
687
|
end
|
|
611
688
|
|
|
612
|
-
instrument :archive, :auto_archive_interval=, :auto_delete_interval=, :auto_stop_interval=,
|
|
689
|
+
instrument :archive, :auto_archive_interval=, :auto_delete_interval=, :auto_pause_interval=, :auto_stop_interval=,
|
|
613
690
|
:update_network_settings, :update_secrets, :update_env,
|
|
614
|
-
:create_ssh_access, :delete, :get_user_home_dir, :get_work_dir, :
|
|
691
|
+
:create_ssh_access, :delete, :get_user_home_dir, :get_work_dir, :get_metrics, :get_metrics_latest,
|
|
692
|
+
:labels=,
|
|
615
693
|
:preview_url, :create_signed_preview_url, :expire_signed_preview_url,
|
|
616
694
|
:refresh, :refresh_activity, :revoke_ssh_access, :start, :recover, :stop,
|
|
617
695
|
:create_lsp_server, :validate_ssh_access, :wait_for_sandbox_start,
|
|
@@ -621,6 +699,82 @@ module Daytona
|
|
|
621
699
|
|
|
622
700
|
private
|
|
623
701
|
|
|
702
|
+
def pivot_sandbox_metrics(series)
|
|
703
|
+
buckets = {}
|
|
704
|
+
Array(series).each do |s|
|
|
705
|
+
Array(s.data_points).each { |point| add_metric_point(buckets, s.metric_name, point.timestamp, point.value) }
|
|
706
|
+
end
|
|
707
|
+
build_sandbox_metrics(buckets)
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def add_metric_point(buckets, name, timestamp, value)
|
|
711
|
+
field = SANDBOX_METRIC_FIELD_BY_NAME[name]
|
|
712
|
+
return if field.nil? || timestamp.nil? || timestamp.to_s.empty? || value.nil?
|
|
713
|
+
|
|
714
|
+
(buckets[timestamp] ||= {})[field] = value
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def build_sandbox_metrics(buckets)
|
|
718
|
+
buckets.keys.sort.map { |ts| build_sandbox_metric(ts, buckets[ts]) }
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def build_sandbox_metric(timestamp, values)
|
|
722
|
+
SandboxMetrics.new(
|
|
723
|
+
cpu_count: values.fetch(:cpu_count, 0).to_i,
|
|
724
|
+
cpu_used_pct: values.fetch(:cpu_used_pct, 0).to_f,
|
|
725
|
+
disk_total: values.fetch(:disk_total, 0).to_i,
|
|
726
|
+
disk_used: values.fetch(:disk_used, 0).to_i,
|
|
727
|
+
mem_total: values.fetch(:mem_total, 0).to_i,
|
|
728
|
+
mem_used: values.fetch(:mem_used, 0).to_i,
|
|
729
|
+
mem_cache: values.fetch(:mem_cache, 0).to_i,
|
|
730
|
+
timestamp: Time.parse(timestamp)
|
|
731
|
+
)
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
def pivot_sandbox_metric_points(points)
|
|
735
|
+
buckets = {}
|
|
736
|
+
Array(points).each { |point| add_metric_point(buckets, point.metric_name, point.timestamp, point.value) }
|
|
737
|
+
build_sandbox_metrics(buckets)
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
def sandbox_metrics_from_system_metrics(system_metrics)
|
|
741
|
+
values = SANDBOX_METRIC_FIELD_BY_NAME.values.to_h { |field| [field, system_metrics.send(field)] }
|
|
742
|
+
build_sandbox_metric(system_metrics.timestamp || Time.now.iso8601, values)
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def metrics_range(start_time, end_time)
|
|
746
|
+
to = end_time || Time.now
|
|
747
|
+
from = start_time || (created_at ? Time.parse(created_at) : to)
|
|
748
|
+
[from, to]
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
def fetch_analytics_api_url
|
|
752
|
+
return @analytics_api_url_provider.call if @analytics_api_url_provider
|
|
753
|
+
|
|
754
|
+
DaytonaApiClient::ConfigApi.new(sandbox_api.api_client).config_controller_get_config.analytics_api_url
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
def fetch_proxy_metrics(from, to)
|
|
758
|
+
sandbox_api.get_sandbox_metrics(id, from, to, metric_names: SANDBOX_METRIC_NAMES).series
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def fetch_analytics_metrics(analytics_api_url, from, to)
|
|
762
|
+
analytics_telemetry_api(analytics_api_url).organization_organization_id_sandbox_sandbox_id_telemetry_metrics_get(
|
|
763
|
+
organization_id, id, from.utc.iso8601, to.utc.iso8601, metric_names: SANDBOX_METRIC_NAMES.join(',')
|
|
764
|
+
)
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def analytics_telemetry_api(analytics_api_url) # rubocop:disable Metrics/AbcSize
|
|
768
|
+
uri = URI(analytics_api_url)
|
|
769
|
+
config = DaytonaAnalyticsApiClient::Configuration.new
|
|
770
|
+
config.scheme = uri.scheme
|
|
771
|
+
config.host = uri.authority
|
|
772
|
+
config.base_path = uri.path
|
|
773
|
+
config.api_key['Authorization'] = sandbox_api.api_client.config.access_token_getter&.call
|
|
774
|
+
config.api_key_prefix['Authorization'] = 'Bearer'
|
|
775
|
+
DaytonaAnalyticsApiClient::TelemetryApi.new(DaytonaAnalyticsApiClient::ApiClient.new(config))
|
|
776
|
+
end
|
|
777
|
+
|
|
624
778
|
# @return [Daytona::OtelState, nil]
|
|
625
779
|
attr_reader :otel_state
|
|
626
780
|
|
|
@@ -661,6 +815,7 @@ module Daytona
|
|
|
661
815
|
@error_reason = sandbox_dto.error_reason
|
|
662
816
|
@backup_state = sandbox_dto.backup_state
|
|
663
817
|
@auto_stop_interval = sandbox_dto.auto_stop_interval
|
|
818
|
+
@auto_pause_interval = sandbox_dto.auto_pause_interval
|
|
664
819
|
@auto_archive_interval = sandbox_dto.auto_archive_interval
|
|
665
820
|
@auto_delete_interval = sandbox_dto.auto_delete_interval
|
|
666
821
|
@created_at = sandbox_dto.created_at
|
data/lib/daytona/sdk/version.rb
CHANGED
data/lib/daytona/sdk.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: daytona
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.197.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daytona Platforms Inc.
|
|
@@ -85,28 +85,28 @@ dependencies:
|
|
|
85
85
|
requirements:
|
|
86
86
|
- - '='
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: 0.
|
|
88
|
+
version: 0.197.0
|
|
89
89
|
type: :runtime
|
|
90
90
|
prerelease: false
|
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
92
|
requirements:
|
|
93
93
|
- - '='
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: 0.
|
|
95
|
+
version: 0.197.0
|
|
96
96
|
- !ruby/object:Gem::Dependency
|
|
97
97
|
name: daytona_toolbox_api_client
|
|
98
98
|
requirement: !ruby/object:Gem::Requirement
|
|
99
99
|
requirements:
|
|
100
100
|
- - '='
|
|
101
101
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: 0.
|
|
102
|
+
version: 0.197.0
|
|
103
103
|
type: :runtime
|
|
104
104
|
prerelease: false
|
|
105
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
106
|
requirements:
|
|
107
107
|
- - '='
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: 0.
|
|
109
|
+
version: 0.197.0
|
|
110
110
|
- !ruby/object:Gem::Dependency
|
|
111
111
|
name: dotenv
|
|
112
112
|
requirement: !ruby/object:Gem::Requirement
|