chef-apply 0.1.2

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