dockerspec 0.1.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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +7 -0
  3. data/CHANGELOG.md +7 -0
  4. data/CONTRIBUTING.md +13 -0
  5. data/LICENSE +190 -0
  6. data/README.md +202 -0
  7. data/Rakefile +57 -0
  8. data/TESTING.md +30 -0
  9. data/TODO.md +6 -0
  10. data/lib/dockerspec.rb +21 -0
  11. data/lib/dockerspec/builder.rb +408 -0
  12. data/lib/dockerspec/builder/config_helpers.rb +425 -0
  13. data/lib/dockerspec/builder/image_gc.rb +71 -0
  14. data/lib/dockerspec/builder/logger.rb +56 -0
  15. data/lib/dockerspec/builder/logger/ci.rb +69 -0
  16. data/lib/dockerspec/builder/logger/debug.rb +47 -0
  17. data/lib/dockerspec/builder/logger/info.rb +111 -0
  18. data/lib/dockerspec/builder/logger/silent.rb +51 -0
  19. data/lib/dockerspec/builder/matchers.rb +134 -0
  20. data/lib/dockerspec/builder/matchers/helpers.rb +88 -0
  21. data/lib/dockerspec/docker_gem.rb +25 -0
  22. data/lib/dockerspec/exceptions.rb +26 -0
  23. data/lib/dockerspec/helper/ci.rb +61 -0
  24. data/lib/dockerspec/helper/docker.rb +44 -0
  25. data/lib/dockerspec/helper/multiple_sources_description.rb +142 -0
  26. data/lib/dockerspec/helper/rspec_example_helpers.rb +48 -0
  27. data/lib/dockerspec/rspec_assertions.rb +54 -0
  28. data/lib/dockerspec/rspec_resources.rb +198 -0
  29. data/lib/dockerspec/rspec_settings.rb +29 -0
  30. data/lib/dockerspec/runner.rb +374 -0
  31. data/lib/dockerspec/serverspec.rb +20 -0
  32. data/lib/dockerspec/serverspec/rspec_resources.rb +174 -0
  33. data/lib/dockerspec/serverspec/rspec_settings.rb +27 -0
  34. data/lib/dockerspec/serverspec/runner.rb +302 -0
  35. data/lib/dockerspec/serverspec/specinfra_backend.rb +128 -0
  36. data/lib/dockerspec/serverspec/specinfra_hack.rb +43 -0
  37. data/lib/dockerspec/version.rb +29 -0
  38. data/spec/spec_helper.rb +44 -0
  39. metadata +293 -0
@@ -0,0 +1,198 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author:: Xabier de Zuazo (<xabier@zuazo.org>)
4
+ # Copyright:: Copyright (c) 2015 Xabier de Zuazo
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'dockerspec/builder'
21
+ require 'dockerspec/runner' # Not really necessary (currently unused)
22
+ require 'dockerspec/rspec_settings'
23
+ require 'dockerspec/rspec_assertions'
24
+
25
+ module Dockerspec
26
+ #
27
+ # Some resources included inside {RSpec::Core::ExampleGroup} to build and run
28
+ # Docker containers.
29
+ #
30
+ # ## RSpec Settings
31
+ #
32
+ # * `dockerfile_path`: The dockerfile path.
33
+ # * `rm_build`: Whether to remove the build after the run.
34
+ # * `log_level`: Log level to use by default.
35
+ #
36
+ # All the RSpec settings are optional.
37
+ #
38
+ # @example RSpec Settings
39
+ # RSpec.configure do |config|
40
+ # config.log_level = :silent
41
+ # end
42
+ #
43
+ module RSpecResources
44
+ #
45
+ # Builds a Docker image.
46
+ #
47
+ # The image can be build from a path or from a string.
48
+ #
49
+ # See the {Dockerspec::Builder::ConfigHelpers} documentation for more
50
+ # information about the available RSpec resource helpers.
51
+ #
52
+ # @example A Simple Example
53
+ # describe 'My Dockerfile' do
54
+ # describe docker_build('.') do
55
+ # it { should have_maintainer /John Doe/ }
56
+ # it { should have_cmd ['/bin/dash'] }
57
+ # it { should have_expose '80' }
58
+ # end
59
+ # end
60
+ #
61
+ # @example A Complete Example
62
+ # describe docker_build(path: '.') do
63
+ # it { should have_maintainer 'John Doe "john.doe@example.com"' }
64
+ # it { should have_maintainer(/John Doe/) }
65
+ # it { should have_cmd %w(2 2000) }
66
+ # it { should have_label 'description' }
67
+ # it { should have_label 'description' => 'My Container' }
68
+ # it { should have_expose '80' }
69
+ # it { should have_expose(/80$/) }
70
+ # it { should have_env 'container' }
71
+ # it { should have_env 'container' => 'docker' }
72
+ # it { should have_env 'CRACKER' => 'RANDOM;PATH=/tmp/bin:/sbin:/bin' }
73
+ # it { should have_entrypoint ['sleep'] }
74
+ # it { should have_volume '/volume1' }
75
+ # it { should have_volume %r{/vol.*2} }
76
+ # it { should have_user 'nobody' }
77
+ # it { should have_workdir '/opt' }
78
+ # it { should have_workdir %r{^/op} }
79
+ # it { should have_onbuild 'RUN echo onbuild' }
80
+ # it { should have_stopsignal 'SIGTERM' }
81
+ # end
82
+ #
83
+ # @example Checking the Attribute Values Using the `its` Method
84
+ # describe docker_build(path: '.') do
85
+ # its(:maintainer) { should eq 'John Doe "john.doe@example.com"' }
86
+ # its(:cmd) { should eq %w(2 2000) }
87
+ # its(:labels) { should include 'description' }
88
+ # its(:labels) { should include 'description' => 'My Container' }
89
+ # its(:exposes) { should include '80' }
90
+ # its(:env) { should include 'container' }
91
+ # its(:env) { should include 'container' => 'docker' }
92
+ # its(:entrypoint) { should eq ['sleep'] }
93
+ # its(:volumes) { should include '/volume1' }
94
+ # its(:user) { should eq 'nobody' }
95
+ # its(:workdir) { should eq '/opt' }
96
+ # its(:onbuilds) { should include 'RUN echo onbuild' }
97
+ # its(:stopsignal) { should eq 'SIGTERM' }
98
+ # end
99
+ #
100
+ # @example Checking Its Size and OS
101
+ # describe docker_build(path: '.') do
102
+ # its(:size) { should be < 20 * 2**20 } # 20M
103
+ # its(:arch) { should eq 'amd64' }
104
+ # its(:os) { should eq 'linux' }
105
+ # end
106
+ #
107
+ # @example Building from a File
108
+ # describe docker_build(path: '../dockerfiles/Dockerfile-nginx') do
109
+ # # [...]
110
+ # end
111
+ #
112
+ # @example Building from a Template
113
+ # describe docker_build(template: 'Dockerfile1.erb') do
114
+ # # [...]
115
+ # end
116
+ #
117
+ # @example Building from a Template with a Context
118
+ # describe docker_build(
119
+ # template: 'Dockerfile1.erb', context: {version: '8'}
120
+ # ) do
121
+ # it { should have_maintainer(/John Doe/) }
122
+ # it { should have_cmd %w(/bin/sh) }
123
+ # # [...]
124
+ # end
125
+ #
126
+ # @example Building from a String
127
+ # describe docker_build(string: "FROM nginx:1.9\n [...]") do
128
+ # # [...]
129
+ # end
130
+ #
131
+ # @example Building from a Docker Image ID
132
+ # describe docker_build(id: '07d362aea98d') do
133
+ # # [...]
134
+ # end
135
+ #
136
+ # @example Building from a Docker Image name
137
+ # describe docker_build(id: 'nginx:1.9') do
138
+ # # [...]
139
+ # end
140
+ #
141
+ # @param opts [String, Hash] The `:path` or a list of options.
142
+ #
143
+ # @option opts [String] :path ('.') The directory or file that contains the
144
+ # *Dockerfile*. By default tries to read it from the `DOCKERFILE_PATH`
145
+ # environment variable and uses `'.'` if it is not set.
146
+ # @option opts [String] :string Use this string as *Dockerfile* instead of
147
+ # `:path`. Not set by default.
148
+ # @option opts [String] :template Use this [Erubis]
149
+ # (http://www.kuwata-lab.com/erubis/users-guide.html) template file as
150
+ # *Dockerfile*.
151
+ # @option opts [String] :id Use this Docker image ID instead of a
152
+ # *Dockerfile*.
153
+ # @option opts [Boolean] :rm Whether to remove the generated docker images
154
+ # after running the tests. By default only removes them if it is running
155
+ # on a CI machine.
156
+ # @option opts [Hash, Erubis::Context] :context ({}) Template *context*
157
+ # used when the `:template` source is used.
158
+ # @option opts [String] :tag Repository tag to be applied to the resulting
159
+ # image.
160
+ # @option opts [Fixnum, Symbol] :log_level Sets the docker library
161
+ # verbosity level. Possible values:
162
+ # `:silent` or `0` (no output),
163
+ # `:ci` or `1` (enables some outputs recommended for CI environments),
164
+ # `:info` or `2` (gives information about main build steps),
165
+ # `:debug` or `3` (outputs all the provided information in its raw
166
+ # original form).
167
+ #
168
+ # @return [Dockerspec::Builder] Builder object.
169
+ #
170
+ # @see Dockerspec::Builder::ConfigHelpers
171
+ #
172
+ # @api public
173
+ #
174
+ def docker_build(*opts)
175
+ builder = Dockerspec::Builder.new(*opts)
176
+ builder.build
177
+ end
178
+
179
+ #
180
+ # Runs a docker image.
181
+ #
182
+ # @param opts [Hash] List of options.
183
+ #
184
+ # @see Dockerspec::Serverspec::RSpecResources#docker_run
185
+ #
186
+ def docker_run(*opts)
187
+ RSpecAssertions.assert_docker_run!(opts)
188
+ end
189
+ end
190
+ end
191
+
192
+ #
193
+ # Add the Dockerspec resources to RSpec core.
194
+ #
195
+ RSpec::Core::ExampleGroup.class_eval do
196
+ extend Dockerspec::RSpecResources
197
+ include Dockerspec::RSpecResources
198
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author:: Xabier de Zuazo (<xabier@zuazo.org>)
4
+ # Copyright:: Copyright (c) 2015 Xabier de Zuazo
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'rspec'
21
+
22
+ #
23
+ # Add some RSpec custom settings for {Dockerspec}.
24
+ #
25
+ RSpec.configure do |c|
26
+ c.add_setting :dockerfile_path
27
+ c.add_setting :rm_build
28
+ c.add_setting :log_level
29
+ end
@@ -0,0 +1,374 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author:: Xabier de Zuazo (<xabier@zuazo.org>)
4
+ # Copyright:: Copyright (c) 2015 Xabier de Zuazo
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'docker'
21
+ require 'dockerspec/docker_gem'
22
+ require 'dockerspec/exceptions'
23
+ require 'dockerspec/helper/multiple_sources_description'
24
+
25
+ module Dockerspec
26
+ #
27
+ # This class runs a docker image (without using Serverspec for that).
28
+ #
29
+ # This class is not used much, only inherited by
30
+ # {Dockerspec::Serverspec::Runner}, which uses Serverspec to run the images.
31
+ # Some of the methods here are used there, others not.
32
+ #
33
+ class Runner
34
+ include Dockerspec::Helper::MultipleSourcesDescription
35
+
36
+ #
37
+ # Constructs a Docker runner class to run Docker images.
38
+ #
39
+ # @example From a Running Docker Image
40
+ # Dockerspec::Runner.new('debian:8') #=> #<Dockerspec::Runner:0x0124>
41
+ #
42
+ # @example From a Running Docker Container ID
43
+ # # This does not start any new container
44
+ # Dockerspec::Runner.new(id: 'c51f86c28340')
45
+ # #=> #<Dockerspec::Runner:0x0124>
46
+ #
47
+ # @example From a Running Docker Container Image Name
48
+ # Dockerspec::Runner.new('my-debian') #=> #<Dockerspec::Runner:0x0125>
49
+ #
50
+ # @param opts [String, Hash] The `:tag` or a list of options.
51
+ #
52
+ # @option opts [String] :tag The Docker image tag name to run.
53
+ # @option opts [String] :id The Docker container ID to use instead of
54
+ # starting a new container.
55
+ # @option opts [Boolean] :rm (calculated) Whether to remove the Docker
56
+ # container afterwards.
57
+ # @option opts [String] :path The environment `PATH` value of the
58
+ # container.
59
+ # @option opts [Hash, Array] :env Some `ENV` instructions to add to the
60
+ # container.
61
+ #
62
+ # @return [Dockerspec::Runner] Runner object.
63
+ #
64
+ # @api public
65
+ #
66
+ def initialize(*opts)
67
+ @options = parse_options(opts)
68
+ send("setup_from_#{source}", @options[source])
69
+ ObjectSpace.define_finalizer(self, proc { finalize })
70
+ end
71
+
72
+ #
73
+ # Runs the Docker Container.
74
+ #
75
+ # @example
76
+ # builder = Dockerspec::Builder.new('.')
77
+ # builder.build
78
+ # runner = Dockerspec::Runner.new(builder)
79
+ # runner.run #=> #<Dockerspec::Runner:0x0123>
80
+ #
81
+ # @return [Dockerspec::Runner] Runner object.
82
+ #
83
+ # @api public
84
+ #
85
+ def run
86
+ create_container
87
+ run_container
88
+ self
89
+ end
90
+
91
+ #
92
+ # Gets the Docker container ID.
93
+ #
94
+ # @example
95
+ # builder = Dockerspec::Builder.new('.').build
96
+ # runner = Dockerspec::Runner.new(builder).run
97
+ # runner.id #=> "b8ba0befc716[...]"
98
+ #
99
+ # @return [String] Container ID.
100
+ #
101
+ # @api public
102
+ #
103
+ def id
104
+ return nil unless @container.respond_to?(:id)
105
+ @container.id
106
+ end
107
+
108
+ #
109
+ # Gets the Docker image ID.
110
+ #
111
+ # @example
112
+ # builder = Dockerspec::Builder.new('.').build
113
+ # runner = Dockerspec::Runner.new(builder)
114
+ # runner.image_id #=> "c51f86c28340[...]"
115
+ #
116
+ # @return [String] Image ID.
117
+ #
118
+ # @api public
119
+ #
120
+ def image_id
121
+ return @build.id unless @build.nil?
122
+ @container.json['Image']
123
+ end
124
+
125
+ #
126
+ # Stops and deletes the Docker Container.
127
+ #
128
+ # Automatically called when `:rm` option is enabled.
129
+ #
130
+ # @return void
131
+ #
132
+ # @api public
133
+ #
134
+ def finalize
135
+ return unless @options[:rm] && !@container.nil?
136
+ @container.stop
137
+ @container.delete
138
+ end
139
+
140
+ #
141
+ # Gets a descriptions of the object.
142
+ #
143
+ # @example Running from a Container Image ID
144
+ # r = Dockerspec::Runner.new('debian')
145
+ # r.to_s #=> "Docker Run from tag: \"debian\""
146
+ #
147
+ # @example Attaching to a Running Container ID
148
+ # r = Dockerspec::Runner.new(id: '92cc98ab560a')
149
+ # r.to_s #=> "Docker Run from id: \"92cc98ab560a\""
150
+ #
151
+ # @return [String] The object description.
152
+ #
153
+ # @api public
154
+ #
155
+ def to_s
156
+ description('Docker Run from')
157
+ end
158
+
159
+ protected
160
+
161
+ #
162
+ # Gets the source to start the container from.
163
+ #
164
+ # Possible values: `:tag`, `:id`.
165
+ #
166
+ # @example Start the Container from an Image Tag
167
+ # self.source #=> :tag
168
+ #
169
+ # @example Attach to a Running Container ID
170
+ # self.source #=> :id
171
+ #
172
+ # @return [Symbol] The source.
173
+ #
174
+ # @api private
175
+ #
176
+ def source
177
+ return @source unless @source.nil?
178
+ %i(tag id).any? do |from|
179
+ next false unless @options.key?(from)
180
+ @source = from # Used for description
181
+ end
182
+ @source
183
+ end
184
+
185
+ #
186
+ # Generates a description from Docker tag name.
187
+ #
188
+ # @example
189
+ # self.description_from_tag('debian') #=> "debian"
190
+ # self.description_from_tag('92cc98ab560a92cc98ab560[...]')
191
+ # #=> "92cc98ab560a"
192
+ #
193
+ # @return [String] The description, shortened if necessary.
194
+ #
195
+ # @see Dockerspec::Helper::MultipleSourceDescription#description_from_docker
196
+ #
197
+ # @api private
198
+ #
199
+ alias_method :description_from_tag, :description_from_docker
200
+
201
+ #
202
+ # Gets the default options configured using `RSpec.configuration`.
203
+ #
204
+ # @example
205
+ # self.rspec_options #=> {}
206
+ #
207
+ # @return [Hash] The configuration options.
208
+ #
209
+ # @api private
210
+ #
211
+ def rspec_options
212
+ {}
213
+ end
214
+
215
+ #
216
+ # Gets the default configuration options after merging them with RSpec
217
+ # configuration options.
218
+ #
219
+ # @example
220
+ # self.default_options #=> {}
221
+ #
222
+ # @return [Hash] The configuration options.
223
+ #
224
+ # @api private
225
+ #
226
+ def default_options
227
+ {}.merge(rspec_options)
228
+ end
229
+
230
+ #
231
+ # Ensures that the passed options are correct.
232
+ #
233
+ # Currently this only checks that you passed the `:tag` or the `:id`
234
+ # argument.
235
+ #
236
+ # @return void
237
+ #
238
+ # @raise [Dockerspec::DockerRunArgumentError] Raises this exception when
239
+ # the required fields are missing.
240
+ #
241
+ # @api private
242
+ #
243
+ def assert_options!(opts)
244
+ return if opts[:tag].is_a?(String) || opts[:id].is_a?(String)
245
+ fail DockerRunArgumentError, 'You need to pass the `:tag` or the `:id` '\
246
+ 'option to the #docker_run method.'
247
+ end
248
+
249
+ #
250
+ # Parses the configuration options passed to the constructor.
251
+ #
252
+ # @example
253
+ # self.parse_options #=> {:rm=>true}
254
+ #
255
+ # @param opts [Array<String, Hash>] The list of options. The strings will
256
+ # be interpreted as `:tag`, others will be merged.
257
+ #
258
+ # @return [Hash] The configuration options.
259
+ #
260
+ # @raise [Dockerspec::DockerRunArgumentError] Raises this exception when
261
+ # some required fields are missing.
262
+ #
263
+ # @see #initialize
264
+ #
265
+ # @api private
266
+ #
267
+ def parse_options(opts)
268
+ opts_hs_ary = opts.map { |x| x.is_a?(Hash) ? x : { tag: x } }
269
+ result = opts_hs_ary.reduce(default_options) { |a, e| a.merge(e) }
270
+ assert_options!(result)
271
+ result
272
+ end
273
+
274
+ #
275
+ # Generates the build object from the Docker image tag.
276
+ #
277
+ # Saves the build internally.
278
+ #
279
+ # @param tag [String] The image name or ID.
280
+ #
281
+ # @return void
282
+ #
283
+ # @api private
284
+ #
285
+ def setup_from_tag(tag)
286
+ @build = Builder.new(id: tag).build
287
+ end
288
+
289
+ #
290
+ # Generates the container object from a running Docker container.
291
+ #
292
+ # Saves the container internally.
293
+ #
294
+ # @param id [String] The container ID or name.
295
+ #
296
+ # @return void
297
+ #
298
+ # @api private
299
+ #
300
+ def setup_from_id(id)
301
+ @container = ::Docker::Container.get(id)
302
+ end
303
+
304
+ #
305
+ # Ensures that the Docker container has a correct `CMD`.
306
+ #
307
+ # @param opts [Hash] {Docker::Container} options.
308
+ #
309
+ # @return [Hash] {Docker::Container} options.
310
+ #
311
+ # @api private
312
+ #
313
+ def add_container_cmd_option(opts)
314
+ opts['Cmd'] = %w(/bin/sh) if @build.cmd.nil?
315
+ opts
316
+ end
317
+
318
+ #
319
+ # Adds some `ENV` options to the Docker container.
320
+ #
321
+ # @param opts [Hash] {Docker::Container} options.
322
+ #
323
+ # @return [Hash] {Docker::Container} options.
324
+ #
325
+ # @api private
326
+ #
327
+ def add_container_env_options(opts)
328
+ opts['Env'] = opts['Env'].to_a << "PATH=#{path}" if @options.key?(:path)
329
+ env = @options[:env].to_a.map { |v| v.join('=') }
330
+ opts['Env'] = opts['Env'].to_a.concat(env)
331
+ opts
332
+ end
333
+
334
+ #
335
+ # Generates the Docker container options for {Docker::Container}.
336
+ #
337
+ # @return [Hash] The container options.
338
+ #
339
+ # @api private
340
+ #
341
+ def container_options
342
+ opts = { 'Image' => image_id, 'OpenStdin' => true }
343
+
344
+ add_container_cmd_option(opts)
345
+ add_container_env_options(opts)
346
+ opts
347
+ end
348
+
349
+ #
350
+ # Creates the Docker container.
351
+ #
352
+ # *Note: Based on Specinfra `:docker` backend code.*
353
+ #
354
+ # @return void
355
+ #
356
+ # @api private
357
+ #
358
+ def create_container
359
+ return @container unless @container.nil?
360
+ @container = ::Docker::Container.create(container_options)
361
+ end
362
+
363
+ #
364
+ # Runs the Docker container.
365
+ #
366
+ # @return void
367
+ #
368
+ # @api private
369
+ #
370
+ def run_container
371
+ @container.start
372
+ end
373
+ end
374
+ end