dockerspec 0.2.0 → 0.3.0

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