jamie 0.1.0.alpha2 → 0.1.0.alpha3

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.
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Jamie
2
2
 
3
+ [![Build Status](https://secure.travis-ci.org/jamie-ci/jamie.png)](https://travis-ci.org/jamie-ci/jamie)
4
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/jamie-ci/jamie)
5
+
3
6
  A Chef convergence integration test harness.
4
7
 
5
8
  ## Installation
@@ -21,6 +21,22 @@ module Jamie
21
21
  run "vagrant provision #{instance.name}"
22
22
  end
23
23
 
24
+ def setup(instance)
25
+ if instance.jr.setup_cmd
26
+ ssh instance, instance.jr.setup_cmd
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def verify(instance)
33
+ if instance.jr.run_cmd
34
+ ssh instance, instance.jr.run_cmd
35
+ else
36
+ super
37
+ end
38
+ end
39
+
24
40
  def destroy(instance)
25
41
  run "vagrant destroy #{instance.name} -f"
26
42
  end
@@ -28,17 +44,26 @@ module Jamie
28
44
  private
29
45
 
30
46
  def run(cmd)
31
- puts " [vagrant command] '#{cmd}'"
47
+ puts " [vagrant command] '#{display_cmd(cmd)}'"
32
48
  shellout = Mixlib::ShellOut.new(
33
49
  cmd, :live_stream => STDOUT, :timeout => 60000
34
50
  )
35
51
  shellout.run_command
36
- puts " [vagrant command] '#{cmd}' ran " +
52
+ puts " [vagrant command] '#{display_cmd(cmd)}' ran " +
37
53
  "in #{shellout.execution_time} seconds."
38
54
  shellout.error!
39
55
  rescue Mixlib::ShellOut::ShellCommandFailed => ex
40
56
  raise ActionFailed, ex.message
41
57
  end
58
+
59
+ def ssh(instance, cmd)
60
+ run %{vagrant ssh #{instance.name} --command '#{cmd}'}
61
+ end
62
+
63
+ def display_cmd(cmd)
64
+ parts = cmd.partition("\n")
65
+ parts[1] == "\n" ? "#{parts[0]}..." : cmd
66
+ end
42
67
  end
43
68
  end
44
69
  end
data/lib/jamie/vagrant.rb CHANGED
@@ -15,7 +15,7 @@ module Jamie
15
15
 
16
16
  def_delegators :@config, :suites, :suites=, :platforms, :platforms=,
17
17
  :instances, :yaml_file, :yaml_file=, :log_level, :log_level=,
18
- :data_bags_base_path, :data_bags_base_path=, :yaml_data
18
+ :test_base_path, :test_base_path=, :yaml_data
19
19
 
20
20
  def initialize
21
21
  @config = Jamie::Config.new
@@ -52,7 +52,7 @@ module Jamie
52
52
  end
53
53
 
54
54
  def self.calculate_data_bags_path(config, instance)
55
- base_path = config.jamie.data_bags_base_path
55
+ base_path = config.jamie.test_base_path
56
56
  instance_data_bags_path = File.join(base_path, instance.name, "data_bags")
57
57
  common_data_bags_path = File.join(base_path, "data_bags")
58
58
 
data/lib/jamie/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Jamie
4
4
 
5
- VERSION = "0.1.0.alpha2"
5
+ VERSION = "0.1.0.alpha3"
6
6
  end
data/lib/jamie.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
+ require 'base64'
4
+ require 'digest'
5
+ require 'net/https'
3
6
  require 'yaml'
4
7
 
5
8
  require 'jamie/core_ext'
@@ -15,7 +18,7 @@ module Jamie
15
18
  attr_writer :platforms
16
19
  attr_writer :suites
17
20
  attr_writer :log_level
18
- attr_writer :data_bags_base_path
21
+ attr_writer :test_base_path
19
22
 
20
23
  # Default path to the Jamie YAML file
21
24
  DEFAULT_YAML_FILE = File.join(Dir.pwd, '.jamie.yml').freeze
@@ -27,7 +30,7 @@ module Jamie
27
30
  DEFAULT_DRIVER_PLUGIN = "vagrant".freeze
28
31
 
29
32
  # Default base path which may contain `data_bags/` directories
30
- DEFAULT_DATA_BAGS_BASE_PATH = File.join(Dir.pwd, 'test/integration').freeze
33
+ DEFAULT_TEST_BASE_PATH = File.join(Dir.pwd, 'test/integration').freeze
31
34
 
32
35
  # Creates a new configuration.
33
36
  #
@@ -68,8 +71,8 @@ module Jamie
68
71
 
69
72
  # @return [String] base path that may contain a common `data_bags/`
70
73
  # directory or an instance's `data_bags/` directory
71
- def data_bags_base_path
72
- @data_bags_path ||= DEFAULT_DATA_BAGS_BASE_PATH
74
+ def test_base_path
75
+ @test_base_path ||= DEFAULT_TEST_BASE_PATH
73
76
  end
74
77
 
75
78
  private
@@ -85,7 +88,22 @@ module Jamie
85
88
  end
86
89
 
87
90
  def yaml
88
- @yaml ||= YAML.load_file(File.expand_path(yaml_file))
91
+ @yaml ||= YAML.load_file(File.expand_path(yaml_file)).rmerge(local_yaml)
92
+ end
93
+
94
+ def local_yaml_file
95
+ std = File.expand_path(yaml_file)
96
+ std.sub(/(#{File.extname(std)})$/, '.local\1')
97
+ end
98
+
99
+ def local_yaml
100
+ @local_yaml ||= begin
101
+ if File.exists?(local_yaml_file)
102
+ YAML.load_file(local_yaml_file)
103
+ else
104
+ Hash.new
105
+ end
106
+ end
89
107
  end
90
108
 
91
109
  def merge_platform_config(platform_config)
@@ -131,12 +149,9 @@ module Jamie
131
149
 
132
150
  private
133
151
 
134
- def validate_options(options)
135
- if options['name'].nil?
136
- raise ArgumentError, "The option 'name' is required."
137
- end
138
- if options['run_list'].nil?
139
- raise ArgumentError, "The option 'run_list' is required."
152
+ def validate_options(opts)
153
+ %w(name run_list).each do |k|
154
+ raise ArgumentError, "Attribute '#{attr}' is required." if opts[k].nil?
140
155
  end
141
156
  end
142
157
  end
@@ -180,12 +195,9 @@ module Jamie
180
195
 
181
196
  private
182
197
 
183
- def validate_options(options)
184
- if options['name'].nil?
185
- raise ArgumentError, "The option 'name' is required."
186
- end
187
- if options['driver'].nil?
188
- raise ArgumentError, "The option 'driver' is required."
198
+ def validate_options(opts)
199
+ %w(name driver).each do |k|
200
+ raise ArgumentError, "Attribute '#{attr}' is required." if opts[k].nil?
189
201
  end
190
202
  end
191
203
  end
@@ -201,6 +213,9 @@ module Jamie
201
213
  # @return [Platform] the target platform configuration
202
214
  attr_reader :platform
203
215
 
216
+ # @return [Jr] jr command string generator
217
+ attr_reader :jr
218
+
204
219
  # Creates a new instance, given a suite and a platform.
205
220
  #
206
221
  # @param suite [Suite] a suite
@@ -208,6 +223,7 @@ module Jamie
208
223
  def initialize(suite, platform)
209
224
  @suite = suite
210
225
  @platform = platform
226
+ @jr = Jr.new(@suite.name)
211
227
  end
212
228
 
213
229
  # @return [String] name of this instance
@@ -259,7 +275,21 @@ module Jamie
259
275
  self
260
276
  end
261
277
 
262
- # Verifies this converged instance by executing tests.
278
+ # Sets up this converged instance for suite tests.
279
+ #
280
+ # @see Driver::Base#setup
281
+ # @return [self] this instance, used to chain actions
282
+ #
283
+ # @todo rescue Driver::ActionFailed and return some kind of null object
284
+ # to gracfully stop action chaining
285
+ def setup
286
+ puts "-----> Setting up instance #{name}"
287
+ platform.driver.setup(self)
288
+ puts " Setup of instance #{name} complete."
289
+ self
290
+ end
291
+
292
+ # Verifies this set up instance by executing suite tests.
263
293
  #
264
294
  # @see Driver::Base#verify
265
295
  # @return [self] this instance, used to chain actions
@@ -294,6 +324,7 @@ module Jamie
294
324
  # @see #destroy
295
325
  # @see #create
296
326
  # @see #converge
327
+ # @see #setup
297
328
  # @see #verify
298
329
  # @return [self] this instance, used to chain actions
299
330
  #
@@ -305,12 +336,157 @@ module Jamie
305
336
  puts "-----> Testing instance #{name}"
306
337
  create
307
338
  converge
339
+ setup
308
340
  verify
309
341
  puts " Testing of instance #{name} complete."
310
342
  self
311
343
  end
312
344
  end
313
345
 
346
+ # Command string generator to interface with Jamie Runner (jr). The
347
+ # commands that are generated are safe to pass to an SSH command or as an
348
+ # unix command argument (escaped in single quotes).
349
+ class Jr
350
+
351
+ # Constructs a new jr command generator, given a suite name.
352
+ #
353
+ # @param [String] suite_name name of suite on which to operate
354
+ # (**Required**)
355
+ # @param [Hash] opts optional configuration
356
+ # @option opts [TrueClass, FalseClass] :use_sudo whether or not to invoke
357
+ # sudo before commands requiring root access (default: `true`)
358
+ def initialize(suite_name, opts = {:use_sudo => true})
359
+ validate_options(suite_name)
360
+
361
+ @suite_name = suite_name
362
+ @use_sudo = opts[:use_sudo]
363
+ end
364
+
365
+ # Returns a command string which installs the Jamie Runner (jr), installs
366
+ # all required jr plugins for the suite, and transfers all suite test
367
+ # files.
368
+ #
369
+ # If no work needs to be performed, for example if there are no tests for
370
+ # the given suite, then `nil` will be returned.
371
+ #
372
+ # @return [String] a command string to setup the test suite, or nil if no
373
+ # work needs to be performed
374
+ def setup_cmd
375
+ @setup_cmd ||= begin
376
+ cmd = []
377
+ cmd << install_cmd if install_cmd
378
+ cmd << sync_cmd if sync_cmd
379
+ cmd.empty? ? nil : cmd.join("\n")
380
+ end
381
+ end
382
+
383
+ # Returns a command string which runs all jr suite tests for the suite.
384
+ #
385
+ # If no work needs to be performed, for example if there are no tests for
386
+ # the given suite, then `nil` will be returned.
387
+ #
388
+ # @return [String] a command string to run the test suites, or nil if no
389
+ # work needs to be performed
390
+ def run_cmd
391
+ @run_cmd ||= if plugins.empty?
392
+ nil
393
+ else
394
+ plugins.map { |p| "#{sudo}#{jr_bin} #{p}" }.join(' && ')
395
+ end
396
+ end
397
+
398
+ private
399
+
400
+ INSTALL_URL = "https://raw.github.com/jamie-ci/jr/go".freeze
401
+ DEFAULT_RUBY_BINPATH = "/opt/chef/embedded/bin".freeze
402
+ DEFAULT_JR_ROOT = "/opt/jr".freeze
403
+ DEFAULT_TEST_ROOT = File.join(Dir.pwd, "test/integration").freeze
404
+
405
+ def validate_options(suite_name)
406
+ raise ArgumentError, "'suite_name' is required." if suite_name.nil?
407
+ end
408
+
409
+ def install_cmd
410
+ @install_cmd ||= if plugins.empty?
411
+ nil
412
+ else
413
+ <<-INSTALL_CMD.gsub(/ {10}/, '')
414
+ #{sudo}#{ruby_bin} -e "$(cat <<"EOF"
415
+ #{install_script}
416
+ EOF
417
+ )"
418
+ #{sudo}#{jr_bin} install #{plugins.join(' ')}
419
+ INSTALL_CMD
420
+ end
421
+ end
422
+
423
+ def sync_cmd
424
+ @sync_cmd ||= if local_suite_files.empty?
425
+ nil
426
+ else
427
+ [ "#{sudo}rm -rf $(#{jr_bin} suitepath)/*",
428
+ local_suite_files.map { |f| stream_file(f, remote_file(f)) }.join
429
+ ].join("\n")
430
+ end
431
+ end
432
+
433
+ def install_script
434
+ @install_script ||= begin
435
+ uri = URI.parse(INSTALL_URL)
436
+ http = Net::HTTP.new(uri.host, 443)
437
+ http.use_ssl = true
438
+ response = http.request(Net::HTTP::Get.new(uri.path))
439
+ response.body
440
+ end
441
+ end
442
+
443
+ def plugins
444
+ Dir.glob(File.join(test_root, @suite_name, "*")).select { |d|
445
+ File.directory?(d) && File.basename(d) != "data_bags"
446
+ }.map { |d| File.basename(d) }.sort.uniq
447
+ end
448
+
449
+ def local_suite_files
450
+ Dir.glob(File.join(test_root, @suite_name, "*/**/*")).reject do |f|
451
+ f["data_bags"] || File.directory?(f)
452
+ end
453
+ end
454
+
455
+ def remote_file(file)
456
+ local_prefix = File.join(test_root, @suite_name)
457
+ "$(#{jr_bin} suitepath)/".concat(file.sub(%r{^#{local_prefix}/}, ''))
458
+ end
459
+
460
+ def stream_file(local_path, remote_path)
461
+ local_file = IO.read(local_path)
462
+ md5 = Digest::MD5.hexdigest(local_file)
463
+ perms = sprintf("%o", File.stat(local_path).mode)[3,3]
464
+ jr_stream_file = "#{jr_bin} stream-file #{remote_path} #{md5} #{perms}"
465
+
466
+ <<-STREAMFILE.gsub(/^ {8}/, '')
467
+ cat <<"__EOFSTREAM__" | #{sudo}#{jr_stream_file}
468
+ #{Base64.encode64(local_file)}
469
+ __EOFSTREAM__
470
+ STREAMFILE
471
+ end
472
+
473
+ def sudo
474
+ @use_sudo ? "sudo " : ""
475
+ end
476
+
477
+ def ruby_bin
478
+ File.join(DEFAULT_RUBY_BINPATH, "ruby")
479
+ end
480
+
481
+ def jr_bin
482
+ File.join(DEFAULT_JR_ROOT, "bin/jr")
483
+ end
484
+
485
+ def test_root
486
+ DEFAULT_TEST_ROOT
487
+ end
488
+ end
489
+
314
490
  module Driver
315
491
 
316
492
  # Wrapped exception for any internally raised driver exceptions.
@@ -365,6 +541,11 @@ module Jamie
365
541
  # @raise [ActionFailed] if the action could not be completed
366
542
  def destroy(instance) ; end
367
543
 
544
+ def setup(instance)
545
+ # Subclass may choose to implement
546
+ puts " Nothing to do!"
547
+ end
548
+
368
549
  # Verifies a converged instance.
369
550
  #
370
551
  # @param instance [Instance] an instance
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jamie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha2
4
+ version: 0.1.0.alpha3
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-03 00:00:00.000000000 Z
12
+ date: 2012-12-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mixlib-shellout
@@ -140,9 +140,6 @@ required_ruby_version: !ruby/object:Gem::Requirement
140
140
  - - ! '>='
141
141
  - !ruby/object:Gem::Version
142
142
  version: '0'
143
- segments:
144
- - 0
145
- hash: -4553058254287566395
146
143
  required_rubygems_version: !ruby/object:Gem::Requirement
147
144
  none: false
148
145
  requirements: