Class: Daytona::Image

Inherits:
Object
  • Object
show all
Defined in:
lib/daytona/common/image.rb

Overview

Represents an image definition for a Daytona sandbox. Do not construct this class directly. Instead use one of its static factory methods, such as ‘Image.base()`, `Image.debian_slim()`, or `Image.from_dockerfile()`.

Constant Summary collapse

SUPPORTED_PYTHON_SERIES =

Supported Python series

%w[3.9 3.10 3.11 3.12 3.13].freeze
LATEST_PYTHON_MICRO_VERSIONS =
%w[3.9.22 3.10.17 3.11.12 3.12.10 3.13.3].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dockerfile: nil, context_list: []) ⇒ Image

Returns a new instance of Image.

Parameters:

  • dockerfile (String, nil) (defaults to: nil)

    The Dockerfile content

  • context_list (Array<Context>) (defaults to: [])

    List of context files



37
38
39
40
# File 'lib/daytona/common/image.rb', line 37

def initialize(dockerfile: nil, context_list: [])
  @dockerfile = dockerfile || ''
  @context_list = context_list
end

Instance Attribute Details

#context_listArray<Context> (readonly)

Returns List of context files for the image.

Returns:

  • (Array<Context>)

    List of context files for the image



29
30
31
# File 'lib/daytona/common/image.rb', line 29

def context_list
  @context_list
end

#dockerfileString? (readonly)

Returns The generated Dockerfile for the image.

Returns:

  • (String, nil)

    The generated Dockerfile for the image



26
27
28
# File 'lib/daytona/common/image.rb', line 26

def dockerfile
  @dockerfile
end

Class Method Details

.base(image) ⇒ Image

Creates an Image from an existing base image

Examples:

image = Image.base("python:3.12-slim-bookworm")

Parameters:

  • image (String)

    The base image to use

Returns:

  • (Image)

    The image with the base image added



309
310
311
312
313
# File 'lib/daytona/common/image.rb', line 309

def base(image)
  img = new
  img.instance_variable_set(:@dockerfile, "FROM #{image}\n")
  img
end

.debian_slim(python_version = nil) ⇒ Image

Creates a Debian slim image based on the official Python Docker image

Examples:

image = Image.debian_slim("3.12")

Parameters:

  • python_version (String, nil) (defaults to: nil)

    The Python version to use

Returns:

  • (Image)

    The image with the Debian slim image added



322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/daytona/common/image.rb', line 322

def debian_slim(python_version = nil) # rubocop:disable Metrics/MethodLength
  python_version = process_python_version(python_version)
  img = new
  commands = [
    "FROM python:#{python_version}-slim-bookworm",
    'RUN apt-get update',
    'RUN apt-get install -y gcc gfortran build-essential',
    'RUN pip install --upgrade pip',
    # Set debian front-end to non-interactive to avoid users getting stuck with input prompts.
    "RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections"
  ]
  img.instance_variable_set(:@dockerfile, "#{commands.join("\n")}\n")
  img
end

.from_dockerfile(path) ⇒ Image

Creates an Image from an existing Dockerfile

Examples:

image = Image.from_dockerfile("Dockerfile")

Parameters:

  • path (String)

    The path to the Dockerfile

Returns:

  • (Image)

    The image with the Dockerfile added



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/daytona/common/image.rb', line 285

def from_dockerfile(path) # rubocop:disable Metrics/AbcSize
  path = Pathname.new(File.expand_path(path))
  dockerfile = path.read
  img = new(dockerfile: dockerfile)

  # Remove dockerfile filename from path
  path_prefix = path.to_s.delete_suffix(path.basename.to_s)

  extract_copy_sources(dockerfile, path_prefix).each do |context_path, original_path|
    archive_base_path = context_path
    archive_base_path = context_path.delete_prefix(path_prefix) unless original_path.start_with?(path_prefix)
    img.context_list << Context.new(source_path: context_path, archive_path: archive_base_path)
  end

  img
end

Instance Method Details

#add_local_dir(local_path, remote_path) ⇒ Image

Adds a local directory to the image

Examples:

image = Image.debian_slim("3.12").add_local_dir("src", "/home/daytona/src")

Parameters:

  • local_path (String)

    The path to the local directory

  • remote_path (String)

    The path to the directory in the image

Returns:

  • (Image)

    The image with the local directory added



154
155
156
157
158
159
160
161
# File 'lib/daytona/common/image.rb', line 154

def add_local_dir(local_path, remote_path)
  local_path = File.expand_path(local_path)
  archive_path = ObjectStorage.compute_archive_base_path(local_path)
  @context_list << Context.new(source_path: local_path, archive_path: archive_path)
  @dockerfile += "COPY #{archive_path} #{remote_path}\n"

  self
end

#add_local_file(local_path, remote_path) ⇒ Image

Adds a local file to the image

Examples:

image = Image.debian_slim("3.12").add_local_file("package.json", "/home/daytona/package.json")

Parameters:

  • local_path (String)

    The path to the local file

  • remote_path (String)

    The path to the file in the image

Returns:

  • (Image)

    The image with the local file added



135
136
137
138
139
140
141
142
143
144
# File 'lib/daytona/common/image.rb', line 135

def add_local_file(local_path, remote_path)
  remote_path = "#{remote_path}/#{File.basename(local_path)}" if remote_path.end_with?('/')

  local_path = File.expand_path(local_path)
  archive_path = ObjectStorage.compute_archive_base_path(local_path)
  @context_list << Context.new(source_path: local_path, archive_path: archive_path)
  @dockerfile += "COPY #{archive_path} #{remote_path}\n"

  self
end

#cmd(cmd) ⇒ Image

Sets the default command for the image

Examples:

image = Image.debian_slim("3.12").cmd(["/bin/bash"])

Parameters:

  • cmd (Array<String>)

    The commands to set as the default command

Returns:

  • (Image)

    The image with the default command added



239
240
241
242
243
244
245
246
247
248
# File 'lib/daytona/common/image.rb', line 239

def cmd(cmd)
  unless cmd.is_a?(Array) && cmd.all? { |x| x.is_a?(String) }
    raise Sdk::Error, 'Image CMD must be a list of strings.'
  end

  cmd_str = flatten_str_args('cmd', 'cmd', cmd)
  cmd_str = cmd_str.map { |arg| "\"#{arg}\"" }.join(', ') if cmd_str.any?
  @dockerfile += "CMD [#{cmd_str}]\n"
  self
end

#dockerfile_commands(dockerfile_commands, context_dir: nil) ⇒ Image

Adds arbitrary Dockerfile-like commands to the image

Examples:

image = Image.debian_slim("3.12").dockerfile_commands(["RUN echo 'Hello, world!'"])

Parameters:

  • dockerfile_commands (Array<String>)

    The commands to add to the Dockerfile

  • context_dir (String, nil) (defaults to: nil)

    The path to the context directory

Returns:

  • (Image)

    The image with the Dockerfile commands added



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/daytona/common/image.rb', line 258

def dockerfile_commands(dockerfile_commands, context_dir: nil) # rubocop:disable Metrics/MethodLength
  if context_dir
    context_dir = File.expand_path(context_dir)
    raise Sdk::Error, "Context directory #{context_dir} does not exist" unless Dir.exist?(context_dir)
  end

  # Extract copy sources from dockerfile commands
  extract_copy_sources(dockerfile_commands.join("\n"), context_dir || '').each do |context_path, original_path|
    archive_base_path = context_path
    if context_dir && !original_path.start_with?(context_dir)
      archive_base_path = context_path.delete_prefix(context_dir)
    end
    @context_list << Context.new(source_path: context_path, archive_path: archive_base_path)
  end

  @dockerfile += "#{dockerfile_commands.join("\n")}\n"
  self
end

#entrypoint(entrypoint_commands) ⇒ Image

Sets the entrypoint for the image

Examples:

image = Image.debian_slim("3.12").entrypoint(["/bin/bash"])

Parameters:

  • entrypoint_commands (Array<String>)

    The commands to set as the entrypoint

Returns:

  • (Image)

    The image with the entrypoint added



220
221
222
223
224
225
226
227
228
229
230
# File 'lib/daytona/common/image.rb', line 220

def entrypoint(entrypoint_commands)
  unless entrypoint_commands.is_a?(Array) && entrypoint_commands.all? { |x| x.is_a?(String) }
    raise Sdk::Error, 'entrypoint_commands must be a list of strings.'
  end

  args_str = flatten_str_args('entrypoint', 'entrypoint_commands', entrypoint_commands)
  args_str = args_str.map { |arg| "\"#{arg}\"" }.join(', ') if args_str.any?
  @dockerfile += "ENTRYPOINT [#{args_str}]\n"

  self
end

#env(env_vars) ⇒ Image

Sets environment variables in the image

Examples:

image = Image.debian_slim("3.12").env({"PROJECT_ROOT" => "/home/daytona"})

Parameters:

  • env_vars (Hash<String, String>)

    The environment variables to set

Returns:

  • (Image)

    The image with the environment variables added

Raises:



190
191
192
193
194
195
196
197
198
199
# File 'lib/daytona/common/image.rb', line 190

def env(env_vars)
  non_str_keys = env_vars.reject { |_key, val| val.is_a?(String) }.keys
  raise Sdk::Error, "Image ENV variables must be strings. Invalid keys: #{non_str_keys}" unless non_str_keys.empty?

  env_vars.each do |key, val|
    @dockerfile += "ENV #{key}=#{Shellwords.escape(val)}\n"
  end

  self
end

#pip_install(*packages, find_links: nil, index_url: nil, extra_index_urls: nil, pre: false, extra_options: '') ⇒ Image

Adds commands to install packages using pip

Examples:

image = Image.debian_slim("3.12").pip_install("requests", "pandas")

Parameters:

  • packages (Array<String>)

    The packages to install

  • find_links (Array<String>, nil) (defaults to: nil)

    The find-links to use

  • index_url (String, nil) (defaults to: nil)

    The index URL to use

  • extra_index_urls (Array<String>, nil) (defaults to: nil)

    The extra index URLs to use

  • pre (Boolean) (defaults to: false)

    Whether to install pre-release packages

  • extra_options (String) (defaults to: '')

    Additional options to pass to pip

Returns:

  • (Image)

    The image with the pip install commands added



54
55
56
57
58
59
60
61
62
# File 'lib/daytona/common/image.rb', line 54

def pip_install(*packages, find_links: nil, index_url: nil, extra_index_urls: nil, pre: false, extra_options: '') # rubocop:disable Metrics/ParameterLists
  pkgs = flatten_str_args('pip_install', 'packages', packages)
  return self if pkgs.empty?

  extra_args = format_pip_install_args(find_links:, index_url:, extra_index_urls:, pre:, extra_options:)
  @dockerfile += "RUN python -m pip install #{Shellwords.join(pkgs.sort)}#{extra_args}\n"

  self
end

#pip_install_from_pyproject(pyproject_toml, optional_dependencies: [], find_links: nil, index_url: nil, extra_index_url: nil, pre: false, extra_options: '') ⇒ Image

Installs dependencies from a pyproject.toml file

Examples:

image = Image.debian_slim("3.12").pip_install_from_pyproject("pyproject.toml", optional_dependencies: ["dev"])

Parameters:

  • pyproject_toml (String)

    The path to the pyproject.toml file

  • optional_dependencies (Array<String>) (defaults to: [])

    The optional dependencies to install

  • find_links (String, nil) (defaults to: nil)

    The find-links to use

  • index_url (String, nil) (defaults to: nil)

    The index URL to use

  • extra_index_url (String, nil) (defaults to: nil)

    The extra index URL to use

  • pre (Boolean) (defaults to: false)

    Whether to install pre-release packages

  • extra_options (String) (defaults to: '')

    Additional options to pass to pip

Returns:

  • (Image)

    The image with the pip install commands added

Raises:

  • (Sdk::Error)

    If pyproject.toml parsing is not supported



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/daytona/common/image.rb', line 106

def pip_install_from_pyproject(pyproject_toml, optional_dependencies: [], find_links: nil, index_url: nil, # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
                               extra_index_url: nil, pre: false, extra_options: '')
  data = TOML.load_file(pyproject_toml)
  dependencies = data.dig('project', 'dependencies')

  unless dependencies
    raise Sdk::Error, 'No [project.dependencies] section in pyproject.toml file. ' \
                      'See https://packaging.python.org/en/latest/guides/writing-pyproject-toml ' \
                      'for further file format guidelines.'
  end

  return unless optional_dependencies

  optionals = data.dig('project', 'optional-dependencies')
  optional_dependencies.each do |group|
    dependencies.concat(optionals.fetch(group, []))
  end

  pip_install(*dependencies, find_links:, index_url:, extra_index_urls: extra_index_url, pre:, extra_options:)
end

#pip_install_from_requirements(requirements_txt, find_links: nil, index_url: nil, extra_index_urls: nil, pre: false, extra_options: '') ⇒ Image

Installs dependencies from a requirements.txt file

Examples:

image = Image.debian_slim("3.12").pip_install_from_requirements("requirements.txt")

Parameters:

  • requirements_txt (String)

    The path to the requirements.txt file

  • find_links (Array<String>, nil) (defaults to: nil)

    The find-links to use

  • index_url (String, nil) (defaults to: nil)

    The index URL to use

  • extra_index_urls (Array<String>, nil) (defaults to: nil)

    The extra index URLs to use

  • pre (Boolean) (defaults to: false)

    Whether to install pre-release packages

  • extra_options (String) (defaults to: '')

    Additional options to pass to pip

Returns:

  • (Image)

    The image with the pip install commands added

Raises:

  • (Sdk::Error)

    If the requirements file does not exist



77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/daytona/common/image.rb', line 77

def pip_install_from_requirements(requirements_txt, find_links: nil, index_url: nil, extra_index_urls: nil, # rubocop:disable Metrics/ParameterLists
                                  pre: false, extra_options: '')
  requirements_txt = File.expand_path(requirements_txt)
  raise Sdk::Error, "Requirements file #{requirements_txt} does not exist" unless File.exist?(requirements_txt)

  extra_args = format_pip_install_args(find_links:, index_url:, extra_index_urls:, pre:, extra_options:)

  archive_path = ObjectStorage.compute_archive_base_path(requirements_txt)
  @context_list << Context.new(source_path: requirements_txt, archive_path:)
  @dockerfile += "COPY #{archive_path} /.requirements.txt\n"
  @dockerfile += "RUN python -m pip install -r /.requirements.txt#{extra_args}\n"

  self
end

#run_commands(*commands) ⇒ Image

Runs commands in the image

Examples:

image = Image.debian_slim("3.12").run_commands('echo "Hello, world!"', 'echo "Hello again!"')

Parameters:

  • commands (Array<String>)

    The commands to run

Returns:

  • (Image)

    The image with the commands added



170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/daytona/common/image.rb', line 170

def run_commands(*commands)
  commands.each do |command|
    if command.is_a?(Array)
      escaped = command.map { |c| c.gsub('"', '\\"').gsub("'", "\\'") }
      @dockerfile += "RUN #{escaped.map { |c| "\"#{c}\"" }.join(' ')}\n"
    else
      @dockerfile += "RUN #{command}\n"
    end
  end

  self
end

#workdir(path) ⇒ Image

Sets the working directory in the image

Examples:

image = Image.debian_slim("3.12").workdir("/home/daytona")

Parameters:

  • path (String)

    The path to the working directory

Returns:

  • (Image)

    The image with the working directory added



208
209
210
211
# File 'lib/daytona/common/image.rb', line 208

def workdir(path)
  @dockerfile += "WORKDIR #{Shellwords.escape(path.to_s)}\n"
  self
end