dockerspec 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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