Module: Docker::API::Tar

Defined in:
lib/docker/api/tar.rb

Overview

Packs a build context into the tar archive the daemon expects.

RubyGems ships Gem::Package::TarWriter with every Ruby, so building a context needs no dependency and no shelling out to tar -- which also means the behaviour is identical on Windows, where there may be no tar to shell out to.

Class Method Summary collapse

Class Method Details

.add_entry(tar, absolute, relative) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.



135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/docker/api/tar.rb', line 135

def add_entry(tar, absolute, relative)
  stat = File.lstat(absolute)

  if stat.directory?
    tar.mkdir(relative, stat.mode)
  elsif stat.symlink?
    tar.add_symlink(relative, File.readlink(absolute), stat.mode)
  elsif stat.file?
    tar.add_file_simple(relative, stat.mode, stat.size) do |entry|
      File.open(absolute, "rb") { |file| IO.copy_stream(file, entry) }
    end
  end
end

.each_entry(root, patterns) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.



124
125
126
127
128
129
130
131
# File 'lib/docker/api/tar.rb', line 124

def each_entry(root, patterns)
  Dir.glob("**/*", File::FNM_DOTMATCH, base: root).sort.each do |relative|
    next if [".", ".."].include?(File.basename(relative))
    next if ignored?(relative, patterns)

    yield File.join(root, relative), relative
  end
end

.ignored?(relative, patterns) ⇒ Boolean

Docker's ignore rules allow a later "!" pattern to re-include something an earlier pattern excluded, so the last matching rule wins rather than the first.

Parameters:

  • relative (String)

    a context-relative path

  • patterns (Array<String>)

Returns:

  • (Boolean)

    whether the path should be left out



98
99
100
101
102
103
104
105
106
107
108
# File 'lib/docker/api/tar.rb', line 98

def ignored?(relative, patterns)
  decision = false

  patterns.each do |pattern|
    negated = pattern.start_with?("!")
    candidate = negated ? pattern[1..] : pattern
    decision = !negated if matches?(relative, candidate)
  end

  decision
end

.matches?(relative, pattern) ⇒ Boolean

Parameters:

  • relative (String)
  • pattern (String)

Returns:

  • (Boolean)


113
114
115
116
117
118
119
120
# File 'lib/docker/api/tar.rb', line 113

def matches?(relative, pattern)
  pattern = pattern.chomp("/")
  return true if File.fnmatch?(pattern, relative, File::FNM_PATHNAME)

  # A directory pattern excludes everything beneath it.
  File.fnmatch?("#{pattern}/**", relative, File::FNM_PATHNAME) ||
    relative.start_with?("#{pattern}/")
end

.pack_directory(directory, ignore: nil) ⇒ File

Pack a directory into an uncompressed tar archive.

Written to a temporary file rather than a String. A build context is whatever the caller points at -- a Rails application with its assets, a monorepo subtree, a directory holding a model checkpoint -- and holding the whole archive in memory to send it costs its full size again on top of what the daemon is about to receive. The connection layer streams an IO body chunked, so the archive never has to be resident at all.

Examples:

Tar.pack_directory("./app")

Parameters:

  • directory (String)

    the build context root

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

    patterns to exclude. Read from the context's .dockerignore when not given.

Returns:

  • (File)

    the archive, rewound and ready to send

Raises:

  • (ArgumentError)


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/docker/api/tar.rb', line 36

def pack_directory(directory, ignore: nil)
  root = File.expand_path(directory)
  raise ArgumentError, "build context #{directory} is not a directory" unless File.directory?(root)

  patterns = ignore || read_dockerignore(root)
  buffer = Tempfile.new(["docker-api-ng-context", ".tar"])
  buffer.binmode

  begin
    Gem::Package::TarWriter.new(buffer) do |tar|
      each_entry(root, patterns) do |absolute, relative|
        add_entry(tar, absolute, relative)
      end
    end
  rescue StandardError
    buffer.close!
    raise
  end

  buffer.rewind
  buffer
end

.pack_dockerfile(contents, files: {}) ⇒ StringIO

Pack a single in-memory Dockerfile into a context of its own.

Useful for the common case of a short generated Dockerfile with no accompanying files, where writing a temporary directory first would be ceremony for its own sake.

Parameters:

  • contents (String)

    the Dockerfile

  • files (Hash{String => String}) (defaults to: {})

    additional files, path to content

Returns:

  • (StringIO)

    the archive, rewound and ready to send



68
69
70
71
72
73
74
75
76
77
78
# File 'lib/docker/api/tar.rb', line 68

def pack_dockerfile(contents, files: {})
  buffer = StringIO.new(+"".b)

  Gem::Package::TarWriter.new(buffer) do |tar|
    write_file(tar, "Dockerfile", contents, 0o644)
    files.each { |path, body| write_file(tar, path, body, 0o644) }
  end

  buffer.rewind
  buffer
end

.read_dockerignore(root) ⇒ Array<String>

Returns patterns from .dockerignore, or an empty list.

Parameters:

  • root (String)

Returns:

  • (Array<String>)

    patterns from .dockerignore, or an empty list



82
83
84
85
86
87
88
89
# File 'lib/docker/api/tar.rb', line 82

def read_dockerignore(root)
  path = File.join(root, ".dockerignore")
  return [] unless File.readable?(path)

  File.readlines(path, chomp: true)
    .map(&:strip)
    .reject { |line| line.empty? || line.start_with?("#") }
end

.write_file(tar, path, contents, mode) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.



151
152
153
154
# File 'lib/docker/api/tar.rb', line 151

def write_file(tar, path, contents, mode)
  bytes = contents.to_s.b
  tar.add_file_simple(path, mode, bytes.bytesize) { |entry| entry.write(bytes) }
end