dry-dock 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +19 -0
- data/README.md +30 -20
- data/VERSION +1 -1
- data/dry-dock.gemspec +15 -3
- data/lib/drydock.rb +4 -0
- data/lib/drydock/docker_api_patch.rb +6 -6
- data/lib/drydock/drydock.rb +19 -7
- data/lib/drydock/instructions/base.rb +13 -0
- data/lib/drydock/instructions/copy.rb +117 -0
- data/lib/drydock/object_caches/base.rb +4 -4
- data/lib/drydock/phase.rb +3 -1
- data/lib/drydock/phase_chain.rb +3 -3
- data/lib/drydock/plugins/package_manager.rb +5 -5
- data/lib/drydock/project.rb +45 -81
- data/lib/drydock/tar_writer.rb +1 -1
- metadata +47 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2627cbab1b9589543ec9a8997d95eb6071ecd17a
|
4
|
+
data.tar.gz: 6fe87f56623532901d4fb285a8cc07a584007cb0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f4360c64a1b7d07b36ef1e6e79fc6fb65d73b14ecea226be78bc75819f0fe77b7fbe4f24b0572bfcb4d84900167f442f1709d332d6d5f71cee5b6d66660e5495
|
7
|
+
data.tar.gz: b28fa39b7acbe2e4044b7f75748d314b198f9f90b9da2098bb96552c5495da6548a9daf53080cc672984368464ac343d1124892443587cb5c879bcf975a7b16d
|
data/.rubocop.yml
ADDED
data/Gemfile
CHANGED
@@ -3,6 +3,7 @@ source 'https://rubygems.org'
|
|
3
3
|
|
4
4
|
group :development do
|
5
5
|
gem 'rake', '~> 10.0'
|
6
|
+
gem 'rubocop', '~> 0.34'
|
6
7
|
gem 'jeweler', '~> 2.0'
|
7
8
|
gem 'pry', '~> 0.10'
|
8
9
|
|
@@ -17,5 +18,7 @@ group :test do
|
|
17
18
|
gem 'fakefs', require: false
|
18
19
|
end
|
19
20
|
|
21
|
+
gem 'attr_extras', '~> 4.4'
|
20
22
|
gem 'docker-api', '~> 1.22', require: 'docker'
|
21
23
|
gem 'excon', '~> 0.45'
|
24
|
+
gem 'memoist', '~> 0.12'
|
data/Gemfile.lock
CHANGED
@@ -2,6 +2,10 @@ GEM
|
|
2
2
|
remote: https://rubygems.org/
|
3
3
|
specs:
|
4
4
|
addressable (2.3.8)
|
5
|
+
ast (2.1.0)
|
6
|
+
astrolabe (1.3.1)
|
7
|
+
parser (~> 2.2)
|
8
|
+
attr_extras (4.4.0)
|
5
9
|
builder (3.2.2)
|
6
10
|
codeclimate-test-reporter (0.4.8)
|
7
11
|
simplecov (>= 0.7.1, < 1.0.0)
|
@@ -39,6 +43,7 @@ GEM
|
|
39
43
|
rdoc
|
40
44
|
json (1.8.3)
|
41
45
|
jwt (1.5.1)
|
46
|
+
memoist (0.12.0)
|
42
47
|
method_source (0.8.2)
|
43
48
|
mini_portile (0.6.2)
|
44
49
|
multi_json (1.11.2)
|
@@ -52,11 +57,15 @@ GEM
|
|
52
57
|
multi_json (~> 1.3)
|
53
58
|
multi_xml (~> 0.5)
|
54
59
|
rack (~> 1.2)
|
60
|
+
parser (2.2.2.6)
|
61
|
+
ast (>= 1.1, < 3.0)
|
62
|
+
powerpack (0.1.1)
|
55
63
|
pry (0.10.1)
|
56
64
|
coderay (~> 1.1.0)
|
57
65
|
method_source (~> 0.8.1)
|
58
66
|
slop (~> 3.4)
|
59
67
|
rack (1.6.4)
|
68
|
+
rainbow (2.0.0)
|
60
69
|
rake (10.4.2)
|
61
70
|
rdoc (4.2.0)
|
62
71
|
json (~> 1.4)
|
@@ -75,6 +84,13 @@ GEM
|
|
75
84
|
diff-lcs (>= 1.2.0, < 2.0)
|
76
85
|
rspec-support (~> 3.3.0)
|
77
86
|
rspec-support (3.3.0)
|
87
|
+
rubocop (0.34.2)
|
88
|
+
astrolabe (~> 1.3)
|
89
|
+
parser (>= 2.2.2.5, < 3.0)
|
90
|
+
powerpack (~> 0.1)
|
91
|
+
rainbow (>= 1.99.1, < 3.0)
|
92
|
+
ruby-progressbar (~> 1.4)
|
93
|
+
ruby-progressbar (1.7.5)
|
78
94
|
simplecov (0.10.0)
|
79
95
|
docile (~> 1.1.0)
|
80
96
|
json (~> 1.8)
|
@@ -89,15 +105,18 @@ PLATFORMS
|
|
89
105
|
ruby
|
90
106
|
|
91
107
|
DEPENDENCIES
|
108
|
+
attr_extras (~> 4.4)
|
92
109
|
codeclimate-test-reporter
|
93
110
|
docker-api (~> 1.22)
|
94
111
|
excon (~> 0.45)
|
95
112
|
fakefs
|
96
113
|
jeweler (~> 2.0)
|
114
|
+
memoist (~> 0.12)
|
97
115
|
pry (~> 0.10)
|
98
116
|
rake (~> 10.0)
|
99
117
|
rspec (~> 3.0)
|
100
118
|
rspec-collection_matchers
|
119
|
+
rubocop (~> 0.34)
|
101
120
|
simplecov (~> 0.9)
|
102
121
|
simplecov-rcov (~> 0.2)
|
103
122
|
|
data/README.md
CHANGED
@@ -3,11 +3,13 @@
|
|
3
3
|
WORK IN PROGRESS — ALPHA-RELEASE SOFTWARE
|
4
4
|
SOME FEATURES REQUIRE DOCKER 1.8.0 OR NEWER
|
5
5
|
|
6
|
-
![Automated Build Status](https://travis-ci.org/ripta/drydock.svg)
|
6
|
+
[![Automated Build Status](https://travis-ci.org/ripta/drydock.svg)](https://travis-ci.org/ripta/drydock)
|
7
|
+
[![Code Climate](https://codeclimate.com/github/ripta/drydock/badges/gpa.svg)](https://codeclimate.com/github/ripta/drydock)
|
7
8
|
|
8
9
|
A ruby DSL to build your own docker images. Images are built based on instructions
|
9
10
|
contained in your project's `Drydockfile`.
|
10
11
|
|
12
|
+
|
11
13
|
## Why not Dockerfile?
|
12
14
|
|
13
15
|
[Dockerfiles](https://docs.docker.com/reference/builder/) are great to start out
|
@@ -27,6 +29,7 @@ it would be nice to import a configurably-older image, import the new Gemfile,
|
|
27
29
|
and re-run the build. On the other hand, it would be important to be able to limit
|
28
30
|
the age of the cache.
|
29
31
|
|
32
|
+
|
30
33
|
## Why Drydock?
|
31
34
|
|
32
35
|
Drydock interfaces directly with the [Docker Remote API](https://docs.docker.com/reference/api/docker_remote_api/)
|
@@ -39,20 +42,24 @@ in your project being built by Drydock.
|
|
39
42
|
|
40
43
|
Drydockfiles are written in ruby.
|
41
44
|
|
42
|
-
|
45
|
+
|
46
|
+
## Installation
|
43
47
|
|
44
48
|
Either (a) `gem install dry-dock`, or (b) add "dry-dock" to your project's Gemfile,
|
45
|
-
and run `bundle`.
|
46
|
-
|
47
|
-
|
49
|
+
and run `bundle`.
|
50
|
+
|
51
|
+
Sorry, but the gem name `drydock` was already taken by a defunct gem, and I'm too
|
52
|
+
lazy to contact them; the binary and name of the project, however, are both `drydock`.
|
48
53
|
|
49
54
|
In your project's root directory, you'll want to create a `Drydockfile` containing
|
50
|
-
drydock functions. When you're ready, build an image using:
|
55
|
+
drydock functions. When you're ready, from your project's directory, build an image using:
|
51
56
|
|
52
57
|
```
|
53
|
-
$ drydock
|
58
|
+
$ bundle exec drydock
|
54
59
|
```
|
55
60
|
|
61
|
+
or `drydock` directly if you're not using bundler.
|
62
|
+
|
56
63
|
Alternatively, point drydock to a directory containing the `Drydockfile`, or to any
|
57
64
|
file to treat it as the `Drydockfile`, e.g.:
|
58
65
|
|
@@ -61,22 +68,14 @@ $ drydock ~/source/miniproject # project directory expects a file named Drydockf
|
|
61
68
|
$ drydock ~/source/miniproject/drydock-definition.rb # expects a drydock-definition.rb
|
62
69
|
```
|
63
70
|
|
64
|
-
Example `Drydockfile`s may be seen in `examples
|
65
|
-
|
66
|
-
## Development Installation
|
71
|
+
**Example `Drydockfile`s may be seen in the `examples/` directory of the source repo.**
|
67
72
|
|
68
|
-
This is needed if you plan on hacking drydock:
|
69
|
-
|
70
|
-
```
|
71
|
-
$ git clone git@github.com:ripta/drydock.git
|
72
|
-
$ bundle
|
73
|
-
```
|
74
73
|
|
75
74
|
## Drydockfile Syntax
|
76
75
|
|
77
76
|
As previously mentioned, Drydockfiles are ruby. The contents of Drydockfile are
|
78
|
-
evaluated in the context of an instance of
|
79
|
-
the documentation for it for more in-depth information.
|
77
|
+
evaluated in the context of an instance of {Drydock::Project}; you can refer to
|
78
|
+
the documentation for it for more in-depth information on each instruction.
|
80
79
|
|
81
80
|
Because Drydockfiles are ruby, most constructs should work as-is: you can declare
|
82
81
|
constants and refer to them later; call `Kernel#abort` to exit the program and
|
@@ -93,7 +92,18 @@ methods of the {Drydock::Project} class or head to the
|
|
93
92
|
[automatically-generated ruby docs](http://www.rubydoc.info/gems/dry-dock).
|
94
93
|
|
95
94
|
|
95
|
+
## Contributing
|
96
|
+
|
97
|
+
If you plan on hacking or contributing to drydock, fork the project, create a new
|
98
|
+
branch, make your changes, commit, and open a pull request.
|
99
|
+
|
100
|
+
After cloning your repo, `bundle` should take care of it.
|
101
|
+
|
102
|
+
|
96
103
|
## Roadmap
|
97
104
|
|
98
|
-
1. Customizable caching subsystem.
|
99
|
-
|
105
|
+
1. Customizable caching subsystem with pluggable caching strategies.
|
106
|
+
2. Squashing layers together, with cache support.
|
107
|
+
3. Unarchiving a file directly into a container.
|
108
|
+
4. Proper `ONBUILD` implementation and expanded support for hooks.
|
109
|
+
5. Drydock instructions corresponding to `LABEL`, `VOLUME`, `USER`, and `WORKDIR` Docker instructions.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.6
|
data/dry-dock.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: dry-dock 0.1.
|
5
|
+
# stub: dry-dock 0.1.6 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "dry-dock"
|
9
|
-
s.version = "0.1.
|
9
|
+
s.version = "0.1.6"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
13
13
|
s.authors = ["Ripta Pasay"]
|
14
|
-
s.date = "2015-10-
|
14
|
+
s.date = "2015-10-08"
|
15
15
|
s.description = "A Dockerfile-replacement DSL for building complex images"
|
16
16
|
s.email = "github@r8y.org"
|
17
17
|
s.executables = ["drydock", "json-test-consumer.rb", "json-test-producer.rb", "test-tar-writer-digest.rb"]
|
@@ -24,6 +24,7 @@ Gem::Specification.new do |s|
|
|
24
24
|
".dockerignore",
|
25
25
|
".pryrc",
|
26
26
|
".rspec",
|
27
|
+
".rubocop.yml",
|
27
28
|
".travis.yml",
|
28
29
|
".yardopts",
|
29
30
|
"Dockerfile",
|
@@ -52,6 +53,8 @@ Gem::Specification.new do |s|
|
|
52
53
|
"lib/drydock/formatters.rb",
|
53
54
|
"lib/drydock/ignorefile_definition.rb",
|
54
55
|
"lib/drydock/image_repository.rb",
|
56
|
+
"lib/drydock/instructions/base.rb",
|
57
|
+
"lib/drydock/instructions/copy.rb",
|
55
58
|
"lib/drydock/logger.rb",
|
56
59
|
"lib/drydock/object_caches/base.rb",
|
57
60
|
"lib/drydock/object_caches/filesystem_cache.rb",
|
@@ -109,26 +112,35 @@ Gem::Specification.new do |s|
|
|
109
112
|
s.specification_version = 4
|
110
113
|
|
111
114
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
115
|
+
s.add_runtime_dependency(%q<attr_extras>, ["~> 4.4"])
|
112
116
|
s.add_runtime_dependency(%q<docker-api>, ["~> 1.22"])
|
113
117
|
s.add_runtime_dependency(%q<excon>, ["~> 0.45"])
|
118
|
+
s.add_runtime_dependency(%q<memoist>, ["~> 0.12"])
|
114
119
|
s.add_development_dependency(%q<rake>, ["~> 10.0"])
|
120
|
+
s.add_development_dependency(%q<rubocop>, ["~> 0.34"])
|
115
121
|
s.add_development_dependency(%q<jeweler>, ["~> 2.0"])
|
116
122
|
s.add_development_dependency(%q<pry>, ["~> 0.10"])
|
117
123
|
s.add_development_dependency(%q<simplecov>, ["~> 0.9"])
|
118
124
|
s.add_development_dependency(%q<simplecov-rcov>, ["~> 0.2"])
|
119
125
|
else
|
126
|
+
s.add_dependency(%q<attr_extras>, ["~> 4.4"])
|
120
127
|
s.add_dependency(%q<docker-api>, ["~> 1.22"])
|
121
128
|
s.add_dependency(%q<excon>, ["~> 0.45"])
|
129
|
+
s.add_dependency(%q<memoist>, ["~> 0.12"])
|
122
130
|
s.add_dependency(%q<rake>, ["~> 10.0"])
|
131
|
+
s.add_dependency(%q<rubocop>, ["~> 0.34"])
|
123
132
|
s.add_dependency(%q<jeweler>, ["~> 2.0"])
|
124
133
|
s.add_dependency(%q<pry>, ["~> 0.10"])
|
125
134
|
s.add_dependency(%q<simplecov>, ["~> 0.9"])
|
126
135
|
s.add_dependency(%q<simplecov-rcov>, ["~> 0.2"])
|
127
136
|
end
|
128
137
|
else
|
138
|
+
s.add_dependency(%q<attr_extras>, ["~> 4.4"])
|
129
139
|
s.add_dependency(%q<docker-api>, ["~> 1.22"])
|
130
140
|
s.add_dependency(%q<excon>, ["~> 0.45"])
|
141
|
+
s.add_dependency(%q<memoist>, ["~> 0.12"])
|
131
142
|
s.add_dependency(%q<rake>, ["~> 10.0"])
|
143
|
+
s.add_dependency(%q<rubocop>, ["~> 0.34"])
|
132
144
|
s.add_dependency(%q<jeweler>, ["~> 2.0"])
|
133
145
|
s.add_dependency(%q<pry>, ["~> 0.10"])
|
134
146
|
s.add_dependency(%q<simplecov>, ["~> 0.9"])
|
data/lib/drydock.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
|
2
|
+
require 'attr_extras/explicit'
|
2
3
|
require 'docker'
|
3
4
|
require 'excon'
|
4
5
|
require 'fileutils'
|
6
|
+
require 'memoist'
|
5
7
|
|
6
8
|
require_relative 'drydock/docker_api_patch'
|
7
9
|
|
@@ -26,6 +28,8 @@ require_relative 'drydock/project'
|
|
26
28
|
require_relative 'drydock/stream_monitor'
|
27
29
|
require_relative 'drydock/tar_writer'
|
28
30
|
|
31
|
+
require_relative 'drydock/instructions/copy'
|
32
|
+
|
29
33
|
require_relative 'drydock/object_caches/filesystem_cache'
|
30
34
|
require_relative 'drydock/object_caches/in_memory_cache'
|
31
35
|
require_relative 'drydock/object_caches/no_cache'
|
@@ -8,17 +8,17 @@ module Docker
|
|
8
8
|
log_request(request)
|
9
9
|
resource.request(request)
|
10
10
|
rescue Excon::Errors::BadRequest => ex
|
11
|
-
|
11
|
+
fail ClientError, ex.response.body
|
12
12
|
rescue Excon::Errors::Unauthorized => ex
|
13
|
-
|
13
|
+
fail UnauthorizedError, ex.response.body
|
14
14
|
rescue Excon::Errors::NotFound => ex
|
15
|
-
|
15
|
+
fail NotFoundError, ex.response.body
|
16
16
|
rescue Excon::Errors::Conflict => ex
|
17
|
-
|
17
|
+
fail ConflictError, ex.response.body
|
18
18
|
rescue Excon::Errors::InternalServerError => ex
|
19
|
-
|
19
|
+
fail ServerError, ex.response.body
|
20
20
|
rescue Excon::Errors::Timeout => ex
|
21
|
-
|
21
|
+
fail TimeoutError, ex.message
|
22
22
|
end
|
23
23
|
|
24
24
|
end
|
data/lib/drydock/drydock.rb
CHANGED
@@ -1,13 +1,29 @@
|
|
1
1
|
|
2
|
+
# Drydock is a command line program that provides a DSL for you to create your
|
3
|
+
# own build pipeline for your docker images. See {file:README.md} for more
|
4
|
+
# information and background on the design.
|
2
5
|
module Drydock
|
3
6
|
|
7
|
+
# The application's banner.
|
8
|
+
#
|
9
|
+
# @return [String] the banner
|
4
10
|
def self.banner
|
5
11
|
"Drydock v#{Drydock.version}"
|
6
12
|
end
|
7
13
|
|
8
|
-
|
9
|
-
|
10
|
-
|
14
|
+
# Create a new project, then run and finalize the build.
|
15
|
+
#
|
16
|
+
# @param (see Project#initialize)
|
17
|
+
# @option (see Project#initialize)
|
18
|
+
# @yield [project] A block that describes the logic on how to search for a
|
19
|
+
# Drydockfile.
|
20
|
+
# @yieldparam project [Project] A newly-instantiated project object.
|
21
|
+
# @yieldreturn [Array<String>] An array of exactly two elements: the contents
|
22
|
+
# of the Drydockfile, and the path to the Drydockfile. The directory of
|
23
|
+
# the path will be made as the working directory.
|
24
|
+
def self.build(build_opts = {}, &blk)
|
25
|
+
Project.new(build_opts).tap do |project|
|
26
|
+
dryfile, dryfilename = yield project
|
11
27
|
|
12
28
|
Dir.chdir(File.dirname(dryfilename))
|
13
29
|
Drydock.logger.info("Working directory set to #{Dir.pwd}")
|
@@ -47,10 +63,6 @@ module Drydock
|
|
47
63
|
@logger = logger
|
48
64
|
end
|
49
65
|
|
50
|
-
def self.using(project)
|
51
|
-
raise NotImplementedError, "TODO(rpasay)"
|
52
|
-
end
|
53
|
-
|
54
66
|
def self.version
|
55
67
|
version_file = File.join(File.dirname(__FILE__), '..', '..', 'VERSION')
|
56
68
|
File.exist?(version_file) ? File.read(version_file).chomp : ""
|
@@ -0,0 +1,117 @@
|
|
1
|
+
|
2
|
+
require_relative 'base'
|
3
|
+
|
4
|
+
module Drydock
|
5
|
+
module Instructions
|
6
|
+
# The concrete implementation of the COPY instruction.
|
7
|
+
# **Do not use this class directly.**
|
8
|
+
#
|
9
|
+
# @see Project#copy
|
10
|
+
class Copy < Base
|
11
|
+
|
12
|
+
attr_accessor :chmod, :ignorefile, :no_cache, :recursive
|
13
|
+
|
14
|
+
attr_reader :chain, :source_path, :target_path
|
15
|
+
attr_initialize :chain, :source_path, :target_path do
|
16
|
+
@chmod = false
|
17
|
+
@ignorefile = '.dockerignore'
|
18
|
+
@no_cache = false
|
19
|
+
@recursive = true
|
20
|
+
end
|
21
|
+
|
22
|
+
# @raise [InvalidInstructionError] when the `source_path` does not exist
|
23
|
+
# @raise [InvalidInstructionError] when the `source_path` is an empty directory
|
24
|
+
# with nothing to copy
|
25
|
+
# @raise [InvalidInstructionError] when the `target_path` does not exist in the
|
26
|
+
# container
|
27
|
+
# @raise [InvalidInstructionError] when the `target_path` exists in the container,
|
28
|
+
# but is not actually a directory
|
29
|
+
def run!
|
30
|
+
if source_path.start_with?('/')
|
31
|
+
Drydock.logger.warn("#{source_path.inspect} is an absolute path; we recommend relative paths")
|
32
|
+
end
|
33
|
+
|
34
|
+
fail InvalidInstructionError, "#{source_path} does not exist" unless File.exist?(source_path)
|
35
|
+
|
36
|
+
buffer = build_tar_from_source!
|
37
|
+
digest = calculate_digest(buffer)
|
38
|
+
write_to_container(buffer, digest)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def build_tar_from_source!
|
44
|
+
buffer = StringIO.new
|
45
|
+
log_info("Processing #{source_files.size} files in tree")
|
46
|
+
|
47
|
+
TarWriter.new(buffer) do |tar|
|
48
|
+
source_files.each do |source_file|
|
49
|
+
File.open(source_file, 'r') do |input|
|
50
|
+
stat = input.stat
|
51
|
+
mode = chmod || stat.mode
|
52
|
+
tar.add_entry(source_file, mode: mode, mtime: stat.mtime) do |tar_file|
|
53
|
+
tar_file.write(input.read)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
buffer.rewind
|
60
|
+
buffer
|
61
|
+
end
|
62
|
+
|
63
|
+
def calculate_digest(buffer)
|
64
|
+
Digest::MD5.hexdigest(buffer.read).tap do |digest|
|
65
|
+
log_info("Tree digest is md5:#{digest}")
|
66
|
+
buffer.rewind
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def log_info(msg, indent: 0)
|
71
|
+
Drydock.logger.info(indent: indent, message: msg)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Retrieve all files inside {#source_path} not matching the {#ignorefile} rules.
|
75
|
+
def source_files
|
76
|
+
files =
|
77
|
+
if File.directory?(source_path)
|
78
|
+
FileManager.find(source_path, ignorefile, prepend_path: true, recursive: recursive).sort
|
79
|
+
else
|
80
|
+
[source_path]
|
81
|
+
end
|
82
|
+
|
83
|
+
fail InvalidInstructionError, "#{source_path} is empty or does not match a path" if files.empty?
|
84
|
+
|
85
|
+
files
|
86
|
+
end
|
87
|
+
memoize :source_files
|
88
|
+
|
89
|
+
# Create a new container on the `chain`, and then write the contents of
|
90
|
+
# `buffer`, whose digest is `digest`.
|
91
|
+
def write_to_container(buffer, digest)
|
92
|
+
label = "# COPY #{recursive ? 'dir' : 'file'}:md5:#{digest} TO #{target_path}"
|
93
|
+
|
94
|
+
chain.run(label, no_cache: no_cache) do |container|
|
95
|
+
target_stat = container.archive_head(target_path)
|
96
|
+
|
97
|
+
# TODO(rpasay): cannot autocreate the target, because `container` here is already dead
|
98
|
+
unless target_stat
|
99
|
+
fail InvalidInstructionError, "Target path #{target_path.inspect} does not exist"
|
100
|
+
end
|
101
|
+
|
102
|
+
unless target_stat.directory?
|
103
|
+
Drydock.logger.debug(target_stat)
|
104
|
+
fail InvalidInstructionError,
|
105
|
+
"Target path #{target_path.inspect} exists, " +
|
106
|
+
"but is not a directory in the container"
|
107
|
+
end
|
108
|
+
|
109
|
+
container.archive_put(target_path) do |output|
|
110
|
+
output.write(buffer.read)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -4,19 +4,19 @@ module Drydock
|
|
4
4
|
class Base
|
5
5
|
|
6
6
|
def clear
|
7
|
-
|
7
|
+
fail NotImplementedError, '#clear must be overridden in the subclass'
|
8
8
|
end
|
9
9
|
|
10
10
|
def fetch(key, &blk)
|
11
|
-
|
11
|
+
fail NotImplementedError, '#fetch must be overridden in the subclass'
|
12
12
|
end
|
13
13
|
|
14
14
|
def get(key, &blk)
|
15
|
-
|
15
|
+
fail NotImplementedError, '#get must be overridden in the subclass'
|
16
16
|
end
|
17
17
|
|
18
18
|
def set(key, value = nil, &blk)
|
19
|
-
|
19
|
+
fail NotImplementedError, '#set must be overridden in the subclass'
|
20
20
|
end
|
21
21
|
|
22
22
|
end
|
data/lib/drydock/phase.rb
CHANGED
@@ -14,7 +14,9 @@ module Drydock
|
|
14
14
|
def self.from(hsh)
|
15
15
|
h = hsh.to_h
|
16
16
|
extra_keys = h.keys - members
|
17
|
-
|
17
|
+
|
18
|
+
fail ArgumentError, "unknown options: #{extra_keys.join(', ')}" unless extra_keys.empty?
|
19
|
+
|
18
20
|
new(*h.values_at(*members))
|
19
21
|
end
|
20
22
|
|
data/lib/drydock/phase_chain.rb
CHANGED
@@ -106,15 +106,15 @@ module Drydock
|
|
106
106
|
results = c.wait(timeout)
|
107
107
|
|
108
108
|
unless results
|
109
|
-
|
109
|
+
fail InvalidCommandExecutionError, {container: c.id, message: "Container did not return anything (API BUG?)"}
|
110
110
|
end
|
111
111
|
|
112
112
|
unless results.key?('StatusCode')
|
113
|
-
|
113
|
+
fail InvalidCommandExecutionError, {container: c.id, message: "Container did not return a status code (API BUG?)"}
|
114
114
|
end
|
115
115
|
|
116
116
|
unless results['StatusCode'] == 0
|
117
|
-
|
117
|
+
fail InvalidCommandExecutionError, {container: c.id, message: "Container exited with code #{results['StatusCode']}"}
|
118
118
|
end
|
119
119
|
rescue
|
120
120
|
# on error, kill the streaming logs and reraise the exception
|
@@ -6,23 +6,23 @@ module Drydock
|
|
6
6
|
class PackageManager < Base
|
7
7
|
|
8
8
|
def add(*pkgs)
|
9
|
-
|
9
|
+
fail NotImplementedError, '#add must be overridde in the subclass'
|
10
10
|
end
|
11
11
|
|
12
12
|
def clean
|
13
|
-
|
13
|
+
fail NotImplementedError, '#clean must be overridde in the subclass'
|
14
14
|
end
|
15
15
|
|
16
16
|
def remove(*pkgs)
|
17
|
-
|
17
|
+
fail NotImplementedError, '#remove must be overridde in the subclass'
|
18
18
|
end
|
19
19
|
|
20
20
|
def update
|
21
|
-
|
21
|
+
fail NotImplementedError, '#update must be overridde in the subclass'
|
22
22
|
end
|
23
23
|
|
24
24
|
def upgrade
|
25
|
-
|
25
|
+
fail NotImplementedError, '#upgrade must be overridde in the subclass'
|
26
26
|
end
|
27
27
|
|
28
28
|
end
|
data/lib/drydock/project.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
|
2
2
|
module Drydock
|
3
|
+
# A project defines the methods available in a `Drydockfile`. When run using
|
4
|
+
# the binary `drydock`, this object will be instantiated automatically for you.
|
5
|
+
#
|
6
|
+
# The contents of a `Drydockfile` is automatically evaluated in the context
|
7
|
+
# of a project, so you don't need to instantiate the object manually.
|
3
8
|
class Project
|
4
9
|
|
5
10
|
DEFAULT_OPTIONS = {
|
@@ -17,11 +22,11 @@ module Drydock
|
|
17
22
|
# @option build_opts [Boolean] :auto_remove Whether intermediate images
|
18
23
|
# created during the build of this project should be automatically removed.
|
19
24
|
# @option build_opts [String] :author The default author field when an
|
20
|
-
# author is not provided explicitly with {#author}.
|
25
|
+
# author is not provided explicitly with {Project#author}.
|
21
26
|
# @option build_opts [ObjectCaches::Base] :cache An object cache manager.
|
22
27
|
# @option build_opts [#call] :event_handler A handler that responds to a
|
23
28
|
# `#call` message with four arguments: `[event, is_new, serial_no, event_type]`
|
24
|
-
# most useful to override logging
|
29
|
+
# most useful to override logging.
|
25
30
|
# @option build_opts [PhaseChain] :chain A phase chain manager.
|
26
31
|
# @option build_opts [String] :ignorefile The name of the ignore-file to load.
|
27
32
|
def initialize(build_opts = {})
|
@@ -52,7 +57,7 @@ module Drydock
|
|
52
57
|
# @raise [InvalidInstructionArgumentError] when neither name nor email is provided
|
53
58
|
def author(name: nil, email: nil)
|
54
59
|
if (name.nil? || name.empty?) && (email.nil? || name.empty?)
|
55
|
-
|
60
|
+
fail InvalidInstructionArgumentError, 'at least one of `name:` or `email:` must be provided'
|
56
61
|
end
|
57
62
|
|
58
63
|
value = email ? "#{name} <#{email}>" : name.to_s
|
@@ -128,73 +133,24 @@ module Drydock
|
|
128
133
|
# the mode provided (in integer octal form) will be used to override *all*
|
129
134
|
# file and directory modes.
|
130
135
|
# @param [Boolean] no_cache When `false` (the default), the hash digest of the
|
131
|
-
# source path
|
136
|
+
# source path--taking into account all its files, directories, and contents--is
|
132
137
|
# used as the cache key. When `true`, the image is rebuilt *every* time.
|
133
138
|
# @param [Boolean] recursive When `true`, then `source_path` is expected to be
|
134
139
|
# a directory, at which point all its contents would be recursively searched.
|
135
140
|
# When `false`, then `source_path` is expected to be a file.
|
136
141
|
#
|
137
|
-
# @raise
|
138
|
-
# @raise [InvalidInstructionError] when the `source_path` is an empty directory
|
139
|
-
# with nothing to copy
|
140
|
-
# @raise [InvalidInstructionError] when the `target_path` does not exist in the
|
141
|
-
# container
|
142
|
-
# @raise [InvalidInstructionError] when the `target_path` exists in the container,
|
143
|
-
# but is not actually a directory
|
142
|
+
# @raise (see Instructions::Copy#run!)
|
144
143
|
def copy(source_path, target_path, chmod: false, no_cache: false, recursive: true)
|
145
144
|
requires_from!(:copy)
|
146
145
|
log_step('copy', source_path, target_path, chmod: (chmod ? sprintf('%o', chmod) : false))
|
147
146
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
147
|
+
Instructions::Copy.new(chain, source_path, target_path).tap do |ins|
|
148
|
+
ins.chmod = chmod if chmod
|
149
|
+
ins.ignorefile = ignorefile
|
150
|
+
ins.no_cache = no_cache
|
151
|
+
ins.recursive = recursive
|
153
152
|
|
154
|
-
|
155
|
-
FileManager.find(source_path, ignorefile, prepend_path: true, recursive: recursive)
|
156
|
-
else
|
157
|
-
[source_path]
|
158
|
-
end
|
159
|
-
source_files.sort!
|
160
|
-
|
161
|
-
raise InvalidInstructionError, "#{source_path} is empty or does not match a path" if source_files.empty?
|
162
|
-
|
163
|
-
buffer = StringIO.new
|
164
|
-
log_info("Processing #{source_files.size} files in tree")
|
165
|
-
TarWriter.new(buffer) do |tar|
|
166
|
-
source_files.each do |source_file|
|
167
|
-
File.open(source_file, 'r') do |input|
|
168
|
-
stat = input.stat
|
169
|
-
mode = chmod || stat.mode
|
170
|
-
tar.add_entry(source_file, mode: stat.mode, mtime: stat.mtime) do |tar_file|
|
171
|
-
tar_file.write(input.read)
|
172
|
-
end
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
buffer.rewind
|
178
|
-
digest = Digest::MD5.hexdigest(buffer.read)
|
179
|
-
|
180
|
-
log_info("Tree digest is md5:#{digest}")
|
181
|
-
chain.run("# COPY #{source_path} #{target_path} DIGEST #{digest}", no_cache: no_cache) do |container|
|
182
|
-
target_stat = container.archive_head(target_path)
|
183
|
-
|
184
|
-
# TODO(rpasay): cannot autocreate the target, because `container` here is already dead
|
185
|
-
unless target_stat
|
186
|
-
raise InvalidInstructionError, "Target path #{target_path.inspect} does not exist"
|
187
|
-
end
|
188
|
-
|
189
|
-
unless target_stat.directory?
|
190
|
-
Drydock.logger.debug(target_stat)
|
191
|
-
raise InvalidInstructionError, "Target path #{target_path.inspect} exists, but is not a directory in the container"
|
192
|
-
end
|
193
|
-
|
194
|
-
container.archive_put(target_path) do |output|
|
195
|
-
buffer.rewind
|
196
|
-
output.write(buffer.read)
|
197
|
-
end
|
153
|
+
ins.run!
|
198
154
|
end
|
199
155
|
|
200
156
|
self
|
@@ -225,7 +181,7 @@ module Drydock
|
|
225
181
|
|
226
182
|
unless cache.key?(source_url)
|
227
183
|
cache.set(source_url) do |obj|
|
228
|
-
chunked =
|
184
|
+
chunked = proc do |chunk, _remaining_bytes, _total_bytes|
|
229
185
|
obj.write(chunk)
|
230
186
|
end
|
231
187
|
Excon.get(source_url, response_block: chunked)
|
@@ -236,7 +192,8 @@ module Drydock
|
|
236
192
|
|
237
193
|
# TODO(rpasay): invalidate cache when the downloaded file changes,
|
238
194
|
# and then force rebuild
|
239
|
-
|
195
|
+
digest = Digest::MD5.hexdigest(source_url)
|
196
|
+
chain.run("# DOWNLOAD file:md5:#{digest} #{target_path}") do |container|
|
240
197
|
container.archive_put do |output|
|
241
198
|
TarWriter.new(output) do |tar|
|
242
199
|
cache.get(source_url) do |input|
|
@@ -252,7 +209,7 @@ module Drydock
|
|
252
209
|
end
|
253
210
|
|
254
211
|
# **This instruction is *optional*, but if specified, must appear at the
|
255
|
-
# beginning of the file.**
|
212
|
+
# beginning of the file.**
|
256
213
|
#
|
257
214
|
# This instruction is used to restrict the version of `drydock` required to
|
258
215
|
# run the `Drydockfile`. When not specified, any version of `drydock` is
|
@@ -266,20 +223,20 @@ module Drydock
|
|
266
223
|
# drydock '~> 0.5'
|
267
224
|
# @param [String] version The version specification to use.
|
268
225
|
def drydock(version = '>= 0')
|
269
|
-
|
226
|
+
fail InvalidInstructionError, '`drydock` must be called before `from`' if chain
|
270
227
|
log_step('drydock', version)
|
271
228
|
|
272
229
|
requirement = Gem::Requirement.create(version)
|
273
230
|
current = Gem::Version.create(Drydock.version)
|
274
231
|
|
275
232
|
unless requirement.satisfied_by?(current)
|
276
|
-
|
233
|
+
fail InsufficientVersionError, "build requires #{version.inspect}, but you're on #{Drydock.version.inspect}"
|
277
234
|
end
|
278
235
|
|
279
236
|
self
|
280
237
|
end
|
281
238
|
|
282
|
-
# Sets the entrypoint command for an image.
|
239
|
+
# Sets the entrypoint command for an image.
|
283
240
|
#
|
284
241
|
# {#entrypoint} corresponds to the `ENTRYPOINT` Dockerfile instruction. This
|
285
242
|
# instruction does **not** run the command, but rather provides the default
|
@@ -303,7 +260,7 @@ module Drydock
|
|
303
260
|
|
304
261
|
# Set an environment variable, which will be persisted in future images
|
305
262
|
# (unless it is specifically overwritten) and derived projects.
|
306
|
-
#
|
263
|
+
#
|
307
264
|
# Subsequent commands can refer to the environment variable by preceeding
|
308
265
|
# the variable with a `$` sign, e.g.:
|
309
266
|
#
|
@@ -315,7 +272,7 @@ module Drydock
|
|
315
272
|
#
|
316
273
|
# Multiple calls to this instruction will build on top of one another.
|
317
274
|
# That is, after the following two instructions:
|
318
|
-
#
|
275
|
+
#
|
319
276
|
# ```
|
320
277
|
# env 'APP_ROOT', '/app'
|
321
278
|
# env 'BUILD_ROOT', '/build'
|
@@ -419,13 +376,13 @@ module Drydock
|
|
419
376
|
# the project,** although non-instructions may appear before this.
|
420
377
|
#
|
421
378
|
# If the `drydock` instruction is provided, `from` should come after it.
|
422
|
-
#
|
379
|
+
#
|
423
380
|
# @param [#to_s] repo The name of the repository, which may be any valid docker
|
424
381
|
# repository name, and may optionally include the registry address, e.g.,
|
425
382
|
# `johndoe/thing` or `quay.io/jane/app`. The name *must not* contain the tag name.
|
426
383
|
# @param [#to_s] tag The tag to use.
|
427
384
|
def from(repo, tag = 'latest')
|
428
|
-
|
385
|
+
fail InvalidInstructionError, '`from` must only be called once per project' if chain
|
429
386
|
|
430
387
|
repo = repo.to_s
|
431
388
|
tag = tag.to_s
|
@@ -506,8 +463,10 @@ module Drydock
|
|
506
463
|
# when `Gemfile` changes but `package.json` does not, only the first
|
507
464
|
# derived project will be rebuilt (and following that, the third as well).
|
508
465
|
#
|
466
|
+
# @param (see #initialize)
|
467
|
+
# @option (see #initialize)
|
509
468
|
def derive(opts = {}, &blk)
|
510
|
-
clean_opts = build_opts.delete_if { |
|
469
|
+
clean_opts = build_opts.delete_if { |_, v| v.nil? }
|
511
470
|
derive_opts = clean_opts.merge(opts).merge(chain: chain)
|
512
471
|
|
513
472
|
Project.new(derive_opts).tap do |project|
|
@@ -538,8 +497,8 @@ module Drydock
|
|
538
497
|
mkdir(path)
|
539
498
|
|
540
499
|
requires_from!(:import)
|
541
|
-
|
542
|
-
|
500
|
+
fail InvalidInstructionError, 'cannot `import` from `/`' if path == '/' && !force
|
501
|
+
fail InvalidInstructionError, '`import` requires a `from:` option' if from.nil?
|
543
502
|
log_step('import', path, from: from.last_image.id)
|
544
503
|
|
545
504
|
total_size = 0
|
@@ -597,8 +556,12 @@ module Drydock
|
|
597
556
|
end
|
598
557
|
end
|
599
558
|
|
559
|
+
# **NOT SUPPORTED YET**
|
560
|
+
#
|
600
561
|
# @todo on_build instructions should be deferred to the end.
|
601
|
-
def on_build(instruction = nil, &
|
562
|
+
def on_build(instruction = nil, &_blk)
|
563
|
+
fail NotImplementedError, "on_build is not yet supported"
|
564
|
+
|
602
565
|
requires_from!(:on_build)
|
603
566
|
log_step('on_build', instruction)
|
604
567
|
chain.run("# ON_BUILD #{instruction}", on_build: instruction)
|
@@ -623,7 +586,7 @@ module Drydock
|
|
623
586
|
# Additional `opts` are also recognized:
|
624
587
|
#
|
625
588
|
# * `author`, a string, preferably in the format of "Name <email@domain.com>".
|
626
|
-
# If provided, this overrides
|
589
|
+
# If provided, this overrides the author name set with {#author}.
|
627
590
|
# * `comment`, an arbitrary string used as a comment for the resulting image
|
628
591
|
#
|
629
592
|
# If `run` results in a container being created and `&blk` is provided, the
|
@@ -645,9 +608,9 @@ module Drydock
|
|
645
608
|
# Set project options.
|
646
609
|
def set(key, value = nil, &blk)
|
647
610
|
key = key.to_sym
|
648
|
-
|
649
|
-
|
650
|
-
|
611
|
+
fail ArgumentError, "unknown option #{key.inspect}" unless build_opts.key?(key)
|
612
|
+
fail ArgumentError, "one of value or block is required" if value.nil? && blk.nil?
|
613
|
+
fail ArgumentError, "only one of value or block may be provided" if value && blk
|
651
614
|
|
652
615
|
build_opts[key] = value || blk
|
653
616
|
end
|
@@ -673,7 +636,7 @@ module Drydock
|
|
673
636
|
# apk.update
|
674
637
|
# end
|
675
638
|
# ```
|
676
|
-
#
|
639
|
+
#
|
677
640
|
# In cases of single commands, the above is the same as:
|
678
641
|
#
|
679
642
|
# ```
|
@@ -681,11 +644,12 @@ module Drydock
|
|
681
644
|
# ```
|
682
645
|
def with(plugin, &blk)
|
683
646
|
(@plugins[plugin] ||= plugin.new(self)).tap do |instance|
|
684
|
-
|
647
|
+
blk.call(instance) if blk
|
685
648
|
end
|
686
649
|
end
|
687
650
|
|
688
651
|
private
|
652
|
+
|
689
653
|
attr_reader :chain, :build_opts, :stream_monitor
|
690
654
|
|
691
655
|
def build_cmd(cmd)
|
@@ -718,7 +682,7 @@ module Drydock
|
|
718
682
|
end
|
719
683
|
|
720
684
|
def requires_from!(instruction)
|
721
|
-
|
685
|
+
fail InvalidInstructionError, "`#{instruction}` cannot be called before `from`" unless chain
|
722
686
|
end
|
723
687
|
|
724
688
|
end
|
data/lib/drydock/tar_writer.rb
CHANGED
@@ -5,7 +5,7 @@ module Drydock
|
|
5
5
|
def add_entry(name, mode: 0644, mtime: Time.now, uid: 0, gid: 0)
|
6
6
|
check_closed
|
7
7
|
|
8
|
-
|
8
|
+
fail Gem::Package::NonSeekableIO unless @io.respond_to?(:pos=)
|
9
9
|
|
10
10
|
name, prefix = split_name(name)
|
11
11
|
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dry-dock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ripta Pasay
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-10-
|
11
|
+
date: 2015-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: attr_extras
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.4'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: docker-api
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +52,20 @@ dependencies:
|
|
38
52
|
- - "~>"
|
39
53
|
- !ruby/object:Gem::Version
|
40
54
|
version: '0.45'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: memoist
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.12'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.12'
|
41
69
|
- !ruby/object:Gem::Dependency
|
42
70
|
name: rake
|
43
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +80,20 @@ dependencies:
|
|
52
80
|
- - "~>"
|
53
81
|
- !ruby/object:Gem::Version
|
54
82
|
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.34'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.34'
|
55
97
|
- !ruby/object:Gem::Dependency
|
56
98
|
name: jeweler
|
57
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -124,6 +166,7 @@ files:
|
|
124
166
|
- ".dockerignore"
|
125
167
|
- ".pryrc"
|
126
168
|
- ".rspec"
|
169
|
+
- ".rubocop.yml"
|
127
170
|
- ".travis.yml"
|
128
171
|
- ".yardopts"
|
129
172
|
- Dockerfile
|
@@ -152,6 +195,8 @@ files:
|
|
152
195
|
- lib/drydock/formatters.rb
|
153
196
|
- lib/drydock/ignorefile_definition.rb
|
154
197
|
- lib/drydock/image_repository.rb
|
198
|
+
- lib/drydock/instructions/base.rb
|
199
|
+
- lib/drydock/instructions/copy.rb
|
155
200
|
- lib/drydock/logger.rb
|
156
201
|
- lib/drydock/object_caches/base.rb
|
157
202
|
- lib/drydock/object_caches/filesystem_cache.rb
|