dockerspec 0.2.0 → 0.3.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/LICENSE +1 -1
  4. data/README.md +190 -24
  5. data/Rakefile +60 -6
  6. data/TODO.md +3 -2
  7. data/lib/dockerspec.rb +1 -2
  8. data/lib/dockerspec/builder.rb +13 -6
  9. data/lib/dockerspec/builder/config_helpers.rb +8 -3
  10. data/lib/dockerspec/builder/logger/ci.rb +2 -2
  11. data/lib/dockerspec/builder/matchers.rb +32 -51
  12. data/lib/dockerspec/configuration.rb +188 -0
  13. data/lib/dockerspec/docker_exception_parser.rb +2 -2
  14. data/lib/dockerspec/engine/base.rb +156 -0
  15. data/lib/dockerspec/engine/infrataster.rb +87 -0
  16. data/lib/dockerspec/engine/specinfra.rb +130 -0
  17. data/lib/dockerspec/engine/specinfra/backend.rb +163 -0
  18. data/lib/dockerspec/{serverspec/specinfra_hack.rb → engine/specinfra/backend_hack.rb} +17 -3
  19. data/lib/dockerspec/engine_list.rb +150 -0
  20. data/lib/dockerspec/exceptions.rb +13 -0
  21. data/lib/dockerspec/helper/multiple_sources_description.rb +3 -3
  22. data/lib/dockerspec/helper/rspec_example_helpers.rb +86 -6
  23. data/lib/dockerspec/infrataster.rb +26 -0
  24. data/lib/dockerspec/rspec.rb +21 -0
  25. data/lib/dockerspec/{rspec_configuration.rb → rspec/configuration.rb} +1 -1
  26. data/lib/dockerspec/rspec/resources.rb +602 -0
  27. data/lib/dockerspec/rspec/resources/its_container.rb +110 -0
  28. data/lib/dockerspec/{rspec_settings.rb → rspec/settings.rb} +5 -1
  29. data/lib/dockerspec/runner.rb +2 -357
  30. data/lib/dockerspec/runner/base.rb +367 -0
  31. data/lib/dockerspec/runner/compose.rb +322 -0
  32. data/lib/dockerspec/runner/docker.rb +302 -0
  33. data/lib/dockerspec/runner/serverspec.rb +21 -0
  34. data/lib/dockerspec/runner/serverspec/base.rb +185 -0
  35. data/lib/dockerspec/runner/serverspec/compose.rb +106 -0
  36. data/lib/dockerspec/runner/serverspec/docker.rb +116 -0
  37. data/lib/dockerspec/runner/serverspec/rspec.rb +20 -0
  38. data/lib/dockerspec/{serverspec/rspec_settings.rb → runner/serverspec/rspec/settings.rb} +1 -1
  39. data/lib/dockerspec/serverspec.rb +12 -2
  40. data/lib/dockerspec/version.rb +1 -1
  41. data/spec/spec_helper.rb +6 -2
  42. metadata +84 -15
  43. data/lib/dockerspec/rspec_assertions.rb +0 -54
  44. data/lib/dockerspec/rspec_resources.rb +0 -203
  45. data/lib/dockerspec/serverspec/rspec_resources.rb +0 -179
  46. data/lib/dockerspec/serverspec/runner.rb +0 -305
  47. data/lib/dockerspec/serverspec/specinfra_backend.rb +0 -128
@@ -0,0 +1,367 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author:: Xabier de Zuazo (<xabier@zuazo.org>)
4
+ # Copyright:: Copyright (c) 2016 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/exceptions'
21
+ require 'dockerspec/engine_list'
22
+ require 'dockerspec/helper/rspec_example_helpers'
23
+
24
+ module Dockerspec
25
+ module Runner
26
+ #
27
+ # A basic class with the minimal skeleton to create a Runner: Classes to
28
+ # start docker containers.
29
+ #
30
+ class Base
31
+ #
32
+ # The option key to set when you pass a string instead of a hash of
33
+ # options.
34
+ #
35
+ OPTIONS_DEFAULT_KEY = :ignored
36
+
37
+ #
38
+ # Gets the configuration options.
39
+ #
40
+ # @return [Hash] The options.
41
+ #
42
+ # @api private
43
+ #
44
+ attr_reader :options
45
+
46
+ #
47
+ # Constructs a runner class to run Docker images.
48
+ #
49
+ # @param opts [String, Hash] The id/name/file or a list of options.
50
+ #
51
+ # @option opts [Boolean] :rm (calculated) Whether to remove the Docker
52
+ # container afterwards.
53
+ # @option opts [Integer] :wait Time to wait before running the tests.
54
+ #
55
+ # @return [Dockerspec::Runner::Base] Runner object.
56
+ #
57
+ # @raise [Dockerspec::EngineError] Raises this exception when the engine
58
+ # list is empty.
59
+ #
60
+ # @api public
61
+ #
62
+ def initialize(*opts)
63
+ @options = parse_options(opts)
64
+ @engines = EngineList.new(self)
65
+ ObjectSpace.define_finalizer(self, proc { finalize })
66
+ end
67
+
68
+ #
69
+ # Runs the Docker Container.
70
+ #
71
+ # 1. Sets up the test context.
72
+ # 2. Runs the container (or Compose).
73
+ # 3. Saves the created underlaying test context.
74
+ # 4. Sets the container as ready.
75
+ # 5. Waits the required (configured) time after container has been
76
+ # started.
77
+ #
78
+ # @example
79
+ # builder = Dockerspec::Builder.new('.')
80
+ # builder.build
81
+ # runner = Dockerspec::Runner::Base.new(builder)
82
+ # runner.run #=> #<Dockerspec::Runner::Base:0x0123>
83
+ #
84
+ # @return [Dockerspec::Runner::Base] Runner object.
85
+ #
86
+ # @raise [Dockerspec::DockerError] For underlaying docker errors.
87
+ #
88
+ # @api public
89
+ #
90
+ def run
91
+ before_running
92
+ start_time = Time.new.utc
93
+ run_container
94
+ when_running
95
+ when_container_ready
96
+ do_wait((Time.new.utc - start_time).to_i)
97
+ self
98
+ end
99
+
100
+ #
101
+ # Restores the Specinfra backend instance to point to this object's
102
+ # container.
103
+ #
104
+ # This is used to avoid Serverspec running against the last started
105
+ # container if you are testing multiple containers at the same time.
106
+ #
107
+ # @return void
108
+ #
109
+ def restore_rspec_context
110
+ @engines.restore
111
+ end
112
+
113
+ #
114
+ # Gets the internal {Docker::Container} object.
115
+ #
116
+ # @return [Docker::Container] The container.
117
+ #
118
+ # @raise [Dockerspec::RunnerError] When the method is no implemented in
119
+ # the subclass.
120
+ #
121
+ # @api public
122
+ #
123
+ def container
124
+ raise RunnerError, "#{self.class}#container method must be implemented"
125
+ end
126
+
127
+ #
128
+ # Gets the container name.
129
+ #
130
+ # @return [String] Container name.
131
+ #
132
+ # @raise [Dockerspec::RunnerError] When the `#container` method is no
133
+ # implemented in the subclass or cannot select the container to test.
134
+ #
135
+ # @api public
136
+ #
137
+ def container_name
138
+ container.json['Name']
139
+ end
140
+
141
+ #
142
+ # Gets the Docker container ID.
143
+ #
144
+ # @example
145
+ # builder = Dockerspec::Builder.new('.').build
146
+ # runner = Dockerspec::Runner::Base.new(builder).run
147
+ # runner.id #=> "b8ba0befc716[...]"
148
+ #
149
+ # @return [String] Container ID.
150
+ #
151
+ # @raise [Dockerspec::RunnerError] When the `#container` method is no
152
+ # implemented in the subclass or cannot select the container to test.
153
+ #
154
+ # @api public
155
+ #
156
+ def id
157
+ return nil unless container.respond_to?(:id)
158
+ container.id
159
+ end
160
+
161
+ #
162
+ # Gets the Docker image ID.
163
+ #
164
+ # @return [String] Image ID.
165
+ #
166
+ # @raise [Dockerspec::RunnerError] When the `#container` method is no
167
+ # implemented in the subclass or cannot select the container to test.
168
+ #
169
+ # @api public
170
+ #
171
+ def image_id
172
+ container.json['Image']
173
+ end
174
+
175
+ #
176
+ # Gets the Docker Container IP address.
177
+ #
178
+ # This is used by {Dockerspec::Engine::Infrataster}.
179
+ #
180
+ # @return [String] IP address.
181
+ #
182
+ # @raise [Dockerspec::RunnerError] When the `#container` method is no
183
+ # implemented in the subclass or cannot select the container to test.
184
+ #
185
+ # @api public
186
+ #
187
+ def ipaddress
188
+ container.json['NetworkSettings']['IPAddress']
189
+ end
190
+
191
+ #
192
+ # Stops and deletes the Docker Container.
193
+ #
194
+ # Automatically called when `:rm` option is enabled.
195
+ #
196
+ # @return void
197
+ #
198
+ # @api public
199
+ #
200
+ def finalize
201
+ return if options[:rm] == false || container.nil?
202
+ container.stop
203
+ container.delete
204
+ end
205
+
206
+ protected
207
+
208
+ #
209
+ # Sets up the context just before starting the docker container.
210
+ #
211
+ # @return void
212
+ #
213
+ # @api public
214
+ #
215
+ def before_running
216
+ @engines.before_running
217
+ end
218
+
219
+ #
220
+ # Saves the context after starting the docker container.
221
+ #
222
+ # @return void
223
+ #
224
+ # @api public
225
+ #
226
+ def when_running
227
+ @engines.when_running
228
+ end
229
+
230
+ #
231
+ # Notifies the engines that the container to test is selected and ready.
232
+ #
233
+ # @return void
234
+ #
235
+ # @api public
236
+ #
237
+ def when_container_ready
238
+ @engines.when_container_ready
239
+ end
240
+
241
+ #
242
+ # Gets the default options configured using `RSpec.configuration`.
243
+ #
244
+ # @example
245
+ # self.rspec_options #=> {}
246
+ #
247
+ # @return [Hash] The configuration options.
248
+ #
249
+ # @api private
250
+ #
251
+ def rspec_options
252
+ config = ::RSpec.configuration
253
+ {}.tap do |opts|
254
+ opts[:wait] = config.docker_wait if config.docker_wait?
255
+ end
256
+ end
257
+
258
+ #
259
+ # The option key to set when you pass a string instead of a hash of
260
+ # options.
261
+ #
262
+ # @return [Symbol] The key name.
263
+ #
264
+ # @api private
265
+ #
266
+ def options_default_key
267
+ self.class::OPTIONS_DEFAULT_KEY
268
+ end
269
+
270
+ #
271
+ # Gets the default configuration options after merging them with RSpec
272
+ # configuration options.
273
+ #
274
+ # @example
275
+ # self.default_options #=> {}
276
+ #
277
+ # @return [Hash] The configuration options.
278
+ #
279
+ # @api private
280
+ #
281
+ def default_options
282
+ {}.merge(rspec_options)
283
+ end
284
+
285
+ #
286
+ # Ensures that the passed options are correct.
287
+ #
288
+ # Does nothing. Must be implemented in subclasses.
289
+ #
290
+ # @return void
291
+ #
292
+ # @api private
293
+ #
294
+ def assert_options!(opts); end
295
+
296
+ #
297
+ # Parses the configuration options passed to the constructor.
298
+ #
299
+ # @example
300
+ # self.parse_options #=> {:rm=>true, :file=> "docker-compose.yml"}
301
+ #
302
+ # @param opts [Array<String, Hash>] The list of options. The strings will
303
+ # be interpreted as `default_opt` key value, others will be merged.
304
+ #
305
+ # @return [Hash] The configuration options.
306
+ #
307
+ # @raise [Dockerspec::DockerRunArgumentError] Raises this exception when
308
+ # some required fields are missing.
309
+ #
310
+ # @see #initialize
311
+ #
312
+ # @api private
313
+ #
314
+ def parse_options(opts)
315
+ opts_hs_ary = opts.map do |x|
316
+ x.is_a?(Hash) ? x : { options_default_key => x }
317
+ end
318
+ result = opts_hs_ary.reduce(default_options) { |a, e| a.merge(e) }
319
+ assert_options!(result)
320
+ result
321
+ end
322
+
323
+ #
324
+ # Starts the Docker container.
325
+ #
326
+ # @return void
327
+ #
328
+ # @raise [Dockerspec::RunnerError] When the `#container` method is no
329
+ # implemented in the subclass.
330
+ #
331
+ # @api private
332
+ #
333
+ def run_container
334
+ container.start
335
+ end
336
+
337
+ #
338
+ # Sleeps for some time if required.
339
+ #
340
+ # Reads the seconds to sleep from the `:docker_wait` or `:wait`
341
+ # configuration option.
342
+ #
343
+ # @param waited [Integer] The time already waited.
344
+ #
345
+ # @return nil
346
+ #
347
+ # @api private
348
+ #
349
+ def do_wait(waited)
350
+ wait = options[:wait]
351
+ return unless wait.is_a?(Integer) || wait.is_a?(Float)
352
+ return if waited >= wait
353
+ sleep(wait - waited)
354
+ end
355
+ end
356
+ end
357
+ end
358
+
359
+ #
360
+ # Restore Specinfra backend:
361
+ #
362
+ RSpec.configure do |c|
363
+ c.before(:each) do
364
+ metadata = RSpec.current_example.metadata
365
+ Dockerspec::Helper::RSpecExampleHelpers.restore_rspec_context(metadata)
366
+ end
367
+ end
@@ -0,0 +1,322 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author:: Xabier de Zuazo (<xabier@zuazo.org>)
4
+ # Copyright:: Copyright (c) 2016 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-compose'
21
+ require 'dockerspec/exceptions'
22
+ require 'dockerspec/helper/multiple_sources_description'
23
+ require 'dockerspec/runner/base'
24
+
25
+ module Dockerspec
26
+ module Runner
27
+ #
28
+ # This class runs Docker Compose (without using Serverspec for that).
29
+ #
30
+ # This class is used mainly when you are not using Serverspec to run the
31
+ # tests.
32
+ #
33
+ class Compose < Base
34
+ class << self
35
+ #
36
+ # Saves the latest created {Dockerspec::Runner::Compose} object.
37
+ #
38
+ # @return [Docker::Runner::Compose::Base] The saved instance.
39
+ #
40
+ # @api public
41
+ #
42
+ attr_accessor :current_instance
43
+ end
44
+
45
+ include Dockerspec::Helper::MultipleSourcesDescription
46
+
47
+ #
48
+ # @return [Symbol] The option key to set when you pass a string instead
49
+ # of a hash of options.
50
+ #
51
+ OPTIONS_DEFAULT_KEY = :file
52
+
53
+ #
54
+ # The internal {DockerCompose} object.
55
+ #
56
+ # @return [DockerCompose] The compose object.
57
+ #
58
+ attr_reader :compose
59
+
60
+ #
61
+ # Constructs a runner class to run Docker Compose.
62
+ #
63
+ # @example From a Directory
64
+ # Dockerspec::Runner::Compose.new('directory1')
65
+ # #=> #<Dockerspec::Runner::Compose:0x0124>
66
+ #
67
+ # @example From a YAML File
68
+ # Dockerspec::Runner::Compose.new('data/docker-compose.yml')
69
+ # #=> #<Dockerspec::Runner::Compose:0x0124>
70
+ #
71
+ # @example From a Directory or File Using Hash Format
72
+ # Dockerspec::Runner::Compose.new(file: 'file.yml')
73
+ # #=> #<Dockerspec::Runner::Compose:0x0124>
74
+ #
75
+ # @param opts [String, Hash] The `:file` or a list of options.
76
+ #
77
+ # @option opts [String] :file The compose YAML file or a directory
78
+ # containing the `'docker-compose.yml'` file.
79
+ # @option opts [Boolean] :rm (calculated) Whether to remove the Docker
80
+ # @option opts [Integer] :wait Time to wait before running the tests.
81
+ #
82
+ # @return [Dockerspec::Runner::Compose] Runner object.
83
+ #
84
+ # @raise [Dockerspec::DockerRunArgumentError] Raises this exception when
85
+ # some required options are missing.
86
+ #
87
+ # @api public
88
+ #
89
+ def initialize(*opts)
90
+ Compose.current_instance = self
91
+ @container_options = {}
92
+ super
93
+ setup_from_file(file)
94
+ end
95
+
96
+ # Does not call ready because container is still not ready.
97
+ #
98
+ # Runs the Docker Container.
99
+ #
100
+ # 1. Sets up the test context.
101
+ # 2. Runs the container (or Compose).
102
+ # 3. Saves the created underlaying test context.
103
+ #
104
+ # @return [Dockerspec::Runner::Compose] Runner object.
105
+ #
106
+ # @raise [Dockerspec::DockerError] For underlaying docker errors.
107
+ #
108
+ # @see #select_conainer
109
+ #
110
+ # @api public
111
+ #
112
+ def run
113
+ before_running
114
+ start_time = Time.new.utc
115
+ run_container
116
+ when_running
117
+ do_wait((Time.new.utc - start_time).to_i)
118
+ self
119
+ end
120
+
121
+ #
122
+ # Selects the container to test and sets its configuration options.
123
+ #
124
+ # Also sets the selected container as ready in the underlaying test
125
+ # engines.
126
+ #
127
+ # @param name [Symbol, String] The container name.
128
+ #
129
+ # @param opts [Hash] Container configuration options.
130
+ #
131
+ # @return void
132
+ #
133
+ # @api public
134
+ #
135
+ def select_container(name, opts = nil)
136
+ @options[:container] = name
137
+ @container_options[name] = @options.merge(opts) if opts.is_a?(Hash)
138
+ when_container_ready
139
+ end
140
+
141
+ #
142
+ # Returns general and container specific options merged.
143
+ #
144
+ # @return void
145
+ #
146
+ # @api private
147
+ #
148
+ def options
149
+ container_name = @options[:container]
150
+ @container_options[container_name] || @options
151
+ end
152
+
153
+ #
154
+ # Gets the selected container name.
155
+ #
156
+ # @return [String, nil] The container name.
157
+ #
158
+ # @api private
159
+ #
160
+ def container_name
161
+ return nil if @options[:container].nil?
162
+ @options[:container].to_s
163
+ end
164
+
165
+ #
166
+ # Gets the selected container object.
167
+ #
168
+ # This method is used in {Dockerspec::Runner::Base} to get information
169
+ # from the container: ID, image ID, ...
170
+ #
171
+ # @return [Docker::Container] The container object.
172
+ #
173
+ # @raise [Dockerspec::RunnerError] When cannot select the container to
174
+ # test.
175
+ #
176
+ # @api public
177
+ #
178
+ def container
179
+ if container_name.nil?
180
+ raise RunnerError,
181
+ 'Use `its_container` to select a container to test.'
182
+ end
183
+ compose_container = compose.containers[container_name]
184
+ if compose_container.nil?
185
+ raise RunnerError, "Container not found: #{compose_container.inspect}"
186
+ end
187
+ compose_container.container
188
+ end
189
+
190
+ #
191
+ # Gets a descriptions of the object.
192
+ #
193
+ # @example Running from a Compose File
194
+ # r = Dockerspec::Runner::Compose.new('docker-compose.yml')
195
+ # r.to_s #=> "Docker Compose Run from file: \"docker-compose.yml\""
196
+ #
197
+ # @example Running from a Compose Directory
198
+ # r = Dockerspec::Runner::Compose.new('docker_images')
199
+ # r.to_s #=> "Docker Compose Run from file: "\
200
+ # # "\"docker_images/docker-compose.yml\""
201
+ #
202
+ # @return [String] The object description.
203
+ #
204
+ # @api public
205
+ #
206
+ def to_s
207
+ description('Docker Compose Run from')
208
+ end
209
+
210
+ #
211
+ # Stops and deletes the Docker Compose containers.
212
+ #
213
+ # Automatically called when `:rm` option is enabled.
214
+ #
215
+ # @return void
216
+ #
217
+ # @api public
218
+ #
219
+ def finalize
220
+ return if options[:rm] == false || compose.nil?
221
+ compose.stop
222
+ compose.delete
223
+ end
224
+
225
+ protected
226
+
227
+ #
228
+ # Gets the full path of the Docker Compose YAML file.
229
+ #
230
+ # It adds `'docker-compose.yml'` if you pass a directory.
231
+ #
232
+ # @return [String] The file path.
233
+ #
234
+ # @api private
235
+ #
236
+ def file
237
+ @file ||=
238
+ if File.directory?(options[source])
239
+ File.join(options[source], 'docker-compose.yml')
240
+ else
241
+ options[source]
242
+ end
243
+ end
244
+
245
+ #
246
+ # Gets the source to start the container from.
247
+ #
248
+ # Possible values: `:file`.
249
+ #
250
+ # @example Start the Container from a YAML Configuration File
251
+ # self.source #=> :file
252
+ #
253
+ # @return [Symbol] The source.
254
+ #
255
+ # @api private
256
+ #
257
+ def source
258
+ return @source unless @source.nil?
259
+ @source = %i(file).find { |from| options.key?(from) }
260
+ end
261
+
262
+ #
263
+ # Gets the default options configured using `RSpec.configuration`.
264
+ #
265
+ # @example
266
+ # self.rspec_options #=> {:container => "webapp", :docker_wait => 30}
267
+ #
268
+ # @return [Hash] The configuration options.
269
+ #
270
+ # @api private
271
+ #
272
+ def rspec_options
273
+ config = ::RSpec.configuration
274
+ super.tap do |opts|
275
+ opts[:container] = config.container_name if config.container_name?
276
+ end
277
+ end
278
+
279
+ #
280
+ # Ensures that the passed options are correct.
281
+ #
282
+ # Currently this only checks that you passed the `:file` argument.
283
+ #
284
+ # @return void
285
+ #
286
+ # @raise [Dockerspec::DockerRunArgumentError] Raises this exception when
287
+ # the required fields are missing.
288
+ #
289
+ # @api private
290
+ #
291
+ def assert_options!(opts)
292
+ return if opts[:file].is_a?(String)
293
+ raise DockerRunArgumentError, 'You need to pass the `:file` option '\
294
+ 'to the #docker_compose method.'
295
+ end
296
+
297
+ #
298
+ # Saves the build internally.
299
+ #
300
+ # @param file [String] The configuration file.
301
+ #
302
+ # @return void
303
+ #
304
+ # @api private
305
+ #
306
+ def setup_from_file(file)
307
+ @compose = ::DockerCompose.load(file)
308
+ end
309
+
310
+ #
311
+ # Runs Docker Compose.
312
+ #
313
+ # @return void
314
+ #
315
+ # @api private
316
+ #
317
+ def run_container
318
+ Dir.chdir(::File.dirname(file)) { compose.start }
319
+ end
320
+ end
321
+ end
322
+ end