nightona 0.191.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/.rspec +3 -0
- data/.rubocop.yml +22 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +190 -0
- data/README.md +184 -0
- data/Rakefile +12 -0
- data/lib/nightona/code_interpreter.rb +359 -0
- data/lib/nightona/common/charts.rb +124 -0
- data/lib/nightona/common/code_interpreter.rb +56 -0
- data/lib/nightona/common/code_language.rb +14 -0
- data/lib/nightona/common/file_system.rb +26 -0
- data/lib/nightona/common/git.rb +19 -0
- data/lib/nightona/common/image.rb +500 -0
- data/lib/nightona/common/nightona.rb +230 -0
- data/lib/nightona/common/process.rb +149 -0
- data/lib/nightona/common/pty.rb +309 -0
- data/lib/nightona/common/resources.rb +39 -0
- data/lib/nightona/common/response.rb +83 -0
- data/lib/nightona/common/snapshot.rb +124 -0
- data/lib/nightona/computer_use.rb +919 -0
- data/lib/nightona/config.rb +116 -0
- data/lib/nightona/file_system.rb +451 -0
- data/lib/nightona/file_transfer.rb +383 -0
- data/lib/nightona/git.rb +334 -0
- data/lib/nightona/lsp_server.rb +139 -0
- data/lib/nightona/nightona.rb +336 -0
- data/lib/nightona/object_storage.rb +172 -0
- data/lib/nightona/otel.rb +183 -0
- data/lib/nightona/process.rb +550 -0
- data/lib/nightona/sandbox.rb +751 -0
- data/lib/nightona/sdk/version.rb +10 -0
- data/lib/nightona/sdk.rb +56 -0
- data/lib/nightona/snapshot_service.rb +238 -0
- data/lib/nightona/util.rb +80 -0
- data/lib/nightona/volume.rb +46 -0
- data/lib/nightona/volume_service.rb +61 -0
- data/lib/nightona.rb +10 -0
- data/project.json +100 -0
- data/scripts/generate-docs.rb +402 -0
- data/sig/nightona/sdk.rbs +6 -0
- metadata +242 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
# Copyright Daytona Platforms Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
require 'timeout'
|
|
7
|
+
|
|
8
|
+
module Nightona
|
|
9
|
+
class Sandbox # rubocop:disable Metrics/ClassLength
|
|
10
|
+
include Instrumentation
|
|
11
|
+
|
|
12
|
+
DEFAULT_TIMEOUT = 60
|
|
13
|
+
|
|
14
|
+
# @return [String] The ID of the sandbox
|
|
15
|
+
attr_reader :id
|
|
16
|
+
|
|
17
|
+
# @return [String] The organization ID of the sandbox
|
|
18
|
+
attr_reader :organization_id
|
|
19
|
+
|
|
20
|
+
# @return [String] The snapshot used for the sandbox
|
|
21
|
+
attr_reader :snapshot
|
|
22
|
+
|
|
23
|
+
# @return [String] The user associated with the project
|
|
24
|
+
attr_reader :user
|
|
25
|
+
|
|
26
|
+
# @return [Hash<String, String>, nil] Environment variables for the sandbox.
|
|
27
|
+
# Not returned by list results; call #refresh on each item to populate.
|
|
28
|
+
attr_reader :env
|
|
29
|
+
|
|
30
|
+
# @return [Hash<String, String>] Labels for the sandbox
|
|
31
|
+
attr_reader :labels
|
|
32
|
+
|
|
33
|
+
# @return [Boolean] Whether the sandbox http preview is public
|
|
34
|
+
attr_reader :public
|
|
35
|
+
|
|
36
|
+
# @return [Boolean, nil] Whether to block all network access for the sandbox.
|
|
37
|
+
# Not returned by list results; call #refresh on each item to populate.
|
|
38
|
+
attr_reader :network_block_all
|
|
39
|
+
|
|
40
|
+
# @return [String, nil] Comma-separated list of allowed CIDR network addresses for the sandbox.
|
|
41
|
+
# Not returned by list results; call #refresh on each item to populate.
|
|
42
|
+
attr_reader :network_allow_list
|
|
43
|
+
|
|
44
|
+
# @return [String, nil] Comma-separated list of allowed domains for the sandbox.
|
|
45
|
+
# Not returned by list results; call #refresh on each item to populate.
|
|
46
|
+
attr_reader :domain_allow_list
|
|
47
|
+
|
|
48
|
+
# @return [String] The target environment for the sandbox
|
|
49
|
+
attr_reader :target
|
|
50
|
+
|
|
51
|
+
# @return [Float] The CPU quota for the sandbox
|
|
52
|
+
attr_reader :cpu
|
|
53
|
+
|
|
54
|
+
# @return [Float] The GPU quota for the sandbox
|
|
55
|
+
attr_reader :gpu
|
|
56
|
+
|
|
57
|
+
# @return [Float] The memory quota for the sandbox
|
|
58
|
+
attr_reader :memory
|
|
59
|
+
|
|
60
|
+
# @return [Float] The disk quota for the sandbox
|
|
61
|
+
attr_reader :disk
|
|
62
|
+
|
|
63
|
+
# @return [NightonaApiClient::SandboxState] The state of the sandbox
|
|
64
|
+
attr_reader :state
|
|
65
|
+
|
|
66
|
+
# @return [NightonaApiClient::SandboxDesiredState] The desired state of the sandbox
|
|
67
|
+
attr_reader :desired_state
|
|
68
|
+
|
|
69
|
+
# @return [String] The error reason of the sandbox
|
|
70
|
+
attr_reader :error_reason
|
|
71
|
+
|
|
72
|
+
# @return [String] The state of the backup
|
|
73
|
+
attr_reader :backup_state
|
|
74
|
+
|
|
75
|
+
# @return [String, nil] The creation timestamp of the last backup.
|
|
76
|
+
# Not returned by list results; call #refresh on each item to populate.
|
|
77
|
+
attr_reader :backup_created_at
|
|
78
|
+
|
|
79
|
+
# @return [Float] Auto-stop interval in minutes (0 means disabled)
|
|
80
|
+
attr_reader :auto_stop_interval
|
|
81
|
+
|
|
82
|
+
# @return [Float] Auto-archive interval in minutes
|
|
83
|
+
attr_reader :auto_archive_interval
|
|
84
|
+
|
|
85
|
+
# @return [Float] Auto-delete interval in minutes
|
|
86
|
+
# (negative value means disabled, 0 means delete immediately upon stopping)
|
|
87
|
+
attr_reader :auto_delete_interval
|
|
88
|
+
|
|
89
|
+
# @return [Array<NightonaApiClient::SandboxVolume>, nil] Volumes attached to the sandbox.
|
|
90
|
+
# Not returned by list results; call #refresh on each item to populate.
|
|
91
|
+
attr_reader :volumes
|
|
92
|
+
|
|
93
|
+
# @return [NightonaApiClient::BuildInfo, nil] Build information for the sandbox if it was
|
|
94
|
+
# created from a dynamic build.
|
|
95
|
+
# Not returned by list results; call #refresh on each item to populate.
|
|
96
|
+
attr_reader :build_info
|
|
97
|
+
|
|
98
|
+
# @return [String] The creation timestamp of the sandbox
|
|
99
|
+
attr_reader :created_at
|
|
100
|
+
|
|
101
|
+
# @return [String] The last update timestamp of the sandbox
|
|
102
|
+
attr_reader :updated_at
|
|
103
|
+
|
|
104
|
+
# @return [String] The last activity timestamp of the sandbox
|
|
105
|
+
attr_reader :last_activity_at
|
|
106
|
+
|
|
107
|
+
# @return [String] The version of the daemon running in the sandbox
|
|
108
|
+
attr_reader :daemon_version
|
|
109
|
+
|
|
110
|
+
# @return [Nightona::Config]
|
|
111
|
+
attr_reader :config
|
|
112
|
+
|
|
113
|
+
# @return [NightonaApiClient::SandboxApi]
|
|
114
|
+
attr_reader :sandbox_api
|
|
115
|
+
|
|
116
|
+
# @return [Nightona::Process]
|
|
117
|
+
attr_reader :process
|
|
118
|
+
|
|
119
|
+
# @return [Nightona::FileSystem]
|
|
120
|
+
attr_reader :fs
|
|
121
|
+
|
|
122
|
+
# @return [Nightona::Git]
|
|
123
|
+
attr_reader :git
|
|
124
|
+
|
|
125
|
+
# @return [Nightona::ComputerUse]
|
|
126
|
+
attr_reader :computer_use
|
|
127
|
+
|
|
128
|
+
# @return [Nightona::CodeInterpreter]
|
|
129
|
+
attr_reader :code_interpreter
|
|
130
|
+
|
|
131
|
+
# @params config [Nightona::Config]
|
|
132
|
+
# @params sandbox_api [NightonaApiClient::SandboxApi]
|
|
133
|
+
# @params sandbox_dto [NightonaApiClient::Sandbox, NightonaApiClient::SandboxListItem]
|
|
134
|
+
# @params otel_state [Nightona::OtelState, nil]
|
|
135
|
+
def initialize(sandbox_dto:, config:, sandbox_api:, otel_state: nil) # rubocop:disable Metrics/MethodLength
|
|
136
|
+
process_response(sandbox_dto)
|
|
137
|
+
@config = config
|
|
138
|
+
@sandbox_api = sandbox_api
|
|
139
|
+
@otel_state = otel_state
|
|
140
|
+
|
|
141
|
+
# Create toolbox API clients with dynamic configuration
|
|
142
|
+
toolbox_api_config = build_toolbox_api_config
|
|
143
|
+
|
|
144
|
+
# Helper to create API client with authentication header
|
|
145
|
+
create_authenticated_client = lambda do
|
|
146
|
+
client = NightonaToolboxApiClient::ApiClient.new(toolbox_api_config)
|
|
147
|
+
client.default_headers['Authorization'] = "Bearer #{config.api_key || config.jwt_token}"
|
|
148
|
+
client.default_headers['X-Nightona-Source'] = 'sdk-ruby'
|
|
149
|
+
client.default_headers['X-Nightona-SDK-Version'] = Sdk::VERSION
|
|
150
|
+
client.default_headers['X-Nightona-Organization-ID'] = config.organization_id if config.jwt_token
|
|
151
|
+
client.user_agent = "sdk-ruby/#{Sdk::VERSION}"
|
|
152
|
+
client
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
process_api = NightonaToolboxApiClient::ProcessApi.new(create_authenticated_client.call)
|
|
156
|
+
fs_api = NightonaToolboxApiClient::FileSystemApi.new(create_authenticated_client.call)
|
|
157
|
+
git_api = NightonaToolboxApiClient::GitApi.new(create_authenticated_client.call)
|
|
158
|
+
lsp_api = NightonaToolboxApiClient::LspApi.new(create_authenticated_client.call)
|
|
159
|
+
computer_use_api = NightonaToolboxApiClient::ComputerUseApi.new(create_authenticated_client.call)
|
|
160
|
+
interpreter_api = NightonaToolboxApiClient::InterpreterApi.new(create_authenticated_client.call)
|
|
161
|
+
info_api = NightonaToolboxApiClient::InfoApi.new(create_authenticated_client.call)
|
|
162
|
+
|
|
163
|
+
@process = Process.new(
|
|
164
|
+
sandbox_id: id,
|
|
165
|
+
toolbox_api: process_api,
|
|
166
|
+
get_preview_link: proc { |port| preview_url(port) },
|
|
167
|
+
language: (labels || {}).fetch(CODE_TOOLBOX_LANGUAGE_LABEL, 'python'),
|
|
168
|
+
otel_state:
|
|
169
|
+
)
|
|
170
|
+
@fs = FileSystem.new(sandbox_id: id, toolbox_api: fs_api, otel_state:)
|
|
171
|
+
@git = Git.new(sandbox_id: id, toolbox_api: git_api, otel_state:)
|
|
172
|
+
@computer_use = ComputerUse.new(sandbox_id: id, toolbox_api: computer_use_api, otel_state:)
|
|
173
|
+
@code_interpreter = CodeInterpreter.new(
|
|
174
|
+
sandbox_id: id,
|
|
175
|
+
toolbox_api: interpreter_api,
|
|
176
|
+
get_preview_link: proc { |port| preview_url(port) },
|
|
177
|
+
otel_state:
|
|
178
|
+
)
|
|
179
|
+
@lsp_api = lsp_api
|
|
180
|
+
@info_api = info_api
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Archives the sandbox, making it inactive and preserving its state. When sandboxes are
|
|
184
|
+
# archived, the entire filesystem state is moved to cost-effective object storage, making it
|
|
185
|
+
# possible to keep sandboxes available for an extended period. The tradeoff between archived
|
|
186
|
+
# and stopped states is that starting an archived sandbox takes more time, depending on its size.
|
|
187
|
+
# Sandbox must be stopped before archiving.
|
|
188
|
+
#
|
|
189
|
+
# @return [void]
|
|
190
|
+
def archive
|
|
191
|
+
sandbox_api.archive_sandbox(id)
|
|
192
|
+
refresh
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Sets the auto-archive interval for the Sandbox.
|
|
196
|
+
# The Sandbox will automatically archive after being continuously stopped for the specified interval.
|
|
197
|
+
#
|
|
198
|
+
# @param interval [Integer]
|
|
199
|
+
# @return [Integer]
|
|
200
|
+
# @raise [Nightona:Sdk::Error]
|
|
201
|
+
def auto_archive_interval=(interval)
|
|
202
|
+
raise Sdk::Error, 'Auto-archive interval must be a non-negative integer' if interval.negative?
|
|
203
|
+
|
|
204
|
+
sandbox_api.set_auto_archive_interval(id, interval)
|
|
205
|
+
@auto_archive_interval = interval
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Sets the auto-delete interval for the Sandbox.
|
|
209
|
+
# The Sandbox will automatically delete after being continuously stopped for the specified interval.
|
|
210
|
+
#
|
|
211
|
+
# @param interval [Integer]
|
|
212
|
+
# @return [Integer]
|
|
213
|
+
# @raise [Nightona:Sdk::Error]
|
|
214
|
+
def auto_delete_interval=(interval)
|
|
215
|
+
sandbox_api.set_auto_delete_interval(id, interval)
|
|
216
|
+
@auto_delete_interval = interval
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Updates outbound network policy on the runner (block all, restore access, or CIDR allow list).
|
|
220
|
+
#
|
|
221
|
+
# @param network_block_all [Boolean, nil]
|
|
222
|
+
# @param network_allow_list [String, nil]
|
|
223
|
+
# @param domain_allow_list [String, nil]
|
|
224
|
+
# @return [void]
|
|
225
|
+
# @raise [Nightona::Sdk::Error]
|
|
226
|
+
def update_network_settings(network_block_all: nil, network_allow_list: nil, domain_allow_list: nil)
|
|
227
|
+
if network_block_all.nil? && network_allow_list.nil? && domain_allow_list.nil?
|
|
228
|
+
raise Sdk::Error,
|
|
229
|
+
'At least one of network_block_all, network_allow_list or domain_allow_list must be provided'
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
body = NightonaApiClient::UpdateSandboxNetworkSettings.new(
|
|
233
|
+
network_block_all:,
|
|
234
|
+
network_allow_list:,
|
|
235
|
+
domain_allow_list:
|
|
236
|
+
)
|
|
237
|
+
data = sandbox_api.update_network_settings(id, body)
|
|
238
|
+
@network_block_all = data.network_block_all
|
|
239
|
+
@network_allow_list = data.network_allow_list
|
|
240
|
+
@domain_allow_list = data.domain_allow_list
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Sets the auto-stop interval for the Sandbox.
|
|
244
|
+
# The Sandbox will automatically stop after being idle (no new events) for the specified interval.
|
|
245
|
+
# Events include any state changes or interactions with the Sandbox through the SDK.
|
|
246
|
+
# Interactions using Sandbox Previews are not included.
|
|
247
|
+
#
|
|
248
|
+
# @param interval [Integer]
|
|
249
|
+
# @return [Integer]
|
|
250
|
+
# @raise [Nightona:Sdk::Error]
|
|
251
|
+
def auto_stop_interval=(interval)
|
|
252
|
+
raise Sdk::Error, 'Auto-stop interval must be a non-negative integer' if interval.negative?
|
|
253
|
+
|
|
254
|
+
sandbox_api.set_autostop_interval(id, interval)
|
|
255
|
+
@auto_stop_interval = interval
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Creates an SSH access token for the sandbox.
|
|
259
|
+
#
|
|
260
|
+
# @param expires_in_minutes [Integer] TThe number of minutes the SSH access token will be valid for
|
|
261
|
+
# @return [NightonaApiClient::SshAccessDto]
|
|
262
|
+
def create_ssh_access(expires_in_minutes) = sandbox_api.create_ssh_access(id, { expires_in_minutes: })
|
|
263
|
+
|
|
264
|
+
# @return [void]
|
|
265
|
+
def delete
|
|
266
|
+
sandbox_api.delete_sandbox(id)
|
|
267
|
+
refresh
|
|
268
|
+
rescue NightonaApiClient::ApiError => e
|
|
269
|
+
raise unless e.code == 404
|
|
270
|
+
|
|
271
|
+
@state = 'destroyed'
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Gets the user's home directory path for the logged in user inside the Sandbox.
|
|
275
|
+
#
|
|
276
|
+
# @return [String] The absolute path to the Sandbox user's home directory for the logged in user
|
|
277
|
+
#
|
|
278
|
+
# @example
|
|
279
|
+
# user_home_dir = sandbox.get_user_home_dir
|
|
280
|
+
# puts "Sandbox user home: #{user_home_dir}"
|
|
281
|
+
def get_user_home_dir
|
|
282
|
+
@info_api.get_user_home_dir.dir
|
|
283
|
+
rescue StandardError => e
|
|
284
|
+
raise Sdk::Error, "Failed to get user home directory: #{e.message}"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Gets the working directory path inside the Sandbox.
|
|
288
|
+
#
|
|
289
|
+
# @return [String] The absolute path to the Sandbox working directory. Uses the WORKDIR specified
|
|
290
|
+
# in the Dockerfile if present, or falling back to the user's home directory if not.
|
|
291
|
+
#
|
|
292
|
+
# @example
|
|
293
|
+
# work_dir = sandbox.get_work_dir
|
|
294
|
+
# puts "Sandbox working directory: #{work_dir}"
|
|
295
|
+
def get_work_dir
|
|
296
|
+
@info_api.get_work_dir.dir
|
|
297
|
+
rescue StandardError => e
|
|
298
|
+
raise Sdk::Error, "Failed to get working directory path: #{e.message}"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Sets labels for the Sandbox.
|
|
302
|
+
#
|
|
303
|
+
# @param labels [Hash<String, String>]
|
|
304
|
+
# @return [Hash<String, String>]
|
|
305
|
+
def labels=(labels)
|
|
306
|
+
@labels = sandbox_api.replace_labels(id, NightonaApiClient::SandboxLabels.build_from_hash(labels:)).labels
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Retrieves the preview link for the sandbox at the specified port. If the port is closed,
|
|
310
|
+
# it will be opened automatically. For private sandboxes, a token is included to grant access
|
|
311
|
+
# to the URL.
|
|
312
|
+
#
|
|
313
|
+
# @param port [Integer]
|
|
314
|
+
# @return [NightonaApiClient::PortPreviewUrl]
|
|
315
|
+
def preview_url(port) = sandbox_api.get_port_preview_url(id, port)
|
|
316
|
+
|
|
317
|
+
# Creates a signed preview URL for the sandbox at the specified port.
|
|
318
|
+
#
|
|
319
|
+
# @param port [Integer] The port to open the preview link on
|
|
320
|
+
# @param expires_in_seconds [Integer, nil] The number of seconds the signed preview URL
|
|
321
|
+
# will be valid for. Defaults to 60 seconds.
|
|
322
|
+
# @return [NightonaApiClient::SignedPortPreviewUrl] The signed preview URL response object
|
|
323
|
+
#
|
|
324
|
+
# @example
|
|
325
|
+
# signed_url = sandbox.create_signed_preview_url(3000, 120)
|
|
326
|
+
# puts "Signed URL: #{signed_url.url}"
|
|
327
|
+
# puts "Token: #{signed_url.token}"
|
|
328
|
+
def create_signed_preview_url(port, expires_in_seconds = nil)
|
|
329
|
+
sandbox_api.get_signed_port_preview_url(id, port, { expires_in_seconds: })
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Expires a signed preview URL for the sandbox at the specified port.
|
|
333
|
+
#
|
|
334
|
+
# @param port [Integer] The port to expire the signed preview URL on
|
|
335
|
+
# @param token [String] The token to expire
|
|
336
|
+
# @return [void]
|
|
337
|
+
#
|
|
338
|
+
# @example
|
|
339
|
+
# sandbox.expire_signed_preview_url(3000, "token-value")
|
|
340
|
+
def expire_signed_preview_url(port, token)
|
|
341
|
+
sandbox_api.expire_signed_port_preview_url(id, port, token)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Refresh the Sandbox data from the API.
|
|
345
|
+
#
|
|
346
|
+
# @return [void]
|
|
347
|
+
def refresh = process_response(sandbox_api.get_sandbox(id))
|
|
348
|
+
|
|
349
|
+
# Refreshes the sandbox activity to reset the timer for automated lifecycle management actions.
|
|
350
|
+
#
|
|
351
|
+
# This method updates the sandbox's last activity timestamp without changing its state.
|
|
352
|
+
# It is useful for keeping long-running sessions alive while there is still user activity.
|
|
353
|
+
#
|
|
354
|
+
# @return [void]
|
|
355
|
+
#
|
|
356
|
+
# @example
|
|
357
|
+
# sandbox.refresh_activity
|
|
358
|
+
def refresh_activity
|
|
359
|
+
sandbox_api.update_last_activity(id)
|
|
360
|
+
nil
|
|
361
|
+
rescue StandardError => e
|
|
362
|
+
raise Sdk::Error, "Failed to refresh sandbox activity: #{e.message}"
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Revokes an SSH access token for the sandbox.
|
|
366
|
+
#
|
|
367
|
+
# @param token [String]
|
|
368
|
+
# @return [void]
|
|
369
|
+
def revoke_ssh_access(token) = sandbox_api.revoke_ssh_access(id, token:)
|
|
370
|
+
|
|
371
|
+
# Starts the Sandbox and waits for it to be ready.
|
|
372
|
+
#
|
|
373
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
374
|
+
# @return [void]
|
|
375
|
+
def start(timeout = DEFAULT_TIMEOUT)
|
|
376
|
+
with_timeout(
|
|
377
|
+
timeout:,
|
|
378
|
+
message: "Sandbox #{id} failed to become ready within the #{timeout} seconds timeout period",
|
|
379
|
+
setup: proc { process_response(sandbox_api.start_sandbox(id)) }
|
|
380
|
+
) { wait_for_states(operation: OPERATION_START, target_states: [NightonaApiClient::SandboxState::STARTED]) }
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Recovers the Sandbox from a recoverable error and waits for it to be ready.
|
|
384
|
+
#
|
|
385
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
386
|
+
# @return [void]
|
|
387
|
+
#
|
|
388
|
+
# @example
|
|
389
|
+
# sandbox = nightona.get('my-sandbox-id')
|
|
390
|
+
# sandbox.recover(timeout: 40) # Wait up to 40 seconds
|
|
391
|
+
# puts 'Sandbox recovered successfully'
|
|
392
|
+
def recover(timeout = DEFAULT_TIMEOUT)
|
|
393
|
+
with_timeout(
|
|
394
|
+
timeout:,
|
|
395
|
+
message: "Sandbox #{id} failed to recover within the #{timeout} seconds timeout period",
|
|
396
|
+
setup: proc { process_response(sandbox_api.recover_sandbox(id)) }
|
|
397
|
+
) { wait_for_states(operation: OPERATION_START, target_states: [NightonaApiClient::SandboxState::STARTED]) }
|
|
398
|
+
rescue StandardError => e
|
|
399
|
+
raise Sdk::Error, "Failed to recover sandbox: #{e.message}"
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Stops the Sandbox and waits for it to be stopped.
|
|
403
|
+
#
|
|
404
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
405
|
+
# @param force [Boolean] If true, uses SIGKILL instead of SIGTERM (defaults to false).
|
|
406
|
+
# @return [void]
|
|
407
|
+
def stop(timeout = DEFAULT_TIMEOUT, force: false) # rubocop:disable Metrics/MethodLength
|
|
408
|
+
with_timeout(
|
|
409
|
+
timeout:,
|
|
410
|
+
message: "Sandbox #{id} failed to become stopped within the #{timeout} seconds timeout period",
|
|
411
|
+
setup: proc {
|
|
412
|
+
sandbox_api.stop_sandbox(id, { force: force })
|
|
413
|
+
refresh
|
|
414
|
+
}
|
|
415
|
+
) do
|
|
416
|
+
wait_for_states(
|
|
417
|
+
operation: OPERATION_STOP,
|
|
418
|
+
target_states: [NightonaApiClient::SandboxState::STOPPED, NightonaApiClient::SandboxState::DESTROYED]
|
|
419
|
+
)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Resizes the Sandbox resources.
|
|
424
|
+
#
|
|
425
|
+
# Changes the CPU, memory, or disk allocation. Resizing a started sandbox accepts
|
|
426
|
+
# only CPU and memory increases. Disk resize requires a stopped sandbox; disk can
|
|
427
|
+
# only grow. GPU is not resizable — to change GPU, create a new sandbox.
|
|
428
|
+
#
|
|
429
|
+
# @param resources [Nightona::Resources] New resource configuration (cpu, memory, disk only)
|
|
430
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
|
|
431
|
+
# @return [void]
|
|
432
|
+
# @raise [Sdk::Error] If resources.gpu or resources.gpu_type is set
|
|
433
|
+
#
|
|
434
|
+
# @example Resize a started sandbox (CPU and memory can be increased)
|
|
435
|
+
# sandbox.resize(Nightona::Resources.new(cpu: 4, memory: 8))
|
|
436
|
+
#
|
|
437
|
+
# @example Resize a stopped sandbox (CPU, memory, and disk can be changed)
|
|
438
|
+
# sandbox.stop
|
|
439
|
+
# sandbox.resize(Nightona::Resources.new(cpu: 2, memory: 4, disk: 30))
|
|
440
|
+
def resize(resources, timeout = DEFAULT_TIMEOUT)
|
|
441
|
+
raise Sdk::Error, 'Resources must not be nil' if resources.nil?
|
|
442
|
+
|
|
443
|
+
if resources.gpu || resources.gpu_type
|
|
444
|
+
raise Sdk::Error,
|
|
445
|
+
'Resize does not support changes to gpu or gpu_type — to change GPU, create a new sandbox'
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
with_timeout(
|
|
449
|
+
timeout:,
|
|
450
|
+
message: "Sandbox #{id} failed to resize within the #{timeout} seconds timeout period",
|
|
451
|
+
setup: proc {
|
|
452
|
+
resize_attrs = {}
|
|
453
|
+
resize_attrs[:cpu] = resources.cpu if resources.cpu
|
|
454
|
+
resize_attrs[:memory] = resources.memory if resources.memory
|
|
455
|
+
resize_attrs[:disk] = resources.disk if resources.disk
|
|
456
|
+
resize_request = NightonaApiClient::ResizeSandbox.new(resize_attrs)
|
|
457
|
+
process_response(sandbox_api.resize_sandbox(id, resize_request))
|
|
458
|
+
}
|
|
459
|
+
) { wait_for_resize_complete }
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Waits for the Sandbox resize operation to complete.
|
|
463
|
+
# Polls the Sandbox status until the state is no longer 'resizing'.
|
|
464
|
+
#
|
|
465
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
|
|
466
|
+
# @return [void]
|
|
467
|
+
def wait_for_resize_complete(_timeout = DEFAULT_TIMEOUT)
|
|
468
|
+
wait_for_states(operation: OPERATION_RESIZE, target_states: [NightonaApiClient::SandboxState::STARTED,
|
|
469
|
+
NightonaApiClient::SandboxState::STOPPED])
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Creates a new Language Server Protocol (LSP) server instance.
|
|
473
|
+
# The LSP server provides language-specific features like code completion,
|
|
474
|
+
# diagnostics, and more.
|
|
475
|
+
#
|
|
476
|
+
# @param language_id [Symbol] The language server type (e.g., Nightona::LspServer::Language::PYTHON)
|
|
477
|
+
# @param path_to_project [String] Path to the project root directory. Relative paths are resolved
|
|
478
|
+
# based on the sandbox working directory.
|
|
479
|
+
# @return [Nightona::LspServer]
|
|
480
|
+
def create_lsp_server(language_id:, path_to_project:)
|
|
481
|
+
LspServer.new(language_id:, path_to_project:, toolbox_api: @lsp_api, sandbox_id: id, otel_state:)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Validates an SSH access token for the sandbox.
|
|
485
|
+
#
|
|
486
|
+
# @param token [String]
|
|
487
|
+
# @return [NightonaApiClient::SshAccessValidationDto]
|
|
488
|
+
def validate_ssh_access(token) = sandbox_api.validate_ssh_access(token)
|
|
489
|
+
|
|
490
|
+
# Waits for the Sandbox to reach the 'started' state. Polls the Sandbox status until it
|
|
491
|
+
# reaches the 'started' state or encounters an error.
|
|
492
|
+
#
|
|
493
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
494
|
+
# @return [void]
|
|
495
|
+
def wait_for_sandbox_start(_timeout = DEFAULT_TIMEOUT)
|
|
496
|
+
wait_for_states(operation: OPERATION_START, target_states: [NightonaApiClient::SandboxState::STARTED])
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Waits for the Sandbox to reach the 'stopped' state. Polls the Sandbox status until it
|
|
500
|
+
# reaches the 'stopped' state or encounters an error.
|
|
501
|
+
# Treats destroyed as stopped to cover ephemeral sandboxes that are automatically deleted after stopping.
|
|
502
|
+
#
|
|
503
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
504
|
+
# @return [void]
|
|
505
|
+
def wait_for_sandbox_stop(_timeout = DEFAULT_TIMEOUT)
|
|
506
|
+
wait_for_states(operation: OPERATION_STOP, target_states: [NightonaApiClient::SandboxState::STOPPED,
|
|
507
|
+
NightonaApiClient::SandboxState::DESTROYED])
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Forks the Sandbox, creating a new Sandbox with an identical filesystem.
|
|
511
|
+
# The forked Sandbox is a copy-on-write clone of the original. It starts
|
|
512
|
+
# with the same disk contents but operates independently from that point on.
|
|
513
|
+
#
|
|
514
|
+
# @param name [String, nil] Optional name for the forked Sandbox
|
|
515
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
|
|
516
|
+
# @return [Nightona::Sandbox] The forked Sandbox
|
|
517
|
+
def experimental_fork(name: nil, timeout: DEFAULT_TIMEOUT) # rubocop:disable Metrics/MethodLength
|
|
518
|
+
forked_dto = nil
|
|
519
|
+
with_timeout(
|
|
520
|
+
timeout:,
|
|
521
|
+
message: "Sandbox #{id} fork failed to become ready within the #{timeout} seconds timeout period",
|
|
522
|
+
setup: proc {
|
|
523
|
+
forked_dto = sandbox_api.fork_sandbox(id, NightonaApiClient::ForkSandbox.new(name:))
|
|
524
|
+
}
|
|
525
|
+
) do
|
|
526
|
+
forked = Sandbox.new(
|
|
527
|
+
sandbox_dto: forked_dto,
|
|
528
|
+
config:,
|
|
529
|
+
sandbox_api:,
|
|
530
|
+
code_toolbox:,
|
|
531
|
+
otel_state:
|
|
532
|
+
)
|
|
533
|
+
forked.send(:wait_for_states, operation: OPERATION_START,
|
|
534
|
+
target_states: [NightonaApiClient::SandboxState::STARTED])
|
|
535
|
+
return forked
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Creates a snapshot from the current state of the Sandbox.
|
|
540
|
+
# The Sandbox will temporarily enter a 'snapshotting' state and return to its previous state when complete.
|
|
541
|
+
#
|
|
542
|
+
# @param name [String] Name for the new snapshot
|
|
543
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
|
|
544
|
+
# @return [void]
|
|
545
|
+
def experimental_create_snapshot(name:, timeout: DEFAULT_TIMEOUT)
|
|
546
|
+
with_timeout(
|
|
547
|
+
timeout:,
|
|
548
|
+
message: "Sandbox #{id} snapshot failed within the #{timeout} seconds timeout period",
|
|
549
|
+
setup: proc {
|
|
550
|
+
sandbox_api.create_sandbox_snapshot(id, NightonaApiClient::CreateSandboxSnapshot.new(name:))
|
|
551
|
+
refresh
|
|
552
|
+
}
|
|
553
|
+
) { wait_for_snapshot_complete }
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Pauses the Sandbox, freezing all running processes.
|
|
557
|
+
# The Sandbox will enter a 'pausing' state and transition to 'paused' when complete.
|
|
558
|
+
#
|
|
559
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
|
|
560
|
+
# @return [void]
|
|
561
|
+
def pause(timeout: DEFAULT_TIMEOUT)
|
|
562
|
+
with_timeout(
|
|
563
|
+
timeout:,
|
|
564
|
+
message: "Sandbox #{id} failed to pause within the #{timeout} seconds timeout period",
|
|
565
|
+
setup: proc {
|
|
566
|
+
sandbox_api.pause_sandbox(id)
|
|
567
|
+
refresh
|
|
568
|
+
}
|
|
569
|
+
) { wait_for_pause_complete }
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
instrument :archive, :auto_archive_interval=, :auto_delete_interval=, :auto_stop_interval=,
|
|
573
|
+
:update_network_settings,
|
|
574
|
+
:create_ssh_access, :delete, :get_user_home_dir, :get_work_dir, :labels=,
|
|
575
|
+
:preview_url, :create_signed_preview_url, :expire_signed_preview_url,
|
|
576
|
+
:refresh, :refresh_activity, :revoke_ssh_access, :start, :recover, :stop,
|
|
577
|
+
:create_lsp_server, :validate_ssh_access, :wait_for_sandbox_start,
|
|
578
|
+
:wait_for_sandbox_stop, :resize, :wait_for_resize_complete,
|
|
579
|
+
:experimental_fork, :experimental_create_snapshot, :pause,
|
|
580
|
+
component: 'Sandbox'
|
|
581
|
+
|
|
582
|
+
private
|
|
583
|
+
|
|
584
|
+
# @return [Nightona::OtelState, nil]
|
|
585
|
+
attr_reader :otel_state
|
|
586
|
+
|
|
587
|
+
# Build toolbox API configuration with dynamic base URL from preview link
|
|
588
|
+
# @return [NightonaToolboxApiClient::Configuration]
|
|
589
|
+
def build_toolbox_api_config
|
|
590
|
+
NightonaToolboxApiClient::Configuration.new.configure do |cfg|
|
|
591
|
+
proxy_url = @toolbox_proxy_url
|
|
592
|
+
proxy_url += '/' unless proxy_url.end_with?('/')
|
|
593
|
+
full_url = "#{proxy_url}#{id}"
|
|
594
|
+
uri = URI(full_url)
|
|
595
|
+
|
|
596
|
+
cfg.scheme = uri.scheme
|
|
597
|
+
cfg.host = uri.authority # Includes hostname:port
|
|
598
|
+
cfg.base_path = uri.path.empty? ? '/' : uri.path
|
|
599
|
+
|
|
600
|
+
cfg
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# @params sandbox_dto [NightonaApiClient::Sandbox, NightonaApiClient::SandboxListItem]
|
|
605
|
+
# @return [void]
|
|
606
|
+
def process_response(sandbox_dto) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
607
|
+
# Fields shared by both NightonaApiClient::Sandbox and NightonaApiClient::SandboxListItem.
|
|
608
|
+
@id = sandbox_dto.id
|
|
609
|
+
@organization_id = sandbox_dto.organization_id
|
|
610
|
+
@snapshot = sandbox_dto.snapshot
|
|
611
|
+
@user = sandbox_dto.user
|
|
612
|
+
@labels = sandbox_dto.labels
|
|
613
|
+
@public = sandbox_dto.public
|
|
614
|
+
@target = sandbox_dto.target
|
|
615
|
+
@cpu = sandbox_dto.cpu
|
|
616
|
+
@gpu = sandbox_dto.gpu
|
|
617
|
+
@memory = sandbox_dto.memory
|
|
618
|
+
@disk = sandbox_dto.disk
|
|
619
|
+
@state = sandbox_dto.state
|
|
620
|
+
@desired_state = sandbox_dto.desired_state
|
|
621
|
+
@error_reason = sandbox_dto.error_reason
|
|
622
|
+
@backup_state = sandbox_dto.backup_state
|
|
623
|
+
@auto_stop_interval = sandbox_dto.auto_stop_interval
|
|
624
|
+
@auto_archive_interval = sandbox_dto.auto_archive_interval
|
|
625
|
+
@auto_delete_interval = sandbox_dto.auto_delete_interval
|
|
626
|
+
@created_at = sandbox_dto.created_at
|
|
627
|
+
@updated_at = sandbox_dto.updated_at
|
|
628
|
+
@last_activity_at = sandbox_dto.last_activity_at
|
|
629
|
+
@daemon_version = sandbox_dto.daemon_version
|
|
630
|
+
@toolbox_proxy_url = sandbox_dto.toolbox_proxy_url
|
|
631
|
+
|
|
632
|
+
# Fields only present on the full NightonaApiClient::Sandbox DTO (not returned by list
|
|
633
|
+
# results; call #refresh on each item to populate them).
|
|
634
|
+
return unless sandbox_dto.is_a?(NightonaApiClient::Sandbox)
|
|
635
|
+
|
|
636
|
+
@env = sandbox_dto.env
|
|
637
|
+
@network_block_all = sandbox_dto.network_block_all
|
|
638
|
+
@network_allow_list = sandbox_dto.network_allow_list
|
|
639
|
+
@domain_allow_list = sandbox_dto.domain_allow_list
|
|
640
|
+
@volumes = sandbox_dto.volumes
|
|
641
|
+
@build_info = sandbox_dto.build_info
|
|
642
|
+
@backup_created_at = sandbox_dto.backup_created_at
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# Monitors block not to exceed max execution time.
|
|
646
|
+
#
|
|
647
|
+
# @param setup [#call, Nil] Optional setup block
|
|
648
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
|
|
649
|
+
# @param message [String] Error message
|
|
650
|
+
# @return [void]
|
|
651
|
+
# @raise [Nightona::Sdk::Error]
|
|
652
|
+
def with_timeout(message:, setup:, timeout: DEFAULT_TIMEOUT, &)
|
|
653
|
+
start_at = Time.now
|
|
654
|
+
setup&.call
|
|
655
|
+
|
|
656
|
+
Timeout.timeout(
|
|
657
|
+
setup ? [NO_TIMEOUT, timeout - (Time.now - start_at)].max : timeout,
|
|
658
|
+
Sdk::Error,
|
|
659
|
+
message,
|
|
660
|
+
&
|
|
661
|
+
)
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
# Waits for the Sandbox to reach the one of the target states. Polls the Sandbox status until it
|
|
665
|
+
# reaches the one of the target states or encounters an error. It will wait up to 60 seconds
|
|
666
|
+
# for the Sandbox to reach one of the target states.
|
|
667
|
+
#
|
|
668
|
+
# @param operation [#to_s] Operation name for error message
|
|
669
|
+
# @param target_states [Array<NightonaApiClient::SandboxState>] List of the target states
|
|
670
|
+
# @return [void]
|
|
671
|
+
# @raise [Nightona::Sdk::Error]
|
|
672
|
+
def wait_for_states(operation:, target_states:)
|
|
673
|
+
interval = INITIAL_POLL_INTERVAL
|
|
674
|
+
start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
675
|
+
loop do
|
|
676
|
+
case state
|
|
677
|
+
when *target_states then return
|
|
678
|
+
when NightonaApiClient::SandboxState::ERROR, NightonaApiClient::SandboxState::BUILD_FAILED
|
|
679
|
+
raise Sdk::Error, "Sandbox #{id} failed to #{operation} with state: #{state}, error reason: #{error_reason}"
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
sleep(interval)
|
|
683
|
+
if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_time > 5
|
|
684
|
+
interval = [interval * BACKOFF_MULTIPLIER, MAX_POLL_INTERVAL].min
|
|
685
|
+
end
|
|
686
|
+
refresh
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
INITIAL_POLL_INTERVAL = 0.1
|
|
691
|
+
private_constant :INITIAL_POLL_INTERVAL
|
|
692
|
+
|
|
693
|
+
MAX_POLL_INTERVAL = 1.0
|
|
694
|
+
private_constant :MAX_POLL_INTERVAL
|
|
695
|
+
|
|
696
|
+
BACKOFF_MULTIPLIER = 1.1
|
|
697
|
+
private_constant :BACKOFF_MULTIPLIER
|
|
698
|
+
|
|
699
|
+
NO_TIMEOUT = 0
|
|
700
|
+
private_constant :NO_TIMEOUT
|
|
701
|
+
|
|
702
|
+
OPERATION_START = :start
|
|
703
|
+
private_constant :OPERATION_START
|
|
704
|
+
|
|
705
|
+
OPERATION_STOP = :stop
|
|
706
|
+
private_constant :OPERATION_STOP
|
|
707
|
+
|
|
708
|
+
OPERATION_RESIZE = :resize
|
|
709
|
+
private_constant :OPERATION_RESIZE
|
|
710
|
+
|
|
711
|
+
def wait_for_snapshot_complete
|
|
712
|
+
interval = INITIAL_POLL_INTERVAL
|
|
713
|
+
start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
714
|
+
while state == NightonaApiClient::SandboxState::SNAPSHOTTING
|
|
715
|
+
refresh
|
|
716
|
+
|
|
717
|
+
if [NightonaApiClient::SandboxState::ERROR, NightonaApiClient::SandboxState::BUILD_FAILED].include?(state)
|
|
718
|
+
raise Sdk::Error,
|
|
719
|
+
"Sandbox #{id} snapshot failed with state: #{state}, error reason: #{error_reason}"
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
break if state != NightonaApiClient::SandboxState::SNAPSHOTTING
|
|
723
|
+
|
|
724
|
+
sleep(interval)
|
|
725
|
+
if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_time > 5
|
|
726
|
+
interval = [interval * BACKOFF_MULTIPLIER, MAX_POLL_INTERVAL].min
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def wait_for_pause_complete
|
|
732
|
+
interval = INITIAL_POLL_INTERVAL
|
|
733
|
+
start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
734
|
+
while state == NightonaApiClient::SandboxState::PAUSING
|
|
735
|
+
refresh
|
|
736
|
+
|
|
737
|
+
if [NightonaApiClient::SandboxState::ERROR, NightonaApiClient::SandboxState::BUILD_FAILED].include?(state)
|
|
738
|
+
raise Sdk::Error,
|
|
739
|
+
"Sandbox #{id} pause failed with state: #{state}, error reason: #{error_reason}"
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
break if state != NightonaApiClient::SandboxState::PAUSING
|
|
743
|
+
|
|
744
|
+
sleep(interval)
|
|
745
|
+
if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_time > 5
|
|
746
|
+
interval = [interval * BACKOFF_MULTIPLIER, MAX_POLL_INTERVAL].min
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
end
|