taste_tester 0.0.13 → 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,6 +19,7 @@ require 'taste_tester/host'
19
19
  require 'taste_tester/config'
20
20
  require 'taste_tester/client'
21
21
  require 'taste_tester/logging'
22
+ require 'taste_tester/exceptions'
22
23
 
23
24
  module TasteTester
24
25
  # Functionality dispatch
@@ -28,6 +29,7 @@ module TasteTester
28
29
  def self.start
29
30
  server = TasteTester::Server.new
30
31
  return if TasteTester::Server.running?
32
+
31
33
  server.start
32
34
  end
33
35
 
@@ -45,7 +47,12 @@ module TasteTester
45
47
  server = TasteTester::Server.new
46
48
  if TasteTester::Server.running?
47
49
  logger.warn("Local taste-tester server running on port #{server.port}")
48
- if server.latest_uploaded_ref
50
+ if TasteTester::Config.no_repo && server.last_upload_time
51
+ logger.warn("Last upload time was #{server.last_upload_time}")
52
+ elsif !TasteTester::Config.no_repo && server.latest_uploaded_ref
53
+ if server.last_upload_time
54
+ logger.warn("Last upload time was #{server.last_upload_time}")
55
+ end
49
56
  logger.warn('Latest uploaded revision is ' +
50
57
  server.latest_uploaded_ref)
51
58
  else
@@ -77,12 +84,16 @@ module TasteTester
77
84
  end
78
85
  server = TasteTester::Server.new
79
86
  unless TasteTester::Config.linkonly
80
- repo = BetweenMeals::Repo.get(
81
- TasteTester::Config.repo_type,
82
- TasteTester::Config.repo,
83
- logger,
84
- )
85
- unless repo.exists?
87
+ if TasteTester::Config.no_repo
88
+ repo = nil
89
+ else
90
+ repo = BetweenMeals::Repo.get(
91
+ TasteTester::Config.repo_type,
92
+ TasteTester::Config.repo,
93
+ logger,
94
+ )
95
+ end
96
+ if repo && !repo.exists?
86
97
  fail "Could not open repo from #{TasteTester::Config.repo}"
87
98
  end
88
99
  end
@@ -93,12 +104,11 @@ module TasteTester
93
104
  tested_hosts = []
94
105
  hosts.each do |hostname|
95
106
  host = TasteTester::Host.new(hostname, server)
96
- if host.in_test?
97
- username = host.who_is_testing
98
- logger.error("User #{username} is already testing on #{hostname}")
99
- else
107
+ begin
100
108
  host.test
101
109
  tested_hosts << hostname
110
+ rescue TasteTester::Exceptions::AlreadyTestingError => e
111
+ logger.error("User #{e.username} is already testing on #{hostname}")
102
112
  end
103
113
  end
104
114
  unless TasteTester::Config.skip_post_test_hook ||
@@ -106,6 +116,24 @@ module TasteTester
106
116
  TasteTester::Hooks.post_test(TasteTester::Config.dryrun, repo,
107
117
  tested_hosts)
108
118
  end
119
+ # Strictly: hosts and tested_hosts should be sets to eliminate variance in
120
+ # order or duplicates. The exact comparison works here because we're
121
+ # building tested_hosts from hosts directly.
122
+ if tested_hosts == hosts
123
+ # No exceptions, complete success: every host listed is now configured
124
+ # to use our chef-zero instance.
125
+ exit(0)
126
+ end
127
+ if tested_hosts.empty?
128
+ # All requested hosts are being tested by another user. We didn't change
129
+ # their configuration.
130
+ exit(3)
131
+ end
132
+ # Otherwise, we got a mix of success and failure due to being tested by
133
+ # another user. We'll be pessemistic and return an error because the
134
+ # intent to taste test the complete list was not successful.
135
+ # code.
136
+ exit(2)
109
137
  end
110
138
 
111
139
  def self.untest
@@ -149,7 +177,7 @@ module TasteTester
149
177
 
150
178
  def self.upload
151
179
  server = TasteTester::Server.new
152
- # On a fore-upload rather than try to clean up whatever's on the server
180
+ # On a force-upload rather than try to clean up whatever's on the server
153
181
  # we'll restart chef-zero which will clear everything and do a full
154
182
  # upload
155
183
  if TasteTester::Config.force_upload
@@ -182,5 +210,165 @@ module TasteTester
182
210
  logger.error(exception.backtrace.join("\n"))
183
211
  exit 1
184
212
  end
213
+
214
+ def self.impact
215
+ # Use the repository specified in config.rb to calculate the changes
216
+ # that may affect Chef. These changes will be further analyzed to
217
+ # determine specific roles which may change due to modifed dependencies.
218
+ repo = BetweenMeals::Repo.get(
219
+ TasteTester::Config.repo_type,
220
+ TasteTester::Config.repo,
221
+ logger,
222
+ )
223
+ if repo && !repo.exists?
224
+ fail "Could not open repo from #{TasteTester::Config.repo}"
225
+ end
226
+
227
+ changes = _find_changeset(repo)
228
+
229
+ # Use Knife (or custom logic) to check the dependencies of each role
230
+ # against the list of changes. `impacted_roles` will contian the set
231
+ # of roles with direct or indirect (dependency) modifications.
232
+ impacted_roles = TasteTester::Hooks.impact_find_roles(changes)
233
+ impacted_roles ||= _find_roles(changes)
234
+
235
+ # Do any post processing required on the list of impacted roles, such
236
+ # as looking up hostnames associated with each role.
237
+ final_impact = TasteTester::Hooks.post_impact(impacted_roles)
238
+ final_impact ||= impacted_roles
239
+
240
+ # Print the calculated impact. If a print hook is defined that
241
+ # returns true, then the default print function is skipped.
242
+ unless TasteTester::Hooks.print_impact(final_impact)
243
+ _print_impact(final_impact)
244
+ end
245
+ end
246
+
247
+ def self._find_changeset(repo)
248
+ # We want to compare changes in the current directory (working set) with
249
+ # the "most recent" commit in the VCS. For SVN, this will be the latest
250
+ # commit on the checked out repository (i.e. 'trunk'). Git/Hg may have
251
+ # different tags or labels assigned to the master branch, (i.e. 'master',
252
+ # 'stable', etc.) and should be configured if different than the default.
253
+ start_ref = case repo
254
+ when BetweenMeals::Repo::Svn
255
+ repo.latest_revision
256
+ when BetweenMeals::Repo::Git
257
+ TasteTester::Config.vcs_start_ref_git
258
+ when BetweenMeals::Repo::Hg
259
+ TasteTester::Config.vcs_start_ref_hg
260
+ end
261
+ end_ref = TasteTester::Config.vcs_end_ref
262
+
263
+ changeset = BetweenMeals::Changeset.new(
264
+ logger,
265
+ repo,
266
+ start_ref,
267
+ end_ref,
268
+ {
269
+ :cookbook_dirs =>
270
+ TasteTester::Config.relative_cookbook_dirs,
271
+ :role_dir =>
272
+ TasteTester::Config.relative_role_dir,
273
+ :databag_dir =>
274
+ TasteTester::Config.relative_databag_dir,
275
+ },
276
+ @track_symlinks,
277
+ )
278
+
279
+ return changeset
280
+ end
281
+
282
+ def self._find_roles(changes)
283
+ if TasteTester::Config.relative_cookbook_dirs.length > 1
284
+ logger.error('Knife deps does not support multiple cookbook paths.')
285
+ logger.error('Please flatten the cookbooks into a single directory' +
286
+ ' or override the impact_find_roles function.')
287
+ exit(1)
288
+ end
289
+
290
+ cookbooks = Set.new(changes.cookbooks)
291
+ roles = Set.new(changes.roles)
292
+ databags = Set.new(changes.databags)
293
+
294
+ if cookbooks.empty? && roles.empty?
295
+ logger.warn('No cookbooks or roles have been modified.')
296
+ return Set.new
297
+ end
298
+
299
+ unless cookbooks.empty?
300
+ logger.info('Modified Cookbooks:')
301
+ cookbooks.each { |cb| logger.info("\t#{cb}") }
302
+ end
303
+ unless roles.empty?
304
+ logger.info('Modified Roles:')
305
+ roles.each { |r| logger.info("\t#{r}") }
306
+ end
307
+ unless databags.empty?
308
+ logger.info('Modified Databags:')
309
+ databags.each { |db| logger.info("\t#{db}") }
310
+ end
311
+
312
+ # Use Knife to list the dependecies for each role in the roles directory.
313
+ # This creates a recursive tree structure that is then searched for
314
+ # instances of modified cookbooks. This can be slow since it must read
315
+ # every line of the Knife output, then search all roles for dependencies.
316
+ # If you have a custom way to calculate these reverse dependencies, this
317
+ # is the part you would replace.
318
+ logger.info('Finding dependencies (this may take a minute or two)...')
319
+ knife = Mixlib::ShellOut.new(
320
+ "knife deps /#{TasteTester::Config.role_dir}/*.rb" +
321
+ " --config #{TasteTester::Config.knife_config}" +
322
+ " --chef-repo-path #{TasteTester::Config.absolute_base_dir}" +
323
+ ' --tree --recurse',
324
+ )
325
+ knife.run_command
326
+ knife.error!
327
+
328
+ # Collapse the output from Knife into a hash structure that maps roles
329
+ # to the set of their dependencies. This will ignore duplicates in the
330
+ # Knife output, but must still process each line.
331
+ logger.info('Processing Dependencies...')
332
+ deps_hash = {}
333
+ curr_role = nil
334
+
335
+ knife.stdout.each_line do |line|
336
+ elem = line.rstrip
337
+ if elem.length == elem.lstrip.length
338
+ curr_role = elem
339
+ deps_hash[curr_role] = Set.new
340
+ else
341
+ deps_hash[curr_role].add(File.basename(elem, File.extname(elem)))
342
+ end
343
+ end
344
+
345
+ # Now we can search for modified dependencies by iterating over each
346
+ # role and checking the hash created earlier. Roles that have been
347
+ # modified directly are automatically included in the impacted set.
348
+ impacted_roles = Set.new(roles.map(&:name))
349
+ deps_hash.each do |role, deplist|
350
+ cookbooks.each do |cb|
351
+ if deplist.include?(cb.name)
352
+ impacted_roles.add(role)
353
+ logger.info("\tFound dependency: #{role} --> #{cb.name}")
354
+ break
355
+ end
356
+ end
357
+ end
358
+
359
+ return impacted_roles
360
+ end
361
+
362
+ def self._print_impact(final_impact)
363
+ if TasteTester::Config.json
364
+ puts JSON.pretty_generate(final_impact.to_a)
365
+ elsif final_impact.empty?
366
+ logger.warn('No impacted roles were found.')
367
+ else
368
+ logger.warn('The following roles have modified dependencies.' +
369
+ ' Please test a host in each of these roles.')
370
+ final_impact.each { |r| logger.warn("\t#{r}") }
371
+ end
372
+ end
185
373
  end
186
374
  end
@@ -37,6 +37,7 @@ module TasteTester
37
37
  config_file '/etc/taste-tester-config.rb'
38
38
  plugin_path nil
39
39
  chef_zero_path nil
40
+ bundle false
40
41
  verbosity Logger::WARN
41
42
  timestamp false
42
43
  user 'root'
@@ -51,6 +52,7 @@ module TasteTester
51
52
  timestamp_file '/etc/chef/test_timestamp'
52
53
  use_ssh_tunnels false
53
54
  ssh_command 'ssh'
55
+ ssh_connect_timeout 5
54
56
  use_ssl true
55
57
  chef_zero_logging true
56
58
  chef_config_path '/etc/chef'
@@ -58,6 +60,16 @@ module TasteTester
58
60
  my_hostname nil
59
61
  track_symlinks false
60
62
  transport 'ssh'
63
+ no_repo false
64
+ json false
65
+
66
+ # Start/End refs for calculating changes in the repo.
67
+ # - start_ref should be the "master" commit of the repository
68
+ # - end_ref should be nil to compare with the working set,
69
+ # or something like '.' to compare with the most recent commit
70
+ vcs_start_ref_git 'origin/HEAD'
71
+ vcs_start_ref_hg 'master'
72
+ vcs_end_ref nil
61
73
 
62
74
  skip_pre_upload_hook false
63
75
  skip_post_upload_hook false
@@ -97,6 +109,10 @@ module TasteTester
97
109
  end
98
110
  end
99
111
 
112
+ def self.absolute_base_dir
113
+ File.join(repo, base_dir)
114
+ end
115
+
100
116
  def self.chef_port
101
117
  require 'taste_tester/state'
102
118
  port_range = (
@@ -116,11 +132,8 @@ module TasteTester
116
132
  end
117
133
 
118
134
  def self.testing_end_time
119
- if TasteTester::Config.testing_until
120
- TasteTester::Config.testing_until
121
- else
135
+ TasteTester::Config.testing_until ||
122
136
  Time.now + TasteTester::Config.testing_time
123
- end
124
137
  end
125
138
  end
126
139
  end
@@ -22,5 +22,12 @@ module TasteTester
22
22
  end
23
23
  class NoOpError < StandardError
24
24
  end
25
+ class AlreadyTestingError < StandardError
26
+ attr_reader :username
27
+
28
+ def initialize(username)
29
+ @username = username
30
+ end
31
+ end
25
32
  end
26
33
  end
@@ -48,6 +48,21 @@ module TasteTester
48
48
  # Additional checks you want to do on the repo
49
49
  def self.repo_checks(_dryrun, _repo); end
50
50
 
51
+ # Find the set of roles dependent on the changed files.
52
+ # If returning something other than a set of roles, post_impact and/or
53
+ # print_impact should be specified to handle the output.
54
+ def self.impact_find_roles(_changes); end
55
+
56
+ # Do stuff after we find impacted roles
57
+ # This should return a Set object with the final impact. To return more
58
+ # complex data, you must also provide a print_impact function which returns
59
+ # true to override the default output.
60
+ def self.post_impact(_impacted_roles); end
61
+
62
+ # Customized the printed output of impact
63
+ # If this method returns true, the default output will not be printed.
64
+ def self.print_impact(_final_impact); end
65
+
51
66
  def self.get(file)
52
67
  path = File.expand_path(file)
53
68
  logger.warn("Loading plugin at #{path}")
@@ -23,12 +23,16 @@ require 'taste_tester/ssh'
23
23
  require 'taste_tester/noop'
24
24
  require 'taste_tester/locallink'
25
25
  require 'taste_tester/tunnel'
26
+ require 'taste_tester/exceptions'
26
27
 
27
28
  module TasteTester
28
29
  # Manage state of the remote node
29
30
  class Host
30
31
  include TasteTester::Logging
31
32
 
33
+ TASTE_TESTER_CONFIG = 'client-taste-tester.rb'.freeze
34
+ USER_PREAMBLE = '# TasteTester by '.freeze
35
+
32
36
  attr_reader :name
33
37
 
34
38
  def initialize(name, server)
@@ -94,22 +98,45 @@ module TasteTester
94
98
  @tunnel.run
95
99
  end
96
100
 
97
- @serialized_config = Base64.encode64(config).delete("\n")
101
+ serialized_config = Base64.encode64(config).delete("\n")
98
102
 
99
103
  # Then setup the testing
100
104
  transport = get_transport
101
105
 
106
+ # see if someone else is taste-testing
107
+ transport << we_testing
108
+
102
109
  transport << 'logger -t taste-tester Moving server into taste-tester' +
103
110
  " for #{@user}"
104
111
  transport << touchcmd
105
- transport << "/bin/echo -n '#{@serialized_config}' | base64 --decode" +
106
- " > #{TasteTester::Config.chef_config_path}/client-taste-tester.rb"
107
- transport << "rm -vf #{TasteTester::Config.chef_config_path}/" +
108
- TasteTester::Config.chef_config
109
- transport << "( ln -vs #{TasteTester::Config.chef_config_path}" +
110
- "/client-taste-tester.rb #{TasteTester::Config.chef_config_path}/" +
112
+ # shell redirection is also racy, so make a temporary file first
113
+ transport << "tmpconf=$(mktemp #{TasteTester::Config.chef_config_path}/" +
114
+ "#{TASTE_TESTER_CONFIG}.TMPXXXXXX)"
115
+ transport << "/bin/echo -n \"#{serialized_config}\" | base64 --decode" +
116
+ ' > "${tmpconf}"'
117
+ # then rename it to replace any existing file
118
+ transport << 'mv -f "${tmpconf}" ' +
119
+ "#{TasteTester::Config.chef_config_path}/#{TASTE_TESTER_CONFIG}"
120
+ transport << "( ln -vsf #{TasteTester::Config.chef_config_path}" +
121
+ "/#{TASTE_TESTER_CONFIG} #{TasteTester::Config.chef_config_path}/" +
111
122
  "#{TasteTester::Config.chef_config}; true )"
112
- transport.run!
123
+
124
+ # look again to see if someone else is taste-testing. This is where
125
+ # we work out if we won or lost a race with another user.
126
+ transport << we_testing
127
+
128
+ transport.run
129
+
130
+ case transport.status
131
+ when 0
132
+ # no problem, keep going.
133
+ nil
134
+ when 42
135
+ fail TasteTester::Exceptions::AlreadyTestingError,
136
+ transport.output.chomp
137
+ else
138
+ transport.error!
139
+ end
113
140
 
114
141
  # Then run any other stuff they wanted
115
142
  cmds = TasteTester::Hooks.test_remote_cmds(
@@ -117,7 +144,7 @@ module TasteTester
117
144
  @name,
118
145
  )
119
146
 
120
- if cmds && cmds.any?
147
+ if cmds&.any?
121
148
  transport = get_transport
122
149
  cmds.each { |c| transport << c }
123
150
  transport.run!
@@ -132,15 +159,12 @@ module TasteTester
132
159
  end
133
160
  config_prod = TasteTester::Config.chef_config.split('.').join('-prod.')
134
161
  [
135
- "rm -vf #{TasteTester::Config.chef_config_path}/" +
136
- TasteTester::Config.chef_config,
137
- "rm -vf #{TasteTester::Config.chef_config_path}/client-taste-tester.rb",
138
- "ln -vs #{TasteTester::Config.chef_config_path}/#{config_prod} " +
162
+ "ln -vsf #{TasteTester::Config.chef_config_path}/#{config_prod} " +
139
163
  "#{TasteTester::Config.chef_config_path}/" +
140
164
  TasteTester::Config.chef_config,
141
- "rm -vf #{TasteTester::Config.chef_config_path}/client.pem",
142
- "ln -vs #{TasteTester::Config.chef_config_path}/client-prod.pem " +
165
+ "ln -vsf #{TasteTester::Config.chef_config_path}/client-prod.pem " +
143
166
  "#{TasteTester::Config.chef_config_path}/client.pem",
167
+ "rm -vf #{TasteTester::Config.chef_config_path}/#{TASTE_TESTER_CONFIG}",
144
168
  "rm -vf #{TasteTester::Config.timestamp_file}",
145
169
  'logger -t taste-tester Returning server to production',
146
170
  ].each do |cmd|
@@ -149,43 +173,25 @@ module TasteTester
149
173
  transport.run!
150
174
  end
151
175
 
152
- def who_is_testing
153
- transport = get_transport
154
- transport << 'grep "^# TasteTester by"' +
155
- " #{TasteTester::Config.chef_config_path}/" +
156
- TasteTester::Config.chef_config
157
- output = transport.run
158
- if output.first.zero?
159
- user = output.last.match(/# TasteTester by (.*)$/)
160
- if user
161
- return user[1]
162
- end
163
- end
164
-
165
- # Legacy FB stuff, remove after migration. Safe for everyone else.
166
- transport = get_transport
167
- transport << "file #{TasteTester::Config.chef_config_path}/" +
176
+ def we_testing
177
+ config_file = "#{TasteTester::Config.chef_config_path}/" +
168
178
  TasteTester::Config.chef_config
169
- output = transport.run
170
- if output.first.zero?
171
- user = output.last.match(/client-(.*)-(taste-tester|test).rb/)
172
- if user
173
- return user[1]
174
- end
175
- end
176
-
177
- return nil
178
- end
179
-
180
- def in_test?
181
- transport = get_transport
182
- transport << "test -f #{TasteTester::Config.timestamp_file}"
183
- if transport.run.first.zero? && who_is_testing &&
184
- who_is_testing != ENV['USER']
185
- true
186
- else
187
- false
188
- end
179
+ # Look for signature of TasteTester
180
+ # 1. Look for USER_PREAMBLE line prefix
181
+ # 2. See if user is us, or someone else
182
+ # 3. if someone else is testing: emit username, exit with code 42 which
183
+ # short circuits the test verb
184
+ # This is written as a squiggly heredoc so the indentation of the awk is
185
+ # preserved. Later we remove the newlines to make it a bit easier to read.
186
+ shellcode = <<~ENDOFSHELLCODE
187
+ awk "\\$0 ~ /^#{USER_PREAMBLE}/{
188
+ if (\\$NF != \\"#{@user}\\"){
189
+ print \\$NF;
190
+ exit 42
191
+ }
192
+ }" #{config_file}
193
+ ENDOFSHELLCODE
194
+ shellcode.delete("\n")
189
195
  end
190
196
 
191
197
  def keeptesting
@@ -220,40 +226,90 @@ module TasteTester
220
226
  if TasteTester::Config.use_ssh_tunnels
221
227
  url = "#{scheme}://localhost:#{@tunnel.port}"
222
228
  else
223
- url = "#{scheme}://#{@server.host}"
229
+ url = +"#{scheme}://#{@server.host}"
224
230
  url << ":#{TasteTester::State.port}" if TasteTester::State.port
225
231
  end
226
- # rubocop:disable Metrics/LineLength
227
- ttconfig = <<-EOS
228
- # TasteTester by #{@user}
229
- # Prevent people from screwing up their permissions
230
- if Process.euid != 0
231
- puts 'Please run chef as root!'
232
- Process.exit!
233
- end
232
+ ttconfig = <<~ENDOFSCRIPT
233
+ #{USER_PREAMBLE}#{@user}
234
+ # Prevent people from screwing up their permissions
235
+ if Process.euid != 0
236
+ puts 'Please run chef as root!'
237
+ Process.exit!
238
+ end
234
239
 
235
- log_level :info
236
- log_location STDOUT
237
- chef_server_url '#{url}'
238
- ssl_verify_mode :verify_none
239
- ohai.plugin_path << '#{TasteTester::Config.chef_config_path}/ohai_plugins'
240
+ log_level :info
241
+ log_location STDOUT
242
+ ssl_verify_mode :verify_none
243
+ ohai.plugin_path << File.join('#{TasteTester::Config.chef_config_path}', 'ohai_plugins')
244
+ ENDOFSCRIPT
240
245
 
241
- EOS
242
- # rubocop:enable Metrics/LineLength
246
+ if TasteTester::Config.bundle
247
+ ttconfig += <<~ENDOFSCRIPT
248
+ taste_tester_dest = File.join(Dir.tmpdir, 'taste-tester')
249
+ puts 'INFO: Downloading bundle from #{url}...'
250
+ FileUtils.rmtree(taste_tester_dest)
251
+ FileUtils.mkpath(taste_tester_dest)
252
+ FileUtils.touch(File.join(taste_tester_dest, 'chefignore'))
253
+ uri = URI('#{url}/file_store/tt.tgz')
254
+ Net::HTTP.start(
255
+ uri.host,
256
+ uri.port,
257
+ :use_ssl => #{TasteTester::Config.use_ssl},
258
+ # we expect self signed certificates
259
+ :verify_mode => OpenSSL::SSL::VERIFY_NONE,
260
+ ) do |http|
261
+ http.request_get(uri) do |response|
262
+ # the use of stringIO means we are buffering the entire file in
263
+ # memory. This isn't very efficient, but it should work for
264
+ # most practical cases.
265
+ stream = Zlib::GzipReader.new(StringIO.new(response.body))
266
+ Gem::Package::TarReader.new(stream).each do |e|
267
+ dest = File.join(taste_tester_dest, e.full_name)
268
+ FileUtils.mkpath(File.dirname(dest))
269
+ if e.symlink?
270
+ File.symlink(e.header.linkname, dest)
271
+ else
272
+ File.open(dest, 'wb+') do |f|
273
+ # https://github.com/rubygems/rubygems/pull/2303
274
+ # IO.copy_stream(e, f)
275
+ # workaround:
276
+ f.write(e.read)
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
282
+ puts 'INFO: Download complete'
283
+ solo true
284
+ local_mode true
285
+ ENDOFSCRIPT
286
+ else
287
+ ttconfig += <<~ENDOFSCRIPT
288
+ chef_server_url '#{url}'
289
+ ENDOFSCRIPT
290
+ end
243
291
 
244
292
  extra = TasteTester::Hooks.test_remote_client_rb_extra_code(@name)
245
293
  if extra
246
- ttconfig += <<-EOS
247
- # Begin user-hook specified code
248
- #{extra}
249
- # End user-hook secified code
294
+ ttconfig += <<~ENDOFSCRIPT
295
+ # Begin user-hook specified code
296
+ #{extra}
297
+ # End user-hook secified code
250
298
 
251
- EOS
299
+ ENDOFSCRIPT
252
300
  end
253
301
 
254
- ttconfig += <<-EOS
255
- puts 'INFO: Running on #{@name} in taste-tester by #{@user}'
256
- EOS
302
+ ttconfig += <<~ENDOFSCRIPT
303
+ puts 'INFO: Running on #{@name} in taste-tester by #{@user}'
304
+ ENDOFSCRIPT
305
+
306
+ if TasteTester::Config.bundle
307
+ # This is last in the configuration file because it needs to override
308
+ # any values in test_remote_client_rb_extra_code
309
+ ttconfig += <<~ENDOFSCRIPT
310
+ chef_repo_path taste_tester_dest
311
+ ENDOFSCRIPT
312
+ end
257
313
  return ttconfig
258
314
  end
259
315
  end
@@ -22,7 +22,7 @@ module TasteTester
22
22
  include TasteTester::Logging
23
23
  include BetweenMeals::Util
24
24
 
25
- attr_reader :output
25
+ attr_reader :output, :status
26
26
 
27
27
  def initialize
28
28
  @host = 'localhost'
@@ -43,7 +43,11 @@ module TasteTester
43
43
  @status, @output = exec!(cmd, logger)
44
44
  rescue StandardError => e
45
45
  logger.error(e.message)
46
- raise TasteTester::Exceptions::LocalLinkError
46
+ error!
47
+ end
48
+
49
+ def error!
50
+ fail TasteTester::Exceptions::LocalLinkError
47
51
  end
48
52
 
49
53
  private
@@ -35,8 +35,8 @@ module TasteTester
35
35
  @logger ||= Logger.new(STDOUT)
36
36
  end
37
37
 
38
- def self.formatterproc=(p)
39
- @@formatter_proc = p
38
+ def self.formatterproc=(process)
39
+ @@formatter_proc = process
40
40
  end
41
41
 
42
42
  def self.use_log_formatter=(use_log_formatter)
@@ -49,6 +49,7 @@ module TasteTester
49
49
 
50
50
  def formatter
51
51
  return @@formatter_proc if @@formatter_proc
52
+
52
53
  if @@use_log_formatter
53
54
  proc do |severity, datetime, _progname, msg|
54
55
  if severity == 'ERROR'
@@ -58,7 +59,7 @@ module TasteTester
58
59
  end
59
60
  else
60
61
  proc do |severity, _datetime, _progname, msg|
61
- msg.to_s.prepend("#{severity}: ") unless severity == 'WARN'
62
+ msg.dup.to_s.prepend("#{severity}: ") unless severity == 'WARN'
62
63
  if severity == 'ERROR'
63
64
  msg = msg.to_s.red
64
65
  end