navo 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3adda44205c06f6444cdc4969561ce867498cc6f
4
- data.tar.gz: a8261f98b2e072aab9451df37b558c4b2376308a
3
+ metadata.gz: 6e19118f65db1a7db3490eb3ce679b05d9c93dd4
4
+ data.tar.gz: 28fdb3567c3f9b883e54a88c258e3e5776f6f5b3
5
5
  SHA512:
6
- metadata.gz: ec8ecc5aee52a8c00aeaab4ffed1062f613f71a85b808327d1c0dd62e9d0326793a5e1cbe4f49a1628d46038b0b050e8ddc7884b90a1d9c9cc6dae39a4fa02a3
7
- data.tar.gz: 940a3980f2acae81e2353e7c57d2a79d523bb7fa2d572f6b28a9c28bc8bdc1d96e02339aa55e0e5e0b97fca54387c1ee2bcffd793a2b2e4f5175bad72cc178e4
6
+ metadata.gz: 8df3881c54fbba49a9bcabd1e1fce93728407f7302f8a115091cf89f25897fdfbd189c24212b237207cd5bc3d8499fe69d1dc48bd8f45583890990486ec4bb0c
7
+ data.tar.gz: 59849b6754981ccf3526f10ec0f893b1f8f52018fb29fa5826a163c5a051a4681ef297a49d92354933302dc8b46468e323792e0b5f6b3860d77424473095154d
@@ -1,8 +1,26 @@
1
1
  require 'navo/constants'
2
2
  require 'navo/errors'
3
3
  require 'navo/configuration'
4
+ require 'navo/logger'
4
5
  require 'navo/sandbox'
5
6
  require 'navo/suite'
6
- require 'navo/suite_state'
7
+ require 'navo/berksfile'
8
+ require 'navo/state_file'
7
9
  require 'navo/utils'
8
10
  require 'navo/version'
11
+
12
+ module Navo
13
+ class << self
14
+ attr_accessor :mutex
15
+
16
+ def synchronize
17
+ mutex.synchronize do
18
+ yield
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ Navo.mutex = Mutex.new
25
+
26
+ Excon.defaults[:read_timeout] = 600
@@ -0,0 +1,58 @@
1
+ require 'fileutils'
2
+
3
+ module Navo
4
+ # A global Berksfile to be shared amongst all threads.
5
+ #
6
+ # This synchronizes access so we don't have multiple threads doing the same
7
+ # work resolving cookbooks.
8
+ class Berksfile
9
+ class << self
10
+ attr_accessor :path
11
+ attr_accessor :config
12
+
13
+ def load
14
+ require 'berkshelf' # Lazily require so we don't have to load for every command
15
+ berksfile
16
+ end
17
+
18
+ def install(logger: nil)
19
+ if @installed
20
+ logger.info 'Berksfile cookbooks already resolved'
21
+ return
22
+ end
23
+
24
+ logger.info 'Installing Berksfile...'
25
+ Berkshelf.logger = Celluloid.logger = logger
26
+ Berkshelf.ui.mute { Berkshelf::Installer.new(berksfile).run }
27
+ Celluloid.logger = nil # Ignore annoying shutdown messages
28
+
29
+ @installed = true
30
+ end
31
+
32
+ def vendor(logger:)
33
+ Berkshelf.logger = Celluloid.logger = logger
34
+ Berkshelf.ui.mute { berksfile.vendor(vendor_directory) }
35
+ Celluloid.logger = nil # Ignore annoying shutdown messages
36
+ end
37
+
38
+ def cache_directory
39
+ Berkshelf::CookbookStore.default_path
40
+ end
41
+
42
+ def vendor_directory
43
+ @vendor_directory ||=
44
+ FileUtils.mkdir_p(File.join(config.repo_root, %w[.navo vendored-cookbooks])).first
45
+ end
46
+
47
+ def lockfile_path
48
+ berksfile.lockfile.filepath
49
+ end
50
+
51
+ private
52
+
53
+ def berksfile
54
+ @berksfile ||= Berkshelf::Berksfile.from_file(@path)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,278 @@
1
+ class Chef
2
+ module Formatters
3
+ class NavoFormatter < Formatters::Base
4
+ cli_name(:navo)
5
+
6
+ def initialize(out, err)
7
+ super
8
+
9
+ @up_to_date_resources = 0
10
+ @updated_resources = 0
11
+ @skipped_resources = 0
12
+ @resource_stack = []
13
+ @resource_action_times = Hash.new { |hash, key| hash[key] = [] }
14
+
15
+ @deprecations = {}
16
+ end
17
+
18
+ def total_resources
19
+ @up_to_date_resources + @updated_resources + @skipped_resources
20
+ end
21
+
22
+ def print_deprecations
23
+ return if @deprecations.empty?
24
+ puts_line 'Deprecated features used!'
25
+
26
+ @deprecations.each do |message, locations|
27
+ if locations.size == 1
28
+ puts_line " #{message} at one location:"
29
+ else
30
+ puts_line " #{message} at #{locations.size} locations:"
31
+ end
32
+ locations.each do |location|
33
+ prefix = ' - '
34
+ Array(location).each do |line|
35
+ puts_line "#{prefix}#{line}"
36
+ prefix = ' '
37
+ end
38
+ end
39
+ end
40
+ puts_line ''
41
+ end
42
+
43
+ def run_start(version)
44
+ @start_time = Time.now
45
+ puts_line "Starting Chef client #{version}...", :cyan
46
+ end
47
+
48
+ def ohai_completed(node)
49
+ puts_line ''
50
+ puts_line 'Ohai run completed', :cyan
51
+ end
52
+
53
+ def library_load_start(file_count)
54
+ @load_start_time = Time.now
55
+ puts_line ''
56
+ puts_line 'Loading cookbook libraries...', :cyan
57
+ end
58
+
59
+ def library_load_complete
60
+ elapsed = Time.now - @load_start_time
61
+ puts_line "Loaded cookbook libraries (#{elapsed}s)", :cyan
62
+ end
63
+
64
+ def attribute_load_start(file_count)
65
+ @load_start_time = Time.now
66
+ puts_line ''
67
+ puts_line 'Loading cookbook attributes...', :cyan
68
+ end
69
+
70
+ def attribute_load_complete
71
+ elapsed = Time.now - @load_start_time
72
+ puts_line "Loaded cookbook attributes (#{elapsed}s)", :cyan
73
+ end
74
+
75
+ def lwrp_load_start(file_count)
76
+ @load_start_time = Time.now
77
+ puts_line ''
78
+ puts_line 'Loading custom resources...', :cyan
79
+ end
80
+
81
+ def lwrp_load_complete
82
+ elapsed = Time.now - @load_start_time
83
+ puts_line "Loaded custom resources (#{elapsed}s)", :cyan
84
+ end
85
+
86
+ def definition_load_start(file_count)
87
+ @load_start_time = Time.now
88
+ puts_line ''
89
+ puts_line 'Loading definitions...', :cyan
90
+ end
91
+
92
+ def definition_load_complete
93
+ elapsed = Time.now - @load_start_time
94
+ puts_line "Loaded definitions (#{elapsed}s)", :cyan
95
+ end
96
+
97
+ def recipe_load_start(recipes)
98
+ @load_start_time = Time.now
99
+ puts_line ''
100
+ puts_line "Loading recipes...", :cyan
101
+ end
102
+
103
+ def recipe_load_complete
104
+ elapsed = Time.now - @load_start_time
105
+ puts_line "Recipes loaded (#{elapsed}s)", :cyan
106
+ end
107
+
108
+ def file_loaded(path)
109
+ puts_line "Loaded #{path}"
110
+ end
111
+
112
+ def converge_start(run_context)
113
+ puts_line ''
114
+ puts_line "Converging #{run_context.resource_collection.all_resources.size} resources..."
115
+ end
116
+
117
+ def converge_complete
118
+ unindent while @resource_stack.pop
119
+ puts_line ''
120
+ puts_line 'Converge completed', :green
121
+ end
122
+
123
+ def converge_failed(e)
124
+ unindent while @resource_stack.pop
125
+ puts_line ''
126
+ puts_line "Converge failed: #{e}", :red
127
+ end
128
+
129
+ def resource_action_start(resource, action, notification_type = nil, notifier = nil)
130
+ # Track the current recipe so we update it only when it changes
131
+ # (i.e. when descending into another recipe via include_recipe)
132
+ if resource.cookbook_name && resource.recipe_name
133
+ current_recipe = "#{resource.cookbook_name}::#{resource.recipe_name}"
134
+
135
+ unless current_recipe == @current_recipe
136
+ @current_recipe = current_recipe
137
+ puts_line current_recipe, :magenta
138
+ end
139
+ end
140
+
141
+ # Record the resource and the time we started so we can figure out how
142
+ # long it took to complete
143
+ @resource_stack << [resource, Time.now]
144
+ indent
145
+
146
+ puts_line "#{resource} action #{action}"
147
+ end
148
+
149
+ def resource_failed_retriable(resource, action, retry_count, exception)
150
+ puts_line "#{resource} action #{action} FAILED; retrying...", :yellow
151
+ end
152
+
153
+ # Called when a resource fails and will not be retried.
154
+ def resource_failed(resource, action, exception)
155
+ _, start_time = @resource_stack.pop
156
+ elapsed = Time.now - start_time
157
+ @resource_action_times[[resource.to_s, action.to_s]] << elapsed
158
+ puts_line "#{resource} action #{action} (#{elapsed}s) FAILED: #{exception}", :red
159
+ unindent
160
+ end
161
+
162
+ def resource_skipped(resource, action, conditional)
163
+ @skipped_resources += 1
164
+ _, start_time = @resource_stack.pop
165
+ elapsed = Time.now - start_time
166
+ @resource_action_times[[resource.to_s, action.to_s]] << elapsed
167
+ puts_line "#{resource} action #{action} (#{elapsed}s) SKIPPED due to: #{conditional.short_description}"
168
+ unindent
169
+ end
170
+
171
+ def resource_up_to_date(resource, action)
172
+ @up_to_date_resources += 1
173
+ _, start_time = @resource_stack.pop
174
+ elapsed = Time.now - start_time
175
+ @resource_action_times[[resource.to_s, action.to_s]] << elapsed
176
+ puts_line "#{resource} action #{action} up-to-date (#{elapsed}s)"
177
+ unindent
178
+ end
179
+
180
+ def resource_update_applied(resource, action, update)
181
+ indent
182
+
183
+ Array(update).compact.each do |line|
184
+ if line.is_a?(String)
185
+ puts_line "- #{line}", :green
186
+ elsif line.is_a?(Array)
187
+ # Expanded output delta
188
+ line.each do |detail|
189
+ if detail =~ /^\+(?!\+\+ )/
190
+ color = :green
191
+ elsif detail =~ /^-(?!-- )/
192
+ color = :red
193
+ else
194
+ color = :cyan
195
+ end
196
+ puts_line detail, color
197
+ end
198
+ end
199
+ end
200
+
201
+ unindent
202
+ end
203
+
204
+ def resource_updated(resource, action)
205
+ @updated_resources += 1
206
+ _, start_time = @resource_stack.pop
207
+ elapsed = Time.now - start_time
208
+ @resource_action_times[[resource.to_s, action.to_s]] << elapsed
209
+ puts_line "#{resource} action #{action} updated (#{elapsed}s)"
210
+ unindent
211
+ end
212
+
213
+ def handlers_start(handler_count)
214
+ @handler_count = handler_count
215
+ puts_line ''
216
+ if @handler_count > 0
217
+ puts_line "Running #{handler_count} handlers:", :cyan
218
+ else
219
+ puts_line 'No registered handlers to run', :cyan
220
+ end
221
+ end
222
+
223
+ def handler_executed(handler)
224
+ puts_line "- #{handler.class.name}"
225
+ end
226
+
227
+ def handlers_completed
228
+ puts_line 'Running handlers complete' unless @handler_count == 0
229
+ end
230
+
231
+ def deprecation(message, location=caller(2..2)[0])
232
+ if Chef::Config[:treat_deprecation_warnings_as_errors]
233
+ super
234
+ end
235
+
236
+ # Save deprecations to the screen until the end
237
+ @deprecations[message] ||= Set.new
238
+ @deprecations[message] << location
239
+ end
240
+
241
+ def run_completed(node)
242
+ @end_time = Time.now
243
+
244
+ print_deprecations
245
+
246
+ puts_line ''
247
+ print_resource_summary
248
+ puts_line "Chef client finished in #{@end_time - @start_time} seconds", :cyan
249
+ end
250
+
251
+ private
252
+
253
+ def print_resource_summary
254
+ puts_line "#{@updated_resources}/#{total_resources} resources updated"
255
+
256
+ slowest_resources = @resource_action_times.sort_by do |key, values|
257
+ -values.inject(:+)
258
+ end
259
+
260
+ puts_line 'Slowest resource actions:'
261
+ slowest_resources[0...10].each do |key, values|
262
+ elapsed = '%-.3fs' % values.inject(:+)
263
+ puts_line "#{elapsed.ljust(8)} #{key.first} (#{key[1]})"
264
+ end
265
+
266
+ puts_line ''
267
+ end
268
+
269
+ def indent
270
+ indent_by 2
271
+ end
272
+
273
+ def unindent
274
+ indent_by -2
275
+ end
276
+ end
277
+ end
278
+ end
@@ -1,40 +1,58 @@
1
- require 'berkshelf'
2
1
  require 'navo'
2
+ require 'parallel'
3
3
  require 'thor'
4
4
 
5
5
  module Navo
6
6
  # Command line application interface.
7
7
  class CLI < Thor
8
- desc 'create', 'create a container for test suite(s) to run within'
9
- def create(pattern = nil)
10
- exit suites_for(pattern).map(&:create).all? ? 0 : 1
8
+ def initialize(*args)
9
+ super
10
+ Navo::Logger.output = STDOUT
11
+ STDOUT.sync = true
12
+ Navo::Logger.level = config['log-level']
11
13
  end
12
14
 
13
- desc 'converge', 'run Chef for test suite(s)'
14
- def converge(pattern = nil)
15
- exit suites_for(pattern).map(&:converge).all? ? 0 : 1
16
- end
17
-
18
- desc 'verify', 'run test suite(s)'
19
- def verify(pattern = nil)
20
- exit suites_for(pattern).map(&:verify).all? ? 0 : 1
21
- end
15
+ {
16
+ create: 'create a container for test suite(s) to run within',
17
+ converge: 'run Chef for test suite(s)',
18
+ verify: 'run test suites(s)',
19
+ test: 'converge and run test suites(s)',
20
+ destroy: 'clean up test suite(s)',
21
+ }.each do |action, description|
22
+ desc "#{action} [suite|regexp]", description
23
+ option :concurrency,
24
+ aliases: '-c',
25
+ type: :numeric,
26
+ default: Parallel.processor_count,
27
+ desc: 'Execute up to the specified number of test suites concurrently'
28
+ option 'log-level',
29
+ aliases: '-l',
30
+ type: :string,
31
+ desc: 'Set the log output verbosity level'
22
32
 
23
- desc 'test', 'converge and run test suite(s)'
24
- def test(pattern = nil)
25
- exit suites_for(pattern).map(&:test).all? ? 0 : 1
26
- end
33
+ if action == :test
34
+ option 'destroy',
35
+ aliases: '-d',
36
+ type: :string,
37
+ desc: 'Destroy strategy to use after testing (passing, always, never)'
38
+ end
27
39
 
28
- desc 'destroy', 'clean up test suite(s)'
29
- def destroy(pattern = nil)
30
- exit suites_for(pattern).map(&:destroy).all? ? 0 : 1
40
+ define_method(action) do |*args|
41
+ apply_flags_to_config!
42
+ execute(action, *args)
43
+ end
31
44
  end
32
45
 
33
46
  desc 'login', "open a shell inside a suite's container"
34
47
  def login(pattern)
48
+ apply_flags_to_config!
49
+
35
50
  suites = suites_for(pattern)
36
- if suites.size > 1
37
- puts 'Pattern matched more than one test suite'
51
+ if suites.size == 0
52
+ logger.console "Pattern '#{pattern}' matched no test suites", severity: :error
53
+ exit 1
54
+ elsif suites.size > 1
55
+ logger.console "Pattern '#{pattern}' matched more than one test suite", severity: :error
38
56
  exit 1
39
57
  else
40
58
  suites.first.login
@@ -47,13 +65,52 @@ module Navo
47
65
  @config ||= Configuration.load_applicable
48
66
  end
49
67
 
68
+ def logger
69
+ @logger ||= Navo::Logger.new
70
+ end
71
+
50
72
  def suites_for(pattern)
51
73
  suite_names = config['suites'].keys
52
74
  suite_names.select! { |name| name =~ /#{pattern}/ } if pattern
53
75
 
54
76
  suite_names.map do |suite_name|
55
- Suite.new(name: suite_name, config: config)
77
+ Suite.new(name: suite_name, config: config, global_state: @global_state)
56
78
  end
57
79
  end
80
+
81
+ def apply_flags_to_config!
82
+ config['log-level'] = options['log-level'] if options['log-level']
83
+ Navo::Logger.level = config['log-level']
84
+ config['concurrency'] = options['concurrency'] if options['concurrency']
85
+ config['destroy'] = options.fetch('destroy', 'passing')
86
+
87
+ # Initialize here so config is correctly set
88
+ Berksfile.path = File.expand_path(config['chef']['berksfile'], config.repo_root)
89
+ Berksfile.config = config
90
+ @global_state = StateFile.new(file: File.join(config.repo_root, %w[.navo global-state.yaml]),
91
+ logger: logger).tap(&:load)
92
+ end
93
+
94
+ def execute(action, pattern = nil)
95
+ suites = suites_for(pattern)
96
+ results = Parallel.map(suites, in_threads: config['concurrency']) do |suite|
97
+ succeeded = suite.send(action)
98
+ [succeeded, suite]
99
+ end
100
+
101
+ failures = results.reject { |succeeded, result| succeeded }
102
+ failures.each do |_, suite|
103
+ logger.console("Failed to #{action} #{suite.name}", severity: :error)
104
+ logger.console("See #{suite.log_file} for full log output", severity: :error)
105
+ end
106
+
107
+ exit failures.any? ? 1 : 0
108
+ rescue Interrupt
109
+ # Handle Ctrl-C
110
+ logger.console('INTERRUPTED', severity: :warn)
111
+ rescue => ex
112
+ logger.console("#{ex.class}: #{ex.message}", severity: :fatal)
113
+ logger.console(ex.backtrace.join("\n"), severity: :fatal)
114
+ end
58
115
  end
59
116
  end