daytona 0.126.0.alpha.6

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.
@@ -0,0 +1,404 @@
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 [Array<DaytonaApiClient::SandboxVolume>] Array of volumes attached to the sandbox
77
+ attr_reader :volumes
78
+
79
+ # @return [DaytonaApiClient::BuildInfo] Build information for the sandbox
80
+ attr_reader :build_info
81
+
82
+ # @return [String] The creation timestamp of the sandbox
83
+ attr_reader :created_at
84
+
85
+ # @return [String] The last update timestamp of the sandbox
86
+ attr_reader :updated_at
87
+
88
+ # @return [String] The version of the daemon running in the sandbox
89
+ attr_reader :daemon_version
90
+
91
+ # @return [Daytona::SandboxPythonCodeToolbox, Daytona::SandboxTsCodeToolbox]
92
+ attr_reader :code_toolbox
93
+
94
+ # @return [Daytona::Config]
95
+ attr_reader :config
96
+
97
+ # @return [DaytonaApiClient::SandboxApi]
98
+ attr_reader :sandbox_api
99
+
100
+ # @return [Daytona::Process]
101
+ attr_reader :process
102
+
103
+ # @return [Daytona::FileSystem]
104
+ attr_reader :fs
105
+
106
+ # @return [Daytona::Git]
107
+ attr_reader :git
108
+
109
+ # @return [Daytona::ComputerUse]
110
+ attr_reader :computer_use
111
+
112
+ # @params code_toolbox [Daytona::SandboxPythonCodeToolbox, Daytona::SandboxTsCodeToolbox]
113
+ # @params config [Daytona::Config]
114
+ # @params sandbox_api [DaytonaApiClient::SandboxApi]
115
+ # @params sandbox_dto [DaytonaApiClient::Sandbox]
116
+ def initialize(code_toolbox:, sandbox_dto:, config:, sandbox_api:, get_proxy_toolbox_url:) # rubocop:disable Metrics/MethodLength
117
+ process_response(sandbox_dto)
118
+ @code_toolbox = code_toolbox
119
+ @config = config
120
+ @sandbox_api = sandbox_api
121
+ @get_proxy_toolbox_url = get_proxy_toolbox_url
122
+
123
+ # Create toolbox API clients with dynamic configuration
124
+ toolbox_api_config = build_toolbox_api_config
125
+
126
+ # Helper to create API client with authentication header
127
+ create_authenticated_client = lambda do
128
+ client = DaytonaToolboxApiClient::ApiClient.new(toolbox_api_config)
129
+ client.default_headers['Authorization'] = "Bearer #{config.api_key || config.jwt_token}"
130
+ client
131
+ end
132
+
133
+ process_api = DaytonaToolboxApiClient::ProcessApi.new(create_authenticated_client.call)
134
+ fs_api = DaytonaToolboxApiClient::FileSystemApi.new(create_authenticated_client.call)
135
+ git_api = DaytonaToolboxApiClient::GitApi.new(create_authenticated_client.call)
136
+ lsp_api = DaytonaToolboxApiClient::LspApi.new(create_authenticated_client.call)
137
+ computer_use_api = DaytonaToolboxApiClient::ComputerUseApi.new(create_authenticated_client.call)
138
+
139
+ @process = Process.new(
140
+ sandbox_id: id,
141
+ code_toolbox:,
142
+ toolbox_api: process_api,
143
+ get_preview_link: proc { |port| preview_url(port) }
144
+ )
145
+ @fs = FileSystem.new(sandbox_id: id, toolbox_api: fs_api)
146
+ @git = Git.new(sandbox_id: id, toolbox_api: git_api)
147
+ @computer_use = ComputerUse.new(sandbox_id: id, toolbox_api: computer_use_api)
148
+ @lsp_api = lsp_api
149
+ end
150
+
151
+ # Archives the sandbox, making it inactive and preserving its state. When sandboxes are
152
+ # archived, the entire filesystem state is moved to cost-effective object storage, making it
153
+ # possible to keep sandboxes available for an extended period. The tradeoff between archived
154
+ # and stopped states is that starting an archived sandbox takes more time, depending on its size.
155
+ # Sandbox must be stopped before archiving.
156
+ #
157
+ # @return [void]
158
+ def archive
159
+ sandbox_api.archive_sandbox(id)
160
+ refresh
161
+ end
162
+
163
+ # Sets the auto-archive interval for the Sandbox.
164
+ # The Sandbox will automatically archive after being continuously stopped for the specified interval.
165
+ #
166
+ # @param interval [Integer]
167
+ # @return [Integer]
168
+ # @raise [Daytona:Sdk::Error]
169
+ def auto_archive_interval=(interval)
170
+ raise Sdk::Error, 'Auto-archive interval must be a non-negative integer' if interval.negative?
171
+
172
+ sandbox_api.set_auto_archive_interval(id, interval)
173
+ @auto_archive_interval = interval
174
+ end
175
+
176
+ # Sets the auto-delete interval for the Sandbox.
177
+ # The Sandbox will automatically delete after being continuously stopped for the specified interval.
178
+ #
179
+ # @param interval [Integer]
180
+ # @return [Integer]
181
+ # @raise [Daytona:Sdk::Error]
182
+ def auto_delete_interval=(interval)
183
+ sandbox_api.set_auto_delete_interval(id, interval)
184
+ @auto_delete_interval = interval
185
+ end
186
+
187
+ # Sets the auto-stop interval for the Sandbox.
188
+ # The Sandbox will automatically stop after being idle (no new events) for the specified interval.
189
+ # Events include any state changes or interactions with the Sandbox through the SDK.
190
+ # Interactions using Sandbox Previews are not included.
191
+ #
192
+ # @param interval [Integer]
193
+ # @return [Integer]
194
+ # @raise [Daytona:Sdk::Error]
195
+ def auto_stop_interval=(interval)
196
+ raise Sdk::Error, 'Auto-stop interval must be a non-negative integer' if interval.negative?
197
+
198
+ sandbox_api.set_autostop_interval(id, interval)
199
+ @auto_stop_interval = interval
200
+ end
201
+
202
+ # Creates an SSH access token for the sandbox.
203
+ #
204
+ # @param expires_in_minutes [Integer] TThe number of minutes the SSH access token will be valid for
205
+ # @return [DaytonaApiClient::SshAccessDto]
206
+ def create_ssh_access(expires_in_minutes) = sandbox_api.create_ssh_access(id, { expires_in_minutes: })
207
+
208
+ # @return [void]
209
+ def delete
210
+ sandbox_api.delete_sandbox(id)
211
+ refresh
212
+ end
213
+
214
+ # Sets labels for the Sandbox.
215
+ #
216
+ # @param labels [Hash<String, String>]
217
+ # @return [Hash<String, String>]
218
+ def labels=(labels)
219
+ @labels = sandbox_api.replace_labels(id, DaytonaApiClient::SandboxLabels.build_from_hash(labels:)).labels
220
+ end
221
+
222
+ # Retrieves the preview link for the sandbox at the specified port. If the port is closed,
223
+ # it will be opened automatically. For private sandboxes, a token is included to grant access
224
+ # to the URL.
225
+ #
226
+ # @param port [Integer]
227
+ # @return [DaytonaApiClient::PortPreviewUrl]
228
+ def preview_url(port) = sandbox_api.get_port_preview_url(id, port)
229
+
230
+ # Refresh the Sandbox data from the API.
231
+ #
232
+ # @return [void]
233
+ def refresh = process_response(sandbox_api.get_sandbox(id))
234
+
235
+ # Revokes an SSH access token for the sandbox.
236
+ #
237
+ # @param token [String]
238
+ # @return [void]
239
+ def revoke_ssh_access(token) = sandbox_api.revoke_ssh_access(id, token:)
240
+
241
+ # Starts the Sandbox and waits for it to be ready.
242
+ #
243
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
244
+ # @return [void]
245
+ def start(timeout = DEFAULT_TIMEOUT)
246
+ with_timeout(
247
+ timeout:,
248
+ message: "Sandbox #{id} failed to become ready within the #{timeout} seconds timeout period",
249
+ setup: proc { process_response(sandbox_api.start_sandbox(id)) }
250
+ ) { wait_for_states(operation: OPERATION_START, target_states: [DaytonaApiClient::SandboxState::STARTED]) }
251
+ end
252
+
253
+ # Stops the Sandbox and waits for it to be stopped.
254
+ #
255
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
256
+ # @return [void]
257
+ def stop(timeout = DEFAULT_TIMEOUT) # rubocop:disable Metrics/MethodLength
258
+ with_timeout(
259
+ timeout:,
260
+ message: "Sandbox #{id} failed to become stopped within the #{timeout} seconds timeout period",
261
+ setup: proc {
262
+ sandbox_api.stop_sandbox(id)
263
+ refresh
264
+ }
265
+ ) do
266
+ wait_for_states(
267
+ operation: OPERATION_STOP,
268
+ target_states: [DaytonaApiClient::SandboxState::STOPPED, DaytonaApiClient::SandboxState::DESTROYED]
269
+ )
270
+ end
271
+ end
272
+
273
+ # Creates a new Language Server Protocol (LSP) server instance.
274
+ # The LSP server provides language-specific features like code completion,
275
+ # diagnostics, and more.
276
+ #
277
+ # @param language_id [Symbol] The language server type (e.g., Daytona::LspServer::Language::PYTHON)
278
+ # @param path_to_project [String] Path to the project root directory. Relative paths are resolved
279
+ # based on the sandbox working directory.
280
+ # @return [Daytona::LspServer]
281
+ def create_lsp_server(language_id:, path_to_project:)
282
+ LspServer.new(language_id:, path_to_project:, toolbox_api: @lsp_api, sandbox_id: id)
283
+ end
284
+
285
+ # Validates an SSH access token for the sandbox.
286
+ #
287
+ # @param token [String]
288
+ # @return [DaytonaApiClient::SshAccessValidationDto]
289
+ def validate_ssh_access(token) = sandbox_api.validate_ssh_access(token)
290
+
291
+ # Waits for the Sandbox to reach the 'started' state. Polls the Sandbox status until it
292
+ # reaches the 'started' state or encounters an error.
293
+ #
294
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
295
+ # @return [void]
296
+ def wait_for_sandbox_start(_timeout = DEFAULT_TIMEOUT)
297
+ wait_for_states(operation: OPERATION_START, target_states: [DaytonaApiClient::SandboxState::STARTED])
298
+ end
299
+
300
+ private
301
+
302
+ # Build toolbox API configuration with dynamic base URL from preview link
303
+ # @return [DaytonaToolboxApiClient::Configuration]
304
+ def build_toolbox_api_config
305
+ DaytonaToolboxApiClient::Configuration.new.configure do |cfg|
306
+ # Get the proxy toolbox URL and append sandbox ID
307
+ proxy_toolbox_url = @get_proxy_toolbox_url.call
308
+ proxy_toolbox_url += '/' unless proxy_toolbox_url.end_with?('/')
309
+ full_url = "#{proxy_toolbox_url}#{id}"
310
+ uri = URI(full_url)
311
+
312
+ cfg.scheme = uri.scheme
313
+ cfg.host = uri.host
314
+ cfg.base_path = uri.path.empty? ? '/' : uri.path
315
+
316
+ cfg
317
+ end
318
+ end
319
+
320
+ # @params sandbox_dto [DaytonaApiClient::Sandbox]
321
+ # @return [void]
322
+ def process_response(sandbox_dto) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
323
+ @id = sandbox_dto.id
324
+ @organization_id = sandbox_dto.organization_id
325
+ @snapshot = sandbox_dto.snapshot
326
+ @user = sandbox_dto.user
327
+ @env = sandbox_dto.env
328
+ @labels = sandbox_dto.labels
329
+ @public = sandbox_dto.public
330
+ @target = sandbox_dto.target
331
+ @cpu = sandbox_dto.cpu
332
+ @gpu = sandbox_dto.gpu
333
+ @memory = sandbox_dto.memory
334
+ @disk = sandbox_dto.disk
335
+ @state = sandbox_dto.state
336
+ @desired_state = sandbox_dto.desired_state
337
+ @error_reason = sandbox_dto.error_reason
338
+ @backup_state = sandbox_dto.backup_state
339
+ @backup_created_at = sandbox_dto.backup_created_at
340
+ @auto_stop_interval = sandbox_dto.auto_stop_interval
341
+ @auto_archive_interval = sandbox_dto.auto_archive_interval
342
+ @auto_delete_interval = sandbox_dto.auto_delete_interval
343
+ @volumes = sandbox_dto.volumes
344
+ @build_info = sandbox_dto.build_info
345
+ @created_at = sandbox_dto.created_at
346
+ @updated_at = sandbox_dto.updated_at
347
+ @daemon_version = sandbox_dto.daemon_version
348
+ @network_block_all = sandbox_dto.network_block_all
349
+ @network_allow_list = sandbox_dto.network_allow_list
350
+ end
351
+
352
+ # Monitors block not to exceed max execution time.
353
+ #
354
+ # @param setup [#call, Nil] Optional setup block
355
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
356
+ # @param message [String] Error message
357
+ # @return [void]
358
+ # @raise [Daytona::Sdk::Error]
359
+ def with_timeout(message:, setup:, timeout: DEFAULT_TIMEOUT, &)
360
+ start_at = Time.now
361
+ setup&.call
362
+
363
+ Timeout.timeout(
364
+ setup ? [NO_TIMEOUT, timeout - (Time.now - start_at)].max : timeout,
365
+ Sdk::Error,
366
+ message,
367
+ &
368
+ )
369
+ end
370
+
371
+ # Waits for the Sandbox to reach the one of the target states. Polls the Sandbox status until it
372
+ # reaches the one of the target states or encounters an error. It will wait up to 60 seconds
373
+ # for the Sandbox to reach one of the target states.
374
+ #
375
+ # @param operation [#to_s] Operation name for error message
376
+ # @param target_states [Array<DaytonaApiClient::SandboxState>] List of the target states
377
+ # @return [void]
378
+ # @raise [Daytona::Sdk::Error]
379
+ def wait_for_states(operation:, target_states:)
380
+ loop do
381
+ case state
382
+ when *target_states then return
383
+ when DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED
384
+ raise Sdk::Error, "Sandbox #{id} failed to #{operation} with state: #{state}, error reason: #{error_reason}"
385
+ end
386
+
387
+ sleep(IDLE_DURATION)
388
+ refresh
389
+ end
390
+ end
391
+
392
+ IDLE_DURATION = 0.1
393
+ private_constant :IDLE_DURATION
394
+
395
+ NO_TIMEOUT = 0
396
+ private_constant :NO_TIMEOUT
397
+
398
+ OPERATION_START = :start
399
+ private_constant :OPERATION_START
400
+
401
+ OPERATION_STOP = :stop
402
+ private_constant :OPERATION_STOP
403
+ end
404
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Daytona
4
+ module Sdk
5
+ VERSION = '0.126.0.alpha.6'
6
+ end
7
+ end
@@ -0,0 +1,46 @@
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 'daytona_toolbox_api_client'
9
+ require 'toml'
10
+ require 'websocket-client-simple'
11
+
12
+ require_relative 'sdk/version'
13
+ require_relative 'config'
14
+ require_relative 'common/charts'
15
+ require_relative 'common/code_language'
16
+ require_relative 'common/daytona'
17
+ require_relative 'common/file_system'
18
+ require_relative 'common/image'
19
+ require_relative 'common/git'
20
+ require_relative 'common/process'
21
+ require_relative 'common/pty'
22
+ require_relative 'common/resources'
23
+ require_relative 'common/response'
24
+ require_relative 'common/snapshot'
25
+ require_relative 'computer_use'
26
+ require_relative 'code_toolbox/sandbox_python_code_toolbox'
27
+ require_relative 'code_toolbox/sandbox_ts_code_toolbox'
28
+ require_relative 'daytona'
29
+ require_relative 'file_system'
30
+ require_relative 'git'
31
+ require_relative 'lsp_server'
32
+ require_relative 'object_storage'
33
+ require_relative 'sandbox'
34
+ require_relative 'snapshot_service'
35
+ require_relative 'util'
36
+ require_relative 'volume'
37
+ require_relative 'volume_service'
38
+ require_relative 'process'
39
+
40
+ module Daytona
41
+ module Sdk
42
+ class Error < StandardError; end
43
+
44
+ def self.logger = @logger ||= Logger.new($stdout, level: Logger::INFO)
45
+ end
46
+ 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