chef-apply 0.1.2

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +26 -0
  3. data/Gemfile.lock +423 -0
  4. data/LICENSE +201 -0
  5. data/README.md +41 -0
  6. data/Rakefile +32 -0
  7. data/bin/chef-run +23 -0
  8. data/chef-apply.gemspec +67 -0
  9. data/i18n/en.yml +513 -0
  10. data/lib/chef_apply.rb +20 -0
  11. data/lib/chef_apply/action/base.rb +158 -0
  12. data/lib/chef_apply/action/converge_target.rb +173 -0
  13. data/lib/chef_apply/action/install_chef.rb +30 -0
  14. data/lib/chef_apply/action/install_chef/base.rb +137 -0
  15. data/lib/chef_apply/action/install_chef/linux.rb +38 -0
  16. data/lib/chef_apply/action/install_chef/windows.rb +54 -0
  17. data/lib/chef_apply/action/reporter.rb +39 -0
  18. data/lib/chef_apply/cli.rb +470 -0
  19. data/lib/chef_apply/cli_options.rb +145 -0
  20. data/lib/chef_apply/config.rb +150 -0
  21. data/lib/chef_apply/error.rb +108 -0
  22. data/lib/chef_apply/errors/ccr_failure_mapper.rb +93 -0
  23. data/lib/chef_apply/file_fetcher.rb +70 -0
  24. data/lib/chef_apply/log.rb +42 -0
  25. data/lib/chef_apply/recipe_lookup.rb +117 -0
  26. data/lib/chef_apply/startup.rb +162 -0
  27. data/lib/chef_apply/status_reporter.rb +42 -0
  28. data/lib/chef_apply/target_host.rb +233 -0
  29. data/lib/chef_apply/target_resolver.rb +202 -0
  30. data/lib/chef_apply/telemeter.rb +162 -0
  31. data/lib/chef_apply/telemeter/patch.rb +32 -0
  32. data/lib/chef_apply/telemeter/sender.rb +121 -0
  33. data/lib/chef_apply/temp_cookbook.rb +159 -0
  34. data/lib/chef_apply/text.rb +77 -0
  35. data/lib/chef_apply/ui/error_printer.rb +261 -0
  36. data/lib/chef_apply/ui/plain_text_element.rb +75 -0
  37. data/lib/chef_apply/ui/terminal.rb +94 -0
  38. data/lib/chef_apply/version.rb +20 -0
  39. metadata +376 -0
@@ -0,0 +1,159 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2018 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "tmpdir"
19
+ require "fileutils"
20
+ require "chef_apply/log"
21
+ require "chef_apply/error"
22
+
23
+ module ChefApply
24
+ # This class knows how to create a local cookbook in a temp file, populate
25
+ # it with various recipes, attributes, config, etc. and delete it when the
26
+ # cookbook is no longer necessary
27
+ class TempCookbook
28
+ attr_reader :path
29
+
30
+ # We expect name to come in as a list of strings - resource/resource_name
31
+ # or cookbook/recipe combination
32
+ def initialize
33
+ @path = Dir.mktmpdir("cw")
34
+ end
35
+
36
+ def from_existing_recipe(existing_recipe_path)
37
+ ext_name = File.extname(existing_recipe_path)
38
+ raise UnsupportedExtension.new(ext_name) unless ext_name == ".rb"
39
+ cb = cookbook_for_recipe(existing_recipe_path)
40
+ if cb
41
+ # Full existing cookbook - only needs policyfile
42
+ ChefApply::Log.debug("Found full cookbook at path '#{cb[:path]}' and using recipe '#{cb[:recipe_name]}'")
43
+ name = cb[:name]
44
+ recipe_name = cb[:recipe_name]
45
+ FileUtils.cp_r(cb[:path], path)
46
+ # cp_r copies the whole existing cookbook into the tempdir so need to reset our path
47
+ @path = File.join(path, File.basename(cb[:path]))
48
+ generate_policyfile(name, recipe_name)
49
+ else
50
+ # Cookbook from single recipe not in a cookbook. We create the full cookbook
51
+ # structure including metadata, then generate policyfile. We set the cookbook
52
+ # name to the recipe name so hopefully this gives us better reporting info
53
+ # in the future
54
+ ChefApply::Log.debug("Found single recipe at path '#{existing_recipe_path}'")
55
+ recipe = File.basename(existing_recipe_path)
56
+ recipe_name = File.basename(existing_recipe_path, ext_name)
57
+ name = "cw_recipe"
58
+ recipes_dir = generate_recipes_dir
59
+ # This has the potential to break if they specify a recipe without a .rb
60
+ # extension, but lets wait to deal with that bug until we encounter it
61
+ FileUtils.cp(existing_recipe_path, File.join(recipes_dir, recipe))
62
+ generate_metadata(name)
63
+ generate_policyfile(name, recipe_name)
64
+ end
65
+ end
66
+
67
+ def from_resource(resource_type, resource_name, properties)
68
+ # Generate a cookbook containing a single default recipe with the specified
69
+ # resource in it. Incloud the resource type in the cookbook name so hopefully
70
+ # this gives us better reporting info in the future.
71
+ ChefApply::Log.debug("Generating cookbook for single resource '#{resource_type}[#{resource_name}]'")
72
+ name = "cw_#{resource_type}"
73
+ recipe_name = "default"
74
+ recipes_dir = generate_recipes_dir
75
+ File.open(File.join(recipes_dir, "#{recipe_name}.rb"), "w+") do |f|
76
+ f.print(create_resource(resource_type, resource_name, properties))
77
+ end
78
+ generate_metadata(name)
79
+ generate_policyfile(name, recipe_name)
80
+ end
81
+
82
+ def delete
83
+ FileUtils.remove_entry path
84
+ end
85
+
86
+ def cookbook_for_recipe(existing_recipe_path)
87
+ metadata = File.expand_path(File.join(existing_recipe_path, "../../metadata.rb"))
88
+ if File.file?(metadata)
89
+ require "chef/cookbook/metadata"
90
+ m = Chef::Cookbook::Metadata.new
91
+ m.from_file(metadata)
92
+ {
93
+ name: m.name,
94
+ recipe_name: File.basename(existing_recipe_path, File.extname(existing_recipe_path)),
95
+ path: File.expand_path(File.join(metadata, "../"))
96
+ }
97
+ else
98
+ nil
99
+ end
100
+ end
101
+
102
+ def generate_recipes_dir
103
+ recipes_path = File.join(path, "recipes")
104
+ FileUtils.mkdir_p(recipes_path)
105
+ recipes_path
106
+ end
107
+
108
+ def generate_metadata(name)
109
+ metadata_file = File.join(path, "metadata.rb")
110
+ File.open(metadata_file, "w+") do |f|
111
+ f.print("name \"#{name}\"\n")
112
+ end
113
+ metadata_file
114
+ end
115
+
116
+ def generate_policyfile(name, recipe_name)
117
+ policy_file = File.join(path, "Policyfile.rb")
118
+ if File.exist?(policy_file)
119
+ File.open(policy_file, "a") do |f|
120
+ # We override the specified run_list with the run_list we want.
121
+ # We append and put this at the end of the file so it overrides
122
+ # any specified run_list.
123
+ f.print("\n# Overriding run_list with command line specified value\n")
124
+ f.print("run_list \"#{name}::#{recipe_name}\"\n")
125
+ end
126
+ else
127
+ File.open(policy_file, "w+") do |f|
128
+ f.print("name \"#{name}_policy\"\n")
129
+ ChefApply::Config.chef.cookbook_repo_paths.each do |p|
130
+ f.print("default_source :chef_repo, \"#{p}\"\n")
131
+ end
132
+ f.print("default_source :supermarket\n")
133
+ f.print("run_list \"#{name}::#{recipe_name}\"\n")
134
+ f.print("cookbook \"#{name}\", path: \".\"\n")
135
+ end
136
+ end
137
+ policy_file
138
+ end
139
+
140
+ def create_resource(resource_type, resource_name, properties)
141
+ r = "#{resource_type} '#{resource_name}'"
142
+ # lets format the properties into the correct syntax Chef expects
143
+ unless properties.empty?
144
+ r += " do\n"
145
+ properties.each do |k, v|
146
+ v = "'#{v}'" if v.is_a? String
147
+ r += " #{k} #{v}\n"
148
+ end
149
+ r += "end"
150
+ end
151
+ r += "\n"
152
+ r
153
+ end
154
+
155
+ class UnsupportedExtension < ChefApply::ErrorNoLogs
156
+ def initialize(ext); super("CHEFVAL009", ext); end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,77 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "r18n-desktop"
19
+
20
+ # A very thin wrapper around R18n, the idea being that we're likely to replace r18n
21
+ # down the road and don't want to have to change all of our commands.
22
+ module ChefApply
23
+ class Text
24
+ R18n.from_env(File.join(File.dirname(__FILE__), "../..", "i18n/"))
25
+ t = R18n.get.t
26
+ t.translation_keys.each do |k|
27
+ k = k.to_sym
28
+ define_singleton_method k do |*args|
29
+ TextWrapper.new(t.send(k, *args))
30
+ end
31
+ end
32
+ end
33
+
34
+ # Our text spinner class really doesn't like handling the TranslatedString or Untranslated classes returned
35
+ # by the R18n library. So instead we return these TextWrapper instances which have dynamically defined methods
36
+ # corresponding to the known structure of the R18n text file. Most importantly, if a user has accessed
37
+ # a leaf node in the code we return a regular String instead of the R18n classes.
38
+ class TextWrapper
39
+ def initialize(translation_tree)
40
+ @tree = translation_tree
41
+ @tree.translation_keys.each do |k|
42
+ k = k.to_sym
43
+ define_singleton_method k do |*args|
44
+ subtree = @tree.send(k, *args)
45
+ if subtree.translation_keys.empty?
46
+ # If there are no more possible children, just return the translated value
47
+ subtree.to_s
48
+ else
49
+ TextWrapper.new(subtree)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def method_missing(name, *args)
56
+ raise InvalidKey.new(@tree.instance_variable_get(:@path), name)
57
+ end
58
+
59
+ class InvalidKey < RuntimeError
60
+ def initialize(path, terminus)
61
+ line = caller(3, 1).first # 1 - TextWrapper.method_missing
62
+ # 2 - TextWrapper.initialize
63
+ # 3 - actual caller
64
+ if line =~ /.*\/lib\/(.*\.rb):(\d+)/
65
+ line = "File: #{$1} Line: #{$2}"
66
+ end
67
+
68
+ # Calling back into Text here seems icky, this is an error
69
+ # that only engineering should see.
70
+ message = "i18n key #{path}.#{terminus} does not exist.\n"
71
+ message << "Referenced from #{line}"
72
+ super(message)
73
+ end
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,261 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "train/errors"
19
+ require "pastel"
20
+ require "chef_apply/error"
21
+ require "chef_apply/config"
22
+ require "chef_apply/text"
23
+ require "chef_apply/ui/terminal"
24
+
25
+ module ChefApply::UI
26
+ class ErrorPrinter
27
+ attr_reader :id, :pastel, :show_log, :show_stack, :exception, :target_host
28
+ # TODO define 't' as a method is a temporary workaround
29
+ # to ensure that text key lookups are testable.
30
+ def t
31
+ ChefApply::Text.errors
32
+ end
33
+
34
+ DEFAULT_ERROR_NO = "CHEFINT001"
35
+
36
+ def self.show_error(e)
37
+ # Name is misleading - it's unwrapping but also doing further
38
+ # error resolution for common errors:
39
+ unwrapped = ChefApply::StandardErrorResolver.unwrap_exception(e)
40
+ if unwrapped.class == ChefApply::MultiJobFailure
41
+ capture_multiple_failures(unwrapped)
42
+ end
43
+ formatter = ErrorPrinter.new(e, unwrapped)
44
+ Terminal.output(formatter.format_error)
45
+ rescue => e
46
+ dump_unexpected_error(e)
47
+ end
48
+
49
+ def self.capture_multiple_failures(e)
50
+ out_file = ChefApply::Config.error_output_path
51
+ e.params << out_file # Tell the operator where to find this info
52
+ File.open(out_file, "w") do |out|
53
+ e.jobs.each do |j|
54
+ wrapped = ChefApply::StandardErrorResolver.wrap_exception(j.exception, j.target_host)
55
+ ep = ErrorPrinter.new(wrapped)
56
+ msg = ep.format_body().tr("\n", " ").gsub(/ {2,}/, " ").chomp.strip
57
+ out.write("Host: #{j.target_host.hostname} ")
58
+ if ep.exception.respond_to? :id
59
+ out.write("Error: #{ep.exception.id}: ")
60
+ else
61
+ out.write(": ")
62
+ end
63
+ out.write("#{msg}\n")
64
+ end
65
+ end
66
+ end
67
+
68
+ def self.write_backtrace(e, args)
69
+ formatter = ErrorPrinter.new(e)
70
+ out = StringIO.new
71
+ formatter.add_backtrace_header(out, args)
72
+ formatter.add_formatted_backtrace(out)
73
+ formatter.save_backtrace(out)
74
+ rescue => ex
75
+ dump_unexpected_error(ex)
76
+ end
77
+
78
+ # Use this to dump an an exception to output. useful
79
+ # if an error occurs in the error handling itself.
80
+ def self.dump_unexpected_error(e)
81
+ Terminal.output "INTERNAL ERROR"
82
+ Terminal.output "-=" * 30
83
+ Terminal.output "Message:"
84
+ Terminal.output e.message if e.respond_to?(:message)
85
+ Terminal.output "Backtrace:"
86
+ Terminal.output e.backtrace if e.respond_to?(:backtrace)
87
+ Terminal.output "=-" * 30
88
+ end
89
+
90
+ def initialize(wrapper, unwrapped = nil, target_host = nil)
91
+ @exception = unwrapped || wrapper.contained_exception
92
+ @target_host = wrapper.target_host || target_host
93
+ @pastel = Pastel.new
94
+ @show_log = exception.respond_to?(:show_log) ? exception.show_log : true
95
+ @show_stack = exception.respond_to?(:show_stack) ? exception.show_stack : true
96
+ @content = StringIO.new
97
+ @command = exception.respond_to?(:command) ? exception.command : nil
98
+ @id = DEFAULT_ERROR_NO
99
+ if exception.respond_to?(:id) && exception.id =~ /CHEF.*/
100
+ @id = exception.id
101
+ end
102
+ if exception.respond_to?(:decorate)
103
+ @decorate = exception.decorate
104
+ else
105
+ @decorate = true
106
+ end
107
+ rescue => e
108
+ ErrorPrinter.dump_unexpected_error(e)
109
+ exit! 128
110
+ end
111
+
112
+ def format_error
113
+ if @decorate
114
+ format_decorated
115
+ else
116
+ format_undecorated
117
+ end
118
+ @content.string
119
+ end
120
+
121
+ def format_undecorated
122
+ @content << "\n"
123
+ @content << format_body()
124
+ if @command
125
+ @content << "\n"
126
+ @content << @command.usage
127
+ end
128
+ end
129
+
130
+ def format_decorated
131
+ @content << "\n"
132
+ @content << format_header()
133
+ @content << "\n\n"
134
+ @content << format_body()
135
+ @content << "\n"
136
+ @content << format_footer()
137
+ @content << "\n"
138
+ end
139
+
140
+ def format_header
141
+ pastel.decorate(@id, :bold)
142
+ end
143
+
144
+ def format_body
145
+ if exception.kind_of? ChefApply::Error
146
+ format_workstation_exception
147
+ elsif exception.kind_of? Train::Error
148
+ format_train_exception
149
+ else
150
+ format_other_exception
151
+ end
152
+ end
153
+
154
+ def format_footer
155
+ if show_log
156
+ if show_stack
157
+ t.footer.both(ChefApply::Config.log.location,
158
+ ChefApply::Config.stack_trace_path)
159
+ else
160
+ t.footer.log_only(ChefApply::Config.log.location)
161
+ end
162
+ else
163
+ if show_stack
164
+ t.footer.stack_only
165
+ else
166
+ t.footer.neither
167
+ end
168
+ end
169
+ end
170
+
171
+ def add_backtrace_header(out, args)
172
+ out.write("\n#{"-" * 80}\n")
173
+ out.print("#{Time.now}: Error encountered while running the following:\n")
174
+ out.print(" #{args.join(' ')}\n")
175
+ out.print("Backtrace:\n")
176
+ end
177
+
178
+ def save_backtrace(output)
179
+ File.open(ChefApply::Config.stack_trace_path, "ab+") do |f|
180
+ f.write(output.string)
181
+ end
182
+ end
183
+
184
+ def self.error_summary(e)
185
+ if e.kind_of? ChefApply::Error
186
+ # By convention, all of our defined messages have a short summary on the first line.
187
+ ChefApply::Text.errors.send(e.id, *e.params).split("\n").first
188
+ elsif e.kind_of? String
189
+ e
190
+ else
191
+ if e.respond_to? :message
192
+ e.message
193
+ else
194
+ ChefApply::Text.errors.UNKNOWN
195
+ end
196
+ end
197
+ end
198
+
199
+ def format_workstation_exception
200
+ params = exception.params
201
+ t.send(@id, *params)
202
+ end
203
+
204
+ def format_train_exception
205
+ backend, host = formatted_host()
206
+ if host.nil?
207
+ t.CHEFTRN002(exception.message)
208
+ else
209
+ t.CHEFTRN001(backend, host, exception.message)
210
+ end
211
+ end
212
+
213
+ def format_other_exception
214
+ t.send(DEFAULT_ERROR_NO, exception.message)
215
+ end
216
+
217
+ def formatted_host
218
+ return nil if target_host.nil?
219
+ cfg = target_host.config
220
+ port = cfg[:port].nil? ? "" : ":#{cfg[:port]}"
221
+ user = cfg[:user].nil? ? "" : "#{cfg[:user]}@"
222
+ "#{user}#{target_host.hostname}#{port}"
223
+ end
224
+
225
+ # mostly copied from
226
+ # https://gist.github.com/stanio/13d74294ca1868fed7fb
227
+ def add_formatted_backtrace(out)
228
+ _format_single(out, exception)
229
+ current_backtrace = exception.backtrace
230
+ cause = exception.cause
231
+ until cause.nil?
232
+ cause_trace = _unique_trace(cause.backtrace.to_a, current_backtrace)
233
+ out.print "Caused by: "
234
+ _format_single(out, cause, cause_trace)
235
+ backtrace_length = cause.backtrace.length
236
+ if backtrace_length > cause_trace.length
237
+ out.print "\t... #{backtrace_length - cause_trace.length} more"
238
+ end
239
+ out.print "\n"
240
+ current_backtrace = cause.backtrace
241
+ cause = cause.cause
242
+ end
243
+ end
244
+
245
+ def _format_single(out, exception, backtrace = nil)
246
+ out.puts "#{exception.class}: #{exception.message}"
247
+ backtrace ||= exception.backtrace.to_a
248
+ backtrace.each { |trace| out.puts "\t#{trace}" }
249
+ end
250
+
251
+ def _unique_trace(backtrace1, backtrace2)
252
+ i = 1
253
+ while i <= backtrace1.size && i <= backtrace2.size
254
+ break if backtrace1[-i] != backtrace2[-i]
255
+ i += 1
256
+ end
257
+ backtrace1[0..-i]
258
+ end
259
+ end
260
+
261
+ end