daytona 0.215.0 → 0.216.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95395a0e8e8a1c853fbbb922baf90d9fe6247dc64454589b82692c058aa1c60f
4
- data.tar.gz: 588d86f42d524472b0e80ce92cc1931ac180eccb8e0a7ae889c9660a94181490
3
+ metadata.gz: 3dd541cde2443a14874df0886c91b5d1480a440a85ad028634482e6fbe168bfc
4
+ data.tar.gz: d883303c65baddabb54bba81137fda52907e1f963d0a85caf5c6dd845988d462
5
5
  SHA512:
6
- metadata.gz: dc20a5a6fc3c2f8614b3318f0e5905c9987d7f9801e99e3d389b1abee89d258a50c13c95bda30548ecb31f406f589786c8f9eeb9f40021f6d27434c8480872ee
7
- data.tar.gz: 9ef485e7c32dceaf5f52760db0cf3589a88800a1c2e95c3dede6519ca3922834522ef73e3e0f4278fa079d02f755ceaf9696a9c0b000817a4f73b6f1f073f771
6
+ metadata.gz: a1b0a946fc2f2d47e3fadcf16166b3035307999be2e594f283cface6b6cc99cd9e4a2a1b679c5bccab0fc059dc779414ea04d81d0c0eccb9533b068397606da0
7
+ data.tar.gz: 3a55596b08d5d38e11c837534dac68217dec2172e55ea97c3b45151ccc7f683855a19e269263ea00bc27c815f2634f86fb04bb3102f123cc5312e2e0c987f73b
@@ -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
- if context_dir
264
- context_dir = File.expand_path(context_dir)
265
- raise Sdk::Error, "Context directory #{context_dir} does not exist" unless Dir.exist?(context_dir)
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, dockerfile_commands.join("\n"),
270
- context_dir || '').each do |context_path, original_path|
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
- if context_dir && !original_path.start_with?(context_dir)
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 += "#{dockerfile_commands.join("\n")}\n"
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 { |matching_file| sources << [matching_file, source] }
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
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Daytona
7
7
  module Sdk
8
- VERSION = '0.215.0'
8
+ VERSION = '0.216.1'
9
9
  end
10
10
  end
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.215.0
4
+ version: 0.216.1
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.215.0
94
+ version: 0.216.1
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.215.0
101
+ version: 0.216.1
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.215.0
108
+ version: 0.216.1
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.215.0
115
+ version: 0.216.1
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.215.0
122
+ version: 0.216.1
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.215.0
129
+ version: 0.216.1
130
130
  - !ruby/object:Gem::Dependency
131
131
  name: dotenv
132
132
  requirement: !ruby/object:Gem::Requirement