daytona 0.210.0 → 0.214.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/code_interpreter.rb +1 -1
- data/lib/daytona/common/image.rb +49 -30
- data/lib/daytona/common/websocket_dialer.rb +166 -0
- data/lib/daytona/config.rb +84 -6
- data/lib/daytona/object_storage.rb +21 -10
- data/lib/daytona/process.rb +4 -4
- data/lib/daytona/sdk/version.rb +1 -1
- data/lib/daytona/sdk.rb +1 -0
- metadata +20 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 868afc4f077432e39b074d0249d2a20277206a1e430f64954b42663b74496118
|
|
4
|
+
data.tar.gz: 5e8dc41efd1f588f325f8fd117d8ad985382d5c7a43a73740d58ded6d5def73e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5fca0242291118c5000ffba802fb450aeab89c8f4b05c2f01cde1192e9c898dff8480edf6d9c12332a317c6282656a77b8b3fc9d75b0172a6bb0a16f1bb3dbc
|
|
7
|
+
data.tar.gz: 11fb9e1f412128cdb6651852bd5af7a7441a49ba1dcd3dab1ae3ce2479ac3769ba5968c3a85e10218b6e6f2ee45bd818adaebd9f3481fdb989e884294def4899
|
|
@@ -108,7 +108,7 @@ module Daytona
|
|
|
108
108
|
|
|
109
109
|
puts "[DEBUG] Connecting to WebSocket: #{ws_url}" if ENV['DEBUG']
|
|
110
110
|
|
|
111
|
-
ws =
|
|
111
|
+
ws = Common::WebSocketDialer.connect(ws_url, headers:) do |client|
|
|
112
112
|
client.on :open do
|
|
113
113
|
puts '[DEBUG] WebSocket opened, sending request' if ENV['DEBUG']
|
|
114
114
|
client.send(JSON.dump(request))
|
data/lib/daytona/common/image.rb
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
require 'digest'
|
|
7
7
|
require 'fileutils'
|
|
8
|
+
require 'json'
|
|
8
9
|
require 'pathname'
|
|
9
10
|
require 'shellwords'
|
|
10
11
|
|
|
@@ -362,7 +363,7 @@ module Daytona
|
|
|
362
363
|
# @return [Array<Array<String>>] The list of the actual file path and its corresponding COPY-command source path
|
|
363
364
|
def extract_copy_sources(dockerfile_content, path_prefix = '') # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
364
365
|
sources = []
|
|
365
|
-
lines = dockerfile_content
|
|
366
|
+
lines = dockerfile_logical_lines(dockerfile_content)
|
|
366
367
|
|
|
367
368
|
lines.each do |line|
|
|
368
369
|
# Skip empty lines and comments
|
|
@@ -404,47 +405,65 @@ module Daytona
|
|
|
404
405
|
sources
|
|
405
406
|
end
|
|
406
407
|
|
|
408
|
+
# Joins backslash-continued physical lines into logical Dockerfile instruction lines
|
|
409
|
+
#
|
|
410
|
+
# @param dockerfile_content [String] The content of the Dockerfile
|
|
411
|
+
# @return [Array<String>] The logical instruction lines
|
|
412
|
+
def dockerfile_logical_lines(dockerfile_content) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
413
|
+
logical_lines = []
|
|
414
|
+
current = nil
|
|
415
|
+
|
|
416
|
+
dockerfile_content.each_line(chomp: true) do |physical_line|
|
|
417
|
+
is_comment = physical_line.lstrip.start_with?('#')
|
|
418
|
+
# Docker drops empty and comment lines that appear inside a continued instruction
|
|
419
|
+
next if current && (physical_line.strip.empty? || is_comment)
|
|
420
|
+
|
|
421
|
+
stripped = physical_line.rstrip
|
|
422
|
+
# A trailing backslash on a comment line is literal; comments never continue onto the next line
|
|
423
|
+
continued = !is_comment && stripped.end_with?('\\')
|
|
424
|
+
segment = continued ? stripped[0..-2] : physical_line
|
|
425
|
+
current = current ? current + segment : segment
|
|
426
|
+
next if continued
|
|
427
|
+
|
|
428
|
+
logical_lines << current
|
|
429
|
+
current = nil
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
logical_lines << current if current
|
|
433
|
+
logical_lines
|
|
434
|
+
end
|
|
435
|
+
|
|
407
436
|
# Parses a COPY command to extract sources and destination
|
|
408
437
|
#
|
|
409
438
|
# @param line [String] The line to parse
|
|
410
439
|
# @return [Hash, nil] A hash containing the sources and destination, or nil if parsing fails
|
|
411
|
-
def parse_copy_command(line)
|
|
440
|
+
def parse_copy_command(line)
|
|
412
441
|
# Remove initial "COPY" and strip whitespace
|
|
413
442
|
parts = line.strip[4..].strip
|
|
414
443
|
|
|
415
|
-
#
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
# Parse the JSON-like array format
|
|
419
|
-
elements = Shellwords.split(parts.delete('[]'))
|
|
420
|
-
return nil if elements.length < 2
|
|
421
|
-
|
|
422
|
-
{ 'sources' => elements[0..-2], 'dest' => elements[-1] }
|
|
423
|
-
rescue StandardError
|
|
424
|
-
nil
|
|
425
|
-
end
|
|
426
|
-
end
|
|
444
|
+
# Skip leading flags. Value-taking flags use the --flag=value form (--chown=..., --chmod=...)
|
|
445
|
+
# and boolean flags stand alone (--link), so a flag never consumes the token that follows it.
|
|
446
|
+
parts = parts.sub(/\A\S+\s*/, '') while parts.start_with?('--')
|
|
427
447
|
|
|
428
|
-
# Handle
|
|
429
|
-
parts
|
|
448
|
+
# Handle JSON array format: COPY ["src1", "src2", "dest"]
|
|
449
|
+
return parse_json_copy_command(parts) if parts.start_with?('[')
|
|
430
450
|
|
|
431
|
-
#
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
break unless part.start_with?('--')
|
|
451
|
+
# Handle the whitespace-separated format
|
|
452
|
+
elements = Shellwords.split(parts)
|
|
453
|
+
return nil if elements.length < 2
|
|
435
454
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
i + 1
|
|
441
|
-
end
|
|
442
|
-
end
|
|
455
|
+
{ 'sources' => elements[0..-2], 'dest' => elements[-1] }
|
|
456
|
+
rescue ArgumentError
|
|
457
|
+
nil
|
|
458
|
+
end
|
|
443
459
|
|
|
444
|
-
|
|
445
|
-
|
|
460
|
+
def parse_json_copy_command(parts)
|
|
461
|
+
elements = JSON.parse(parts)
|
|
462
|
+
return nil unless elements.is_a?(Array) && elements.all?(String) && elements.length >= 2
|
|
446
463
|
|
|
447
|
-
{ 'sources' =>
|
|
464
|
+
{ 'sources' => elements[0..-2], 'dest' => elements[-1] }
|
|
465
|
+
rescue JSON::ParserError
|
|
466
|
+
nil
|
|
448
467
|
end
|
|
449
468
|
end
|
|
450
469
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Daytona Platforms Inc.
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require 'openssl'
|
|
7
|
+
require 'socket'
|
|
8
|
+
require 'uri'
|
|
9
|
+
require 'websocket'
|
|
10
|
+
require 'websocket-client-simple'
|
|
11
|
+
|
|
12
|
+
module Daytona
|
|
13
|
+
module Common
|
|
14
|
+
# Establishes WebSocket connections with the peer certificate fully verified
|
|
15
|
+
# before any request bytes are written.
|
|
16
|
+
#
|
|
17
|
+
# `websocket-client-simple` builds its own `SSLContext` and never calls
|
|
18
|
+
# `OpenSSL::SSL::SSLContext#set_params`, so it does not pick up Ruby's own
|
|
19
|
+
# defaults (`verify_mode: VERIFY_PEER`, `verify_hostname: true`). It applies
|
|
20
|
+
# `verify_mode` only when the caller passes it, and offers no way to enable
|
|
21
|
+
# hostname verification at all. Because the context is frozen by
|
|
22
|
+
# `SSLSocket.new` and the handshake is written before `connect` returns,
|
|
23
|
+
# there is no caller-side hook to correct this - so the dial itself has to
|
|
24
|
+
# be owned here.
|
|
25
|
+
#
|
|
26
|
+
# Chain verification alone is not sufficient: without hostname verification
|
|
27
|
+
# a peer holding any valid certificate can terminate the connection and
|
|
28
|
+
# receive the request headers.
|
|
29
|
+
module WebSocketDialer
|
|
30
|
+
# Opens a verified WebSocket connection.
|
|
31
|
+
#
|
|
32
|
+
# Mirrors `WebSocket::Client::Simple.connect`: the block, if given, receives
|
|
33
|
+
# the client before the connection is established so handlers can be
|
|
34
|
+
# registered, and the client is returned.
|
|
35
|
+
#
|
|
36
|
+
# @param url [String] The `ws://` or `wss://` URL to dial.
|
|
37
|
+
# @param options [Hash] Passed through to the client; `:headers`, `:ssl_version` and `:cert_store` are honoured.
|
|
38
|
+
# @return [VerifyingClient] The connected client.
|
|
39
|
+
# @raise [OpenSSL::SSL::SSLError] If the peer certificate fails chain or hostname verification.
|
|
40
|
+
def self.connect(url, options = {})
|
|
41
|
+
client = VerifyingClient.new
|
|
42
|
+
yield client if block_given?
|
|
43
|
+
client.connect(url, options)
|
|
44
|
+
client
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A `websocket-client-simple` client that verifies the peer before writing.
|
|
48
|
+
#
|
|
49
|
+
# `#connect` is reimplemented rather than extended because the upstream
|
|
50
|
+
# method builds the socket, performs the TLS handshake and writes the
|
|
51
|
+
# request in one pass, and returns early when `@socket` is already set.
|
|
52
|
+
#
|
|
53
|
+
# Everything after TLS setup mirrors upstream so that framing, the reader
|
|
54
|
+
# thread and event semantics stay identical. That couples to the instance
|
|
55
|
+
# variables the inherited `#send`, `#close`, `#open?` and `#closed?` read,
|
|
56
|
+
# so the gemspec pins the gem to `~> 0.9.0` - a 0.x minor bump could rename
|
|
57
|
+
# them. `websocket_dialer_spec.rb` drives a full send/close round trip
|
|
58
|
+
# against a real listener, which fails if that contract breaks.
|
|
59
|
+
class VerifyingClient < WebSocket::Client::Simple::Client
|
|
60
|
+
# @param url [String] The `ws://` or `wss://` URL to dial.
|
|
61
|
+
# @param options [Hash] Connection options.
|
|
62
|
+
# @return [void]
|
|
63
|
+
def connect(url, options = {})
|
|
64
|
+
return if @socket
|
|
65
|
+
|
|
66
|
+
@url = url
|
|
67
|
+
uri = URI.parse(url)
|
|
68
|
+
@socket = TCPSocket.new(uri.host, uri.port || (uri.scheme == 'wss' ? 443 : 80))
|
|
69
|
+
@socket = verified_ssl_socket(@socket, uri, options) if %w[https wss].include?(uri.scheme)
|
|
70
|
+
|
|
71
|
+
start_websocket(url, options)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Wraps a socket in TLS with the peer certificate verified.
|
|
77
|
+
#
|
|
78
|
+
# @param socket [TCPSocket] The connected plaintext socket.
|
|
79
|
+
# @param uri [URI] The dialed URI, whose host the certificate must match.
|
|
80
|
+
# @param options [Hash] Connection options.
|
|
81
|
+
# @return [OpenSSL::SSL::SSLSocket] The connected, verified socket.
|
|
82
|
+
# @raise [OpenSSL::SSL::SSLError] If chain or hostname verification fails.
|
|
83
|
+
def verified_ssl_socket(socket, uri, options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
84
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
85
|
+
ctx.ssl_version = options[:ssl_version] if options[:ssl_version]
|
|
86
|
+
|
|
87
|
+
# Only seed system roots into a store we own. Adding them to a
|
|
88
|
+
# caller-supplied store would silently widen their trust policy.
|
|
89
|
+
cert_store = options[:cert_store]
|
|
90
|
+
unless cert_store
|
|
91
|
+
cert_store = OpenSSL::X509::Store.new
|
|
92
|
+
cert_store.set_default_paths
|
|
93
|
+
end
|
|
94
|
+
ctx.cert_store = cert_store
|
|
95
|
+
|
|
96
|
+
# Must precede SSLSocket.new, which freezes the context.
|
|
97
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
98
|
+
ctx.verify_hostname = true
|
|
99
|
+
|
|
100
|
+
ssl = OpenSSL::SSL::SSLSocket.new(socket, ctx)
|
|
101
|
+
ssl.sync_close = true
|
|
102
|
+
ssl.hostname = uri.host
|
|
103
|
+
begin
|
|
104
|
+
ssl.connect
|
|
105
|
+
# Redundant while verify_hostname holds, but keeps the check explicit
|
|
106
|
+
# and independent of the context surviving future changes.
|
|
107
|
+
ssl.post_connection_check(uri.host)
|
|
108
|
+
rescue StandardError
|
|
109
|
+
ssl.close
|
|
110
|
+
raise
|
|
111
|
+
end
|
|
112
|
+
ssl
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Performs the WebSocket handshake and starts the reader thread.
|
|
116
|
+
#
|
|
117
|
+
# Mirrors `WebSocket::Client::Simple::Client#connect` from the request
|
|
118
|
+
# onward. Only reached once the socket is verified.
|
|
119
|
+
#
|
|
120
|
+
# @param url [String] The dialed URL.
|
|
121
|
+
# @param options [Hash] Connection options.
|
|
122
|
+
# @return [void]
|
|
123
|
+
def start_websocket(url, options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
124
|
+
WebSocket.should_raise = true
|
|
125
|
+
@handshake = WebSocket::Handshake::Client.new(url: url, headers: options[:headers])
|
|
126
|
+
@handshaked = false
|
|
127
|
+
@pipe_broken = false
|
|
128
|
+
@closed = false
|
|
129
|
+
frame = WebSocket::Frame::Incoming::Client.new
|
|
130
|
+
|
|
131
|
+
once :__close do |err|
|
|
132
|
+
close
|
|
133
|
+
emit :close, err
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
@thread = Thread.new do
|
|
137
|
+
until @closed
|
|
138
|
+
begin
|
|
139
|
+
unless (recv_data = @socket.getc)
|
|
140
|
+
sleep 1
|
|
141
|
+
next
|
|
142
|
+
end
|
|
143
|
+
if @handshaked
|
|
144
|
+
frame << recv_data
|
|
145
|
+
while (msg = frame.next)
|
|
146
|
+
emit :message, msg
|
|
147
|
+
end
|
|
148
|
+
else
|
|
149
|
+
@handshake << recv_data
|
|
150
|
+
if @handshake.finished?
|
|
151
|
+
@handshaked = true
|
|
152
|
+
emit :open
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
emit :error, e
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
@socket.write @handshake.to_s
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
data/lib/daytona/config.rb
CHANGED
|
@@ -6,6 +6,45 @@
|
|
|
6
6
|
require 'dotenv'
|
|
7
7
|
|
|
8
8
|
module Daytona
|
|
9
|
+
# Dotenv runs `$(...)` in a value through the shell while parsing, and the working
|
|
10
|
+
# directory is frequently a cloned repository, so parsing `.env` there would execute
|
|
11
|
+
# whatever its author put in it.
|
|
12
|
+
module EnvFile
|
|
13
|
+
# Dotenv::Parser picks its substitutions up from `self.class.substitutions`, and Ruby
|
|
14
|
+
# does not inherit class-level instance variables, so declaring the list here drops
|
|
15
|
+
# command substitution while leaving Dotenv::Parser alone — a host application's own
|
|
16
|
+
# Dotenv.load keeps the behaviour its author chose. `${VAR}` is kept: it reads only
|
|
17
|
+
# keys already parsed or the process environment.
|
|
18
|
+
class Parser < Dotenv::Parser
|
|
19
|
+
@substitutions = [Dotenv::Substitutions::Variable].freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Read with the mode dotenv itself uses, so the accepted format does not narrow: `bom`
|
|
23
|
+
# skips a byte-order mark an editor on Windows may have written, and pinning `utf-8`
|
|
24
|
+
# keeps the file readable under a POSIX locale, where the default external encoding
|
|
25
|
+
# would make one accented byte anywhere — a comment included — raise while it is scanned.
|
|
26
|
+
def self.parse(path)
|
|
27
|
+
verify_suppression!
|
|
28
|
+
Parser.call(File.read(path, mode: 'rb:bom|utf-8'))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The subclass reaches into dotenv's internals rather than a public API, and dotenv has
|
|
32
|
+
# already reorganised them once inside the range the gemspec allows, so confirm the
|
|
33
|
+
# suppression actually holds in the process that relies on it rather than trusting the
|
|
34
|
+
# version pinned in CI. Checked here rather than on load: this is the operation the
|
|
35
|
+
# guard protects, so a lapse stops it, and a gem that never reads a .env still loads.
|
|
36
|
+
# A failure is not memoised, so it is raised again on the next attempt.
|
|
37
|
+
def self.verify_suppression!
|
|
38
|
+
return if @verified
|
|
39
|
+
|
|
40
|
+
probe = Parser.call('DAYTONA_PROBE=$(echo substituted)')['DAYTONA_PROBE']
|
|
41
|
+
raise "dotenv command substitution is not suppressed (got #{probe.inspect})" unless
|
|
42
|
+
probe == '$(echo substituted)'
|
|
43
|
+
|
|
44
|
+
@verified = true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
9
48
|
class Config
|
|
10
49
|
API_URL = 'https://app.daytona.io/api'
|
|
11
50
|
|
|
@@ -80,7 +119,9 @@ module Daytona
|
|
|
80
119
|
|
|
81
120
|
@api_key = api_key || @env_reader.call('DAYTONA_API_KEY')
|
|
82
121
|
@jwt_token = jwt_token || @env_reader.call('DAYTONA_JWT_TOKEN')
|
|
83
|
-
|
|
122
|
+
# Resolved from the process environment only, never from .env / .env.local:
|
|
123
|
+
# the endpoint decides where the credential above is sent.
|
|
124
|
+
@api_url = resolve_api_url(api_url)
|
|
84
125
|
@target = target || @env_reader.call('DAYTONA_TARGET')
|
|
85
126
|
@organization_id = organization_id || @env_reader.call('DAYTONA_ORGANIZATION_ID')
|
|
86
127
|
@otel_enabled = otel_enabled
|
|
@@ -101,14 +142,44 @@ module Daytona
|
|
|
101
142
|
|
|
102
143
|
private
|
|
103
144
|
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
|
|
145
|
+
# Resolves the API endpoint without consulting the working directory, then reports a
|
|
146
|
+
# dotenv value that was passed over. Staying silent when the file value matches the
|
|
147
|
+
# endpoint in use keeps the documented .env layout quiet, while a file that would have
|
|
148
|
+
# changed the destination is surfaced.
|
|
149
|
+
def resolve_api_url(api_url)
|
|
150
|
+
resolved = api_url || process_env('DAYTONA_API_URL') || API_URL
|
|
151
|
+
file_value = env_file_vars['DAYTONA_API_URL']
|
|
152
|
+
return resolved if file_value.nil? || file_value.empty? || file_value == resolved
|
|
153
|
+
|
|
154
|
+
warn(
|
|
155
|
+
'`DAYTONA_API_URL` set in a .env or .env.local file was ignored: the Daytona API endpoint ' \
|
|
156
|
+
'is never read from dotenv files, because the working directory is not always authored ' \
|
|
157
|
+
"by you. Using `#{resolved}` instead. To change the endpoint, pass `api_url:` to " \
|
|
158
|
+
'`Daytona::Config.new` or set `DAYTONA_API_URL` in the environment of the process.'
|
|
159
|
+
)
|
|
160
|
+
resolved
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Parses DAYTONA_-prefixed vars out of .env and .env.local in the working directory.
|
|
164
|
+
# These files are not necessarily authored by whoever runs the process, so anything
|
|
165
|
+
# that determines where a credential is sent must not be read from them.
|
|
166
|
+
def env_file_vars
|
|
167
|
+
@env_file_vars ||= parse_dotenv_files
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def parse_dotenv_files
|
|
107
171
|
file_vars = {}
|
|
108
172
|
env_file = File.join(Dir.pwd, '.env')
|
|
109
|
-
file_vars.merge!(daytona_filter(
|
|
173
|
+
file_vars.merge!(daytona_filter(EnvFile.parse(env_file))) if File.exist?(env_file)
|
|
110
174
|
env_local_file = File.join(Dir.pwd, '.env.local')
|
|
111
|
-
file_vars.merge!(daytona_filter(
|
|
175
|
+
file_vars.merge!(daytona_filter(EnvFile.parse(env_local_file))) if File.exist?(env_local_file)
|
|
176
|
+
file_vars
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Returns a lambda that looks up DAYTONA_-prefixed env vars without writing to ENV.
|
|
180
|
+
# Files are parsed once; lookups check runtime env first, then .env.local, then .env.
|
|
181
|
+
def daytona_env_reader
|
|
182
|
+
file_vars = env_file_vars
|
|
112
183
|
|
|
113
184
|
lambda do |name|
|
|
114
185
|
raise ArgumentError, "Variable must start with 'DAYTONA_', got '#{name}'" unless name.start_with?('DAYTONA_')
|
|
@@ -117,6 +188,13 @@ module Daytona
|
|
|
117
188
|
end
|
|
118
189
|
end
|
|
119
190
|
|
|
191
|
+
# Reads a DAYTONA_-prefixed variable from the process environment only.
|
|
192
|
+
def process_env(name)
|
|
193
|
+
raise ArgumentError, "Variable must start with 'DAYTONA_', got '#{name}'" unless name.start_with?('DAYTONA_')
|
|
194
|
+
|
|
195
|
+
ENV.fetch(name, nil)
|
|
196
|
+
end
|
|
197
|
+
|
|
120
198
|
def daytona_filter(env_hash)
|
|
121
199
|
env_hash.select { |k, _| k.start_with?('DAYTONA_') }
|
|
122
200
|
end
|
|
@@ -34,7 +34,8 @@ module Daytona
|
|
|
34
34
|
endpoint: endpoint_url,
|
|
35
35
|
access_key_id: aws_access_key_id,
|
|
36
36
|
secret_access_key: aws_secret_access_key,
|
|
37
|
-
session_token: aws_session_token
|
|
37
|
+
session_token: aws_session_token,
|
|
38
|
+
**CLIENT_OPTIONS
|
|
38
39
|
)
|
|
39
40
|
end
|
|
40
41
|
|
|
@@ -146,17 +147,15 @@ module Daytona
|
|
|
146
147
|
self.class.compute_archive_base_path(source_path) if archive_base_path.nil?
|
|
147
148
|
|
|
148
149
|
temp_file = Tempfile.new(['context', '.tar'])
|
|
150
|
+
archive_path = temp_file.path
|
|
149
151
|
|
|
150
152
|
begin
|
|
151
|
-
system('tar', '-cf',
|
|
152
|
-
|
|
153
|
-
File.open(temp_file.path, 'rb') do |file|
|
|
154
|
-
s3_client.put_object(
|
|
155
|
-
bucket: bucket_name,
|
|
156
|
-
key: s3_key,
|
|
157
|
-
body: file
|
|
158
|
-
)
|
|
153
|
+
unless system('tar', '-cf', archive_path, '-C', File.dirname(source_path), File.basename(source_path))
|
|
154
|
+
raise Sdk::Error, "Failed to create tar archive for #{source_path}"
|
|
159
155
|
end
|
|
156
|
+
|
|
157
|
+
Aws::S3::TransferManager.new(client: s3_client)
|
|
158
|
+
.upload_file(archive_path, bucket: bucket_name, key: s3_key, **UPLOAD_OPTIONS)
|
|
160
159
|
ensure
|
|
161
160
|
temp_file.close
|
|
162
161
|
temp_file.unlink
|
|
@@ -164,6 +163,18 @@ module Daytona
|
|
|
164
163
|
end
|
|
165
164
|
|
|
166
165
|
DEFAULT_BUCKET_NAME = 'daytona-volume-builds'
|
|
167
|
-
|
|
166
|
+
|
|
167
|
+
# multipart_threshold is also the part size floor: S3 rejects any part but the
|
|
168
|
+
# last below 5 MiB, and uploading in parts means a slow or unstable connection
|
|
169
|
+
# only ever has to carry 5 MiB per request instead of the whole context.
|
|
170
|
+
UPLOAD_OPTIONS = {
|
|
171
|
+
multipart_threshold: 5 * 1024 * 1024,
|
|
172
|
+
thread_count: 4,
|
|
173
|
+
content_type: 'application/x-tar'
|
|
174
|
+
}.freeze
|
|
175
|
+
|
|
176
|
+
CLIENT_OPTIONS = { http_open_timeout: 15, http_read_timeout: 120, retry_limit: 3 }.freeze
|
|
177
|
+
|
|
178
|
+
private_constant :DEFAULT_BUCKET_NAME, :UPLOAD_OPTIONS, :CLIENT_OPTIONS
|
|
168
179
|
end
|
|
169
180
|
end
|
data/lib/daytona/process.rb
CHANGED
|
@@ -257,7 +257,7 @@ module Daytona
|
|
|
257
257
|
|
|
258
258
|
completion_queue = Queue.new
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
Common::WebSocketDialer.connect(
|
|
261
261
|
url.to_s,
|
|
262
262
|
headers: toolbox_api.api_client.default_headers.dup.merge(
|
|
263
263
|
'X-Daytona-Preview-Token' => preview_link.token,
|
|
@@ -326,7 +326,7 @@ module Daytona
|
|
|
326
326
|
|
|
327
327
|
completion_queue = Queue.new
|
|
328
328
|
|
|
329
|
-
|
|
329
|
+
Common::WebSocketDialer.connect(
|
|
330
330
|
url.to_s,
|
|
331
331
|
headers: toolbox_api.api_client.default_headers.dup.merge(
|
|
332
332
|
'X-Daytona-Preview-Token' => preview_link.token,
|
|
@@ -465,7 +465,7 @@ module Daytona
|
|
|
465
465
|
headers['Sec-WebSocket-Protocol'] = protocols.join(', ')
|
|
466
466
|
|
|
467
467
|
PtyHandle.new(
|
|
468
|
-
|
|
468
|
+
Common::WebSocketDialer.connect(url.to_s, headers:),
|
|
469
469
|
session_id: id,
|
|
470
470
|
handle_resize: ->(pty_size_arg) { resize_pty_session(id, pty_size_arg) },
|
|
471
471
|
handle_kill: -> { delete_pty_session(id) }
|
|
@@ -502,7 +502,7 @@ module Daytona
|
|
|
502
502
|
[headers['Sec-WebSocket-Protocol'], PTY_EXIT_CONTROL_SUBPROTOCOL].compact.join(', ')
|
|
503
503
|
|
|
504
504
|
handle = nil
|
|
505
|
-
|
|
505
|
+
Common::WebSocketDialer.connect(url.to_s, headers:) do |client|
|
|
506
506
|
handle = PtyHandle.new(
|
|
507
507
|
client,
|
|
508
508
|
session_id:,
|
data/lib/daytona/sdk/version.rb
CHANGED
data/lib/daytona/sdk.rb
CHANGED
|
@@ -33,6 +33,7 @@ require_relative 'common/snapshot'
|
|
|
33
33
|
require_relative 'code_interpreter'
|
|
34
34
|
require_relative 'computer_use'
|
|
35
35
|
require_relative 'common/socketio_client'
|
|
36
|
+
require_relative 'common/websocket_dialer'
|
|
36
37
|
require_relative 'common/event_dispatcher'
|
|
37
38
|
require_relative 'common/event_subscription_manager'
|
|
38
39
|
require_relative 'daytona'
|
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.214.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daytona Platforms Inc.
|
|
@@ -69,58 +69,64 @@ dependencies:
|
|
|
69
69
|
name: aws-sdk-s3
|
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
|
71
71
|
requirements:
|
|
72
|
-
- - "
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 1.197.0
|
|
75
|
+
- - "<"
|
|
73
76
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '
|
|
77
|
+
version: '2'
|
|
75
78
|
type: :runtime
|
|
76
79
|
prerelease: false
|
|
77
80
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
81
|
requirements:
|
|
79
|
-
- - "
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: 1.197.0
|
|
85
|
+
- - "<"
|
|
80
86
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: '
|
|
87
|
+
version: '2'
|
|
82
88
|
- !ruby/object:Gem::Dependency
|
|
83
89
|
name: daytona_analytics_api_client
|
|
84
90
|
requirement: !ruby/object:Gem::Requirement
|
|
85
91
|
requirements:
|
|
86
92
|
- - '='
|
|
87
93
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: 0.
|
|
94
|
+
version: 0.214.0
|
|
89
95
|
type: :runtime
|
|
90
96
|
prerelease: false
|
|
91
97
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
98
|
requirements:
|
|
93
99
|
- - '='
|
|
94
100
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: 0.
|
|
101
|
+
version: 0.214.0
|
|
96
102
|
- !ruby/object:Gem::Dependency
|
|
97
103
|
name: daytona_api_client
|
|
98
104
|
requirement: !ruby/object:Gem::Requirement
|
|
99
105
|
requirements:
|
|
100
106
|
- - '='
|
|
101
107
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: 0.
|
|
108
|
+
version: 0.214.0
|
|
103
109
|
type: :runtime
|
|
104
110
|
prerelease: false
|
|
105
111
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
112
|
requirements:
|
|
107
113
|
- - '='
|
|
108
114
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: 0.
|
|
115
|
+
version: 0.214.0
|
|
110
116
|
- !ruby/object:Gem::Dependency
|
|
111
117
|
name: daytona_toolbox_api_client
|
|
112
118
|
requirement: !ruby/object:Gem::Requirement
|
|
113
119
|
requirements:
|
|
114
120
|
- - '='
|
|
115
121
|
- !ruby/object:Gem::Version
|
|
116
|
-
version: 0.
|
|
122
|
+
version: 0.214.0
|
|
117
123
|
type: :runtime
|
|
118
124
|
prerelease: false
|
|
119
125
|
version_requirements: !ruby/object:Gem::Requirement
|
|
120
126
|
requirements:
|
|
121
127
|
- - '='
|
|
122
128
|
- !ruby/object:Gem::Version
|
|
123
|
-
version: 0.
|
|
129
|
+
version: 0.214.0
|
|
124
130
|
- !ruby/object:Gem::Dependency
|
|
125
131
|
name: dotenv
|
|
126
132
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -175,14 +181,14 @@ dependencies:
|
|
|
175
181
|
requirements:
|
|
176
182
|
- - "~>"
|
|
177
183
|
- !ruby/object:Gem::Version
|
|
178
|
-
version:
|
|
184
|
+
version: 0.9.0
|
|
179
185
|
type: :runtime
|
|
180
186
|
prerelease: false
|
|
181
187
|
version_requirements: !ruby/object:Gem::Requirement
|
|
182
188
|
requirements:
|
|
183
189
|
- - "~>"
|
|
184
190
|
- !ruby/object:Gem::Version
|
|
185
|
-
version:
|
|
191
|
+
version: 0.9.0
|
|
186
192
|
description: 'High-level Ruby SDK for Daytona: sandboxes, git, filesystem, LSP, process,
|
|
187
193
|
and object storage. Requires Ruby >= 3.2.'
|
|
188
194
|
email:
|
|
@@ -215,6 +221,7 @@ files:
|
|
|
215
221
|
- lib/daytona/common/response.rb
|
|
216
222
|
- lib/daytona/common/snapshot.rb
|
|
217
223
|
- lib/daytona/common/socketio_client.rb
|
|
224
|
+
- lib/daytona/common/websocket_dialer.rb
|
|
218
225
|
- lib/daytona/computer_use.rb
|
|
219
226
|
- lib/daytona/config.rb
|
|
220
227
|
- lib/daytona/daytona.rb
|