daytona 0.215.0 → 0.216.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/image.rb +145 -16
- data/lib/daytona/sdk/version.rb +1 -1
- metadata +7 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a34e3a8e7bbc3cc889eb3ef79b13b306355d55860a8f50148ce81b45c5df56f
|
|
4
|
+
data.tar.gz: a56bb3a488e721f05722f8cba62ced2253bdcec09491b376f68cc73da1e53f6c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06503be06f45fdbacf029776b60896ea595ebfc562e2882d1005a3201a43172dbd23db1d8a5fdd9a7db2bffa9247f53ff74e5c7c44d8e392ad5732eb9ed14da5
|
|
7
|
+
data.tar.gz: 79e93c94ce080d2dffc0eb1f01bc13d9b43ec70a3d68d452dc981c7e54f23467206f09184b32f0ca4641dc8d23392cac2302b9ae068f02b676a8507b9943a5be
|
data/lib/daytona/common/image.rb
CHANGED
|
@@ -255,27 +255,28 @@ module Daytona
|
|
|
255
255
|
#
|
|
256
256
|
# @param dockerfile_commands [Array<String>] The commands to add to the Dockerfile
|
|
257
257
|
# @param context_dir [String, nil] The path to the context directory
|
|
258
|
+
# @param strict_context [Boolean] When true, a COPY source that resolves outside context_dir is
|
|
259
|
+
# rejected instead of read. Requires context_dir, so that the boundary is explicit rather than
|
|
260
|
+
# taken from the working directory. Sources that legitimately live elsewhere belong in
|
|
261
|
+
# add_local_file or add_local_dir
|
|
258
262
|
# @return [Image] The image with the Dockerfile commands added
|
|
259
263
|
#
|
|
260
264
|
# @example
|
|
261
265
|
# image = Image.debian_slim("3.12").dockerfile_commands(["RUN echo 'Hello, world!'"])
|
|
262
|
-
def dockerfile_commands(dockerfile_commands, context_dir: nil) # rubocop:disable Metrics/MethodLength
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
end
|
|
266
|
+
def dockerfile_commands(dockerfile_commands, context_dir: nil, strict_context: false) # rubocop:disable Metrics/MethodLength
|
|
267
|
+
context_dir = self.class.send(:validate_context_dir, context_dir, strict_context)
|
|
268
|
+
root, real_root = self.class.send(:context_boundary, context_dir, strict_context)
|
|
269
|
+
commands = dockerfile_commands.join("\n")
|
|
267
270
|
|
|
268
271
|
# Extract copy sources from dockerfile commands (class-level helper)
|
|
269
|
-
self.class.send(:extract_copy_sources,
|
|
270
|
-
|
|
272
|
+
copy_sources = self.class.send(:extract_copy_sources, commands, context_dir || '', real_root)
|
|
273
|
+
copy_sources.each do |context_path, original_path|
|
|
271
274
|
archive_base_path = context_path
|
|
272
|
-
|
|
273
|
-
archive_base_path = context_path.delete_prefix(context_dir)
|
|
274
|
-
end
|
|
275
|
+
archive_base_path = context_path.delete_prefix(root) unless original_path.start_with?(root)
|
|
275
276
|
@context_list << Context.new(source_path: context_path, archive_path: archive_base_path)
|
|
276
277
|
end
|
|
277
278
|
|
|
278
|
-
@dockerfile += "#{
|
|
279
|
+
@dockerfile += "#{commands}\n"
|
|
279
280
|
self
|
|
280
281
|
end
|
|
281
282
|
|
|
@@ -283,19 +284,23 @@ module Daytona
|
|
|
283
284
|
# Creates an Image from an existing Dockerfile
|
|
284
285
|
#
|
|
285
286
|
# @param path [String] The path to the Dockerfile
|
|
287
|
+
# @param strict_context [Boolean] When true, a COPY source that resolves outside the
|
|
288
|
+
# Dockerfile's directory is rejected instead of read. Sources that legitimately live
|
|
289
|
+
# elsewhere belong in add_local_file or add_local_dir
|
|
286
290
|
# @return [Image] The image with the Dockerfile added
|
|
287
291
|
#
|
|
288
292
|
# @example
|
|
289
293
|
# image = Image.from_dockerfile("Dockerfile")
|
|
290
|
-
def from_dockerfile(path) # rubocop:disable Metrics/AbcSize
|
|
294
|
+
def from_dockerfile(path, strict_context: false) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
291
295
|
path = Pathname.new(File.expand_path(path))
|
|
292
296
|
dockerfile = path.read
|
|
293
297
|
img = new(dockerfile: dockerfile)
|
|
294
298
|
|
|
295
299
|
# Remove dockerfile filename from path
|
|
296
300
|
path_prefix = path.to_s.delete_suffix(path.basename.to_s)
|
|
301
|
+
real_root = strict_root(path_prefix, strict_context)
|
|
297
302
|
|
|
298
|
-
extract_copy_sources(dockerfile, path_prefix).each do |context_path, original_path|
|
|
303
|
+
extract_copy_sources(dockerfile, path_prefix, real_root).each do |context_path, original_path|
|
|
299
304
|
archive_base_path = context_path
|
|
300
305
|
archive_base_path = context_path.delete_prefix(path_prefix) unless original_path.start_with?(path_prefix)
|
|
301
306
|
img.context_list << Context.new(source_path: context_path, archive_path: archive_base_path)
|
|
@@ -361,7 +366,7 @@ module Daytona
|
|
|
361
366
|
# @param dockerfile_content [String] The content of the Dockerfile
|
|
362
367
|
# @param path_prefix [String] The path prefix to use for the sources
|
|
363
368
|
# @return [Array<Array<String>>] The list of the actual file path and its corresponding COPY-command source path
|
|
364
|
-
def extract_copy_sources(dockerfile_content, path_prefix = '') # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
369
|
+
def extract_copy_sources(dockerfile_content, path_prefix = '', real_root = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
365
370
|
sources = []
|
|
366
371
|
lines = dockerfile_logical_lines(dockerfile_content)
|
|
367
372
|
|
|
@@ -381,22 +386,28 @@ module Daytona
|
|
|
381
386
|
|
|
382
387
|
# Get source paths from the parsed command parts
|
|
383
388
|
command_parts['sources'].each do |source|
|
|
389
|
+
ensure_source_within_context(source) unless real_root.nil?
|
|
390
|
+
|
|
384
391
|
# Handle absolute and relative paths differently
|
|
385
392
|
full_path_pattern = if Pathname.new(source).absolute?
|
|
386
393
|
# Absolute path - use as is
|
|
387
394
|
source
|
|
388
395
|
else
|
|
389
396
|
# Relative path - add prefix
|
|
390
|
-
File.join(path_prefix, source)
|
|
397
|
+
File.join(context_root(path_prefix), source)
|
|
391
398
|
end
|
|
392
399
|
|
|
393
400
|
# Handle glob patterns
|
|
394
401
|
matching_files = Dir.glob(full_path_pattern)
|
|
395
402
|
|
|
396
403
|
if matching_files.any?
|
|
397
|
-
matching_files.each
|
|
404
|
+
matching_files.each do |matching_file|
|
|
405
|
+
ensure_within_build_context(real_root, matching_file)
|
|
406
|
+
sources << [matching_file, source]
|
|
407
|
+
end
|
|
398
408
|
else
|
|
399
409
|
# If no files match, include the pattern anyway
|
|
410
|
+
ensure_within_build_context(real_root, full_path_pattern)
|
|
400
411
|
sources << [full_path_pattern, source]
|
|
401
412
|
end
|
|
402
413
|
end
|
|
@@ -405,6 +416,124 @@ module Daytona
|
|
|
405
416
|
sources
|
|
406
417
|
end
|
|
407
418
|
|
|
419
|
+
# Validates the context directory and the strict_context combination
|
|
420
|
+
#
|
|
421
|
+
# @param context_dir [String, nil] The caller-supplied context directory
|
|
422
|
+
# @param strict_context [Boolean] Whether the build context boundary is enforced
|
|
423
|
+
# @return [String, nil] The expanded context directory
|
|
424
|
+
# @raise [Sdk::Error] If the directory is missing, or strict_context was asked for without one
|
|
425
|
+
def validate_context_dir(context_dir, strict_context)
|
|
426
|
+
# An empty string is truthy in Ruby and expands to the working directory, which would let
|
|
427
|
+
# a strict context take its boundary from wherever the process was started
|
|
428
|
+
context_dir = nil if context_dir.nil? || context_dir.to_s.strip.empty?
|
|
429
|
+
|
|
430
|
+
if context_dir
|
|
431
|
+
context_dir = File.expand_path(context_dir)
|
|
432
|
+
raise Sdk::Error, "Context directory #{context_dir} does not exist" unless Dir.exist?(context_dir)
|
|
433
|
+
elsif strict_context
|
|
434
|
+
raise Sdk::Error, 'strict_context requires context_dir so that the build context boundary is explicit'
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
context_dir
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# The join root and the strict boundary for a set of COPY sources
|
|
441
|
+
#
|
|
442
|
+
# @param context_dir [String, nil] The expanded context directory
|
|
443
|
+
# @param strict_context [Boolean] Whether the build context boundary is enforced
|
|
444
|
+
# @return [Array<String, String, nil>] The join root and the strict boundary
|
|
445
|
+
def context_boundary(context_dir, strict_context)
|
|
446
|
+
[context_root(context_dir || ''), strict_root(context_dir, strict_context)]
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# The resolved boundary a strict build context is enforced against, or nil when the caller
|
|
450
|
+
# did not ask for one
|
|
451
|
+
#
|
|
452
|
+
# @param root [String, nil] The build context root
|
|
453
|
+
# @param strict_context [Boolean] Whether the build context boundary is enforced
|
|
454
|
+
# @return [String, nil] The resolved boundary, or nil
|
|
455
|
+
def strict_root(root, strict_context)
|
|
456
|
+
return nil unless strict_context
|
|
457
|
+
|
|
458
|
+
real_path(root) || root
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# The build context root that a COPY source is resolved against. An empty prefix means
|
|
462
|
+
# the caller supplied no context directory, in which case `docker build .` semantics
|
|
463
|
+
# apply and the working directory is the context.
|
|
464
|
+
#
|
|
465
|
+
# @param path_prefix [String, nil] The path prefix the sources are resolved against
|
|
466
|
+
# @return [String] The build context root
|
|
467
|
+
def context_root(path_prefix)
|
|
468
|
+
path_prefix.nil? || path_prefix.empty? ? Dir.pwd : path_prefix
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Rejects a COPY source that names something outside the build context. This is decided on
|
|
472
|
+
# the source itself, before anything is read, so that a source is accepted or rejected the
|
|
473
|
+
# same way whether or not it happens to exist on disk. An absolute source, including a
|
|
474
|
+
# Windows drive, UNC or root-relative path, and one whose normalised form climbs above the
|
|
475
|
+
# context both name something the build context cannot address. Backslashes are read as
|
|
476
|
+
# separators, since that is what they are on Windows.
|
|
477
|
+
#
|
|
478
|
+
# @param source [String] The COPY-command source path
|
|
479
|
+
# @raise [Sdk::Error] If the source names something outside the build context
|
|
480
|
+
def ensure_source_within_context(source)
|
|
481
|
+
# A backslash is a separator on Windows, so fold it before deciding: otherwise
|
|
482
|
+
# '..\secret.txt' reads as an ordinary filename here and as a traversal there.
|
|
483
|
+
candidate = Pathname.new(source.tr('\\', '/'))
|
|
484
|
+
normalized = candidate.cleanpath.to_s
|
|
485
|
+
outside = candidate.absolute? ||
|
|
486
|
+
source.match?(/\A[A-Za-z]:/) ||
|
|
487
|
+
normalized == '..' ||
|
|
488
|
+
normalized.start_with?('../')
|
|
489
|
+
return unless outside
|
|
490
|
+
|
|
491
|
+
raise Sdk::Error, "forbidden path outside the build context: #{source}"
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Rejects a source that resolves outside the build context. Normalisation alone cannot see
|
|
495
|
+
# this: a symlinked parent directory is traversed transparently by the archiver, so a
|
|
496
|
+
# regular file reached through one is stored with its contents even though the written
|
|
497
|
+
# path stays inside the context.
|
|
498
|
+
#
|
|
499
|
+
# @param real_root [String] The resolved build context root
|
|
500
|
+
# @param candidate [String] The resolved source path to check
|
|
501
|
+
# @raise [Sdk::Error] If the candidate resolves outside the build context
|
|
502
|
+
def ensure_within_build_context(real_root, candidate)
|
|
503
|
+
# real_root is nil unless the caller asked for a strict build context
|
|
504
|
+
return if real_root.nil?
|
|
505
|
+
|
|
506
|
+
resolved = real_path(candidate)
|
|
507
|
+
# A path that does not exist cannot be archived. A symlink whose target is missing still
|
|
508
|
+
# leaves the context, so it is rejected rather than tolerated.
|
|
509
|
+
return if resolved.nil? && !File.symlink?(candidate)
|
|
510
|
+
|
|
511
|
+
raise Sdk::Error, "forbidden path outside the build context: #{candidate}" if resolved.nil?
|
|
512
|
+
raise Sdk::Error, "forbidden path outside the build context: #{resolved}" unless within?(real_root, resolved)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Whether a resolved path lies inside the resolved build context root
|
|
516
|
+
#
|
|
517
|
+
# @param real_root [String] The resolved build context root
|
|
518
|
+
# @param resolved [String] The resolved candidate path
|
|
519
|
+
# @return [Boolean]
|
|
520
|
+
def within?(real_root, resolved)
|
|
521
|
+
relative = Pathname.new(resolved).relative_path_from(Pathname.new(real_root)).to_s
|
|
522
|
+
relative != '..' && !relative.start_with?("..#{File::SEPARATOR}")
|
|
523
|
+
rescue ArgumentError
|
|
524
|
+
false
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Resolves a path to its real location, or nil when it cannot be resolved
|
|
528
|
+
#
|
|
529
|
+
# @param path [String] The path to resolve
|
|
530
|
+
# @return [String, nil] The real path, or nil
|
|
531
|
+
def real_path(path)
|
|
532
|
+
File.realpath(path)
|
|
533
|
+
rescue SystemCallError
|
|
534
|
+
nil
|
|
535
|
+
end
|
|
536
|
+
|
|
408
537
|
# Joins backslash-continued physical lines into logical Dockerfile instruction lines
|
|
409
538
|
#
|
|
410
539
|
# @param dockerfile_content [String] The content of the Dockerfile
|
data/lib/daytona/sdk/version.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.216.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daytona Platforms Inc.
|
|
@@ -91,42 +91,42 @@ dependencies:
|
|
|
91
91
|
requirements:
|
|
92
92
|
- - '='
|
|
93
93
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: 0.
|
|
94
|
+
version: 0.216.0
|
|
95
95
|
type: :runtime
|
|
96
96
|
prerelease: false
|
|
97
97
|
version_requirements: !ruby/object:Gem::Requirement
|
|
98
98
|
requirements:
|
|
99
99
|
- - '='
|
|
100
100
|
- !ruby/object:Gem::Version
|
|
101
|
-
version: 0.
|
|
101
|
+
version: 0.216.0
|
|
102
102
|
- !ruby/object:Gem::Dependency
|
|
103
103
|
name: daytona_api_client
|
|
104
104
|
requirement: !ruby/object:Gem::Requirement
|
|
105
105
|
requirements:
|
|
106
106
|
- - '='
|
|
107
107
|
- !ruby/object:Gem::Version
|
|
108
|
-
version: 0.
|
|
108
|
+
version: 0.216.0
|
|
109
109
|
type: :runtime
|
|
110
110
|
prerelease: false
|
|
111
111
|
version_requirements: !ruby/object:Gem::Requirement
|
|
112
112
|
requirements:
|
|
113
113
|
- - '='
|
|
114
114
|
- !ruby/object:Gem::Version
|
|
115
|
-
version: 0.
|
|
115
|
+
version: 0.216.0
|
|
116
116
|
- !ruby/object:Gem::Dependency
|
|
117
117
|
name: daytona_toolbox_api_client
|
|
118
118
|
requirement: !ruby/object:Gem::Requirement
|
|
119
119
|
requirements:
|
|
120
120
|
- - '='
|
|
121
121
|
- !ruby/object:Gem::Version
|
|
122
|
-
version: 0.
|
|
122
|
+
version: 0.216.0
|
|
123
123
|
type: :runtime
|
|
124
124
|
prerelease: false
|
|
125
125
|
version_requirements: !ruby/object:Gem::Requirement
|
|
126
126
|
requirements:
|
|
127
127
|
- - '='
|
|
128
128
|
- !ruby/object:Gem::Version
|
|
129
|
-
version: 0.
|
|
129
|
+
version: 0.216.0
|
|
130
130
|
- !ruby/object:Gem::Dependency
|
|
131
131
|
name: dotenv
|
|
132
132
|
requirement: !ruby/object:Gem::Requirement
|