Class: Chef::Cookbook::SyntaxCheck

Inherits:
Object
  • Object
show all
Includes:
Mixin::Checksum, Mixin::ShellOut
Defined in:
lib/chef/cookbook/syntax_check.rb

Overview

Chef::Cookbook::SyntaxCheck

Encapsulates the process of validating the ruby syntax of files in Chef cookbooks.

Defined Under Namespace

Classes: PersistentSet

Constant Summary

Constants included from Mixin::ShellOut

Mixin::ShellOut::DEPRECATED_OPTIONS

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Mixin::Checksum

#checksum

Methods included from Mixin::ShellOut

#run_command_compatible_options, #shell_out, #shell_out!

Constructor Details

#initialize(cookbook_path) ⇒ SyntaxCheck

Create a new SyntaxCheck object

Arguments

cookbook_path:

the (on disk) path to the cookbook



95
96
97
98
99
100
# File 'lib/chef/cookbook/syntax_check.rb', line 95

def initialize(cookbook_path)
  @cookbook_path = cookbook_path
  @chefignore ||= Chefignore.new(cookbook_path)

  @validated_files = PersistentSet.new
end

Instance Attribute Details

#chefignoreObject (readonly)

Returns the value of attribute chefignore.



80
81
82
# File 'lib/chef/cookbook/syntax_check.rb', line 80

def chefignore
  @chefignore
end

#cookbook_pathObject (readonly)

Returns the value of attribute cookbook_path.



74
75
76
# File 'lib/chef/cookbook/syntax_check.rb', line 74

def cookbook_path
  @cookbook_path
end

#validated_filesObject (readonly)

A PersistentSet object that tracks which files have already been validated.



78
79
80
# File 'lib/chef/cookbook/syntax_check.rb', line 78

def validated_files
  @validated_files
end

Class Method Details

.for_cookbook(cookbook_name, cookbook_path = nil) ⇒ Object

Creates a new SyntaxCheck given the cookbook_name and a cookbook_path. If no cookbook_path is given, Chef::Config.cookbook_path is used.



84
85
86
87
88
89
90
# File 'lib/chef/cookbook/syntax_check.rb', line 84

def self.for_cookbook(cookbook_name, cookbook_path=nil)
  cookbook_path ||= Chef::Config.cookbook_path
  unless cookbook_path
    raise ArgumentError, "Cannot find cookbook #{cookbook_name} unless Chef::Config.cookbook_path is set or an explicit cookbook path is given"
  end
  new(File.join(cookbook_path, cookbook_name.to_s))
end

Instance Method Details

#invalid_erb_file(erb_file, error_message) ⇒ Object

Debug a syntax error in a template.



232
233
234
235
236
237
# File 'lib/chef/cookbook/syntax_check.rb', line 232

def invalid_erb_file(erb_file, error_message)
  file_relative_path = erb_file[/^#{Regexp.escape(cookbook_path+File::Separator)}(.*)/, 1]
  Chef::Log.fatal("Erb template #{file_relative_path} has a syntax error:")
  error_message.each_line { |l| Chef::Log.fatal(l.chomp) }
  nil
end

#invalid_ruby_file(ruby_file, error_message) ⇒ Object

Debugs ruby syntax errors by printing the path to the file and any diagnostic info given in error_message



280
281
282
283
284
285
# File 'lib/chef/cookbook/syntax_check.rb', line 280

def invalid_ruby_file(ruby_file, error_message)
  file_relative_path = ruby_file[/^#{Regexp.escape(cookbook_path+File::Separator)}(.*)/, 1]
  Chef::Log.fatal("Cookbook file #{file_relative_path} has a ruby syntax error:")
  error_message.each_line { |l| Chef::Log.fatal(l.chomp) }
  false
end

#remove_ignored_files(file_list) ⇒ Object



102
103
104
105
106
107
108
109
110
# File 'lib/chef/cookbook/syntax_check.rb', line 102

def remove_ignored_files(file_list)
  return file_list unless chefignore.ignores.length > 0
  file_list.reject do |full_path|
    cookbook_pn = Pathname.new cookbook_path
    full_pn = Pathname.new full_path
    relative_pn = full_pn.relative_path_from cookbook_pn
    chefignore.ignored? relative_pn.to_s
  end
end

#rubyObject

Returns the full path to the running ruby.



288
289
290
# File 'lib/chef/cookbook/syntax_check.rb', line 288

def ruby
  Gem.ruby
end

#ruby_filesObject



112
113
114
# File 'lib/chef/cookbook/syntax_check.rb', line 112

def ruby_files
  remove_ignored_files Dir[File.join(cookbook_path, '**', '*.rb')]
end

#template_filesObject



127
128
129
# File 'lib/chef/cookbook/syntax_check.rb', line 127

def template_files
  remove_ignored_files Dir[File.join(cookbook_path, '**', '*.erb')]
end

#untested_ruby_filesObject



116
117
118
119
120
121
122
123
124
125
# File 'lib/chef/cookbook/syntax_check.rb', line 116

def untested_ruby_files
  ruby_files.reject do |file|
    if validated?(file)
      Chef::Log.debug("Ruby file #{file} is unchanged, skipping syntax check")
      true
    else
      false
    end
  end
end

#untested_template_filesObject



131
132
133
134
135
136
137
138
139
140
# File 'lib/chef/cookbook/syntax_check.rb', line 131

def untested_template_files
  template_files.reject do |file|
    if validated?(file)
      Chef::Log.debug("Template #{file} is unchanged, skipping syntax check")
      true
    else
      false
    end
  end
end

#validate_erb_file_inline(erb_file) ⇒ Object

Validate the ruby code in an erb template. Uses RubyVM to do syntax checking, so callers should check #validate_inline? before calling.



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/chef/cookbook/syntax_check.rb', line 191

def validate_erb_file_inline(erb_file)
  old_stderr = $stderr

  engine = Erubis::Eruby.new
  engine.convert!(IO.read(erb_file))

  ruby_code = engine.src

  # Even when we're compiling the code w/ RubyVM, syntax errors just
  # print to $stderr. We want to capture this and handle the printing
  # ourselves, so we must temporarily swap $stderr to capture the output.
  tmp_stderr = $stderr = StringIO.new

  abs_path = File.expand_path(erb_file)
  RubyVM::InstructionSequence.new(ruby_code, erb_file, abs_path, 0)

  true
rescue SyntaxError
  $stderr = old_stderr
  invalid_erb_file(erb_file, tmp_stderr.string)
  false
ensure
  # be paranoid about setting stderr back to the old value.
  $stderr = old_stderr if defined?(old_stderr) && old_stderr
end

#validate_erb_via_subcommand(erb_file) ⇒ Object

Validate the ruby code in an erb template. Pipes the output of ‘erubis -x` to `ruby -c`, so it works with any ruby version, but is much slower than the inline version. – TODO: This can be removed when ruby 1.8 support is dropped.



222
223
224
225
226
227
228
229
# File 'lib/chef/cookbook/syntax_check.rb', line 222

def validate_erb_via_subcommand(erb_file)
  result = shell_out("erubis -x #{erb_file} | #{ruby} -c")
  result.error!
  true
rescue Mixlib::ShellOut::ShellCommandFailed
  invalid_erb_file(erb_file, result.stderr)
  false
end

#validate_inline?Boolean

Whether or not we’re running on a version of ruby that can support inline validation. Inline validation relies on the RubyVM features introduced with ruby 1.9, so 1.8 cannot be supported.

Returns:

  • (Boolean)


185
186
187
# File 'lib/chef/cookbook/syntax_check.rb', line 185

def validate_inline?
  defined?(RubyVM::InstructionSequence)
end

#validate_ruby_by_subcommand(ruby_file) ⇒ Object

Validate the syntax of a ruby file by shelling out to ‘ruby -c`. Should work for all ruby versions, but is slower and uses more resources than the inline strategy.



269
270
271
272
273
274
275
276
# File 'lib/chef/cookbook/syntax_check.rb', line 269

def validate_ruby_by_subcommand(ruby_file)
  result = shell_out("#{ruby} -c #{ruby_file}")
  result.error!
  true
rescue Mixlib::ShellOut::ShellCommandFailed
  invalid_ruby_file(ruby_file, result.stderr)
  false
end

#validate_ruby_file(ruby_file) ⇒ Object



173
174
175
176
177
178
179
180
# File 'lib/chef/cookbook/syntax_check.rb', line 173

def validate_ruby_file(ruby_file)
  Chef::Log.debug("Testing #{ruby_file} for syntax errors...")
  if validate_inline?
    validate_ruby_file_inline(ruby_file)
  else
    validate_ruby_by_subcommand(ruby_file)
  end
end

#validate_ruby_file_inline(ruby_file) ⇒ Object

Validate the syntax of a ruby file. Uses (Ruby 1.9+ only) RubyVM to compile the code without evaluating it or spawning a new process. Callers should check #validate_inline? before calling.



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/chef/cookbook/syntax_check.rb', line 242

def validate_ruby_file_inline(ruby_file)
  # Even when we're compiling the code w/ RubyVM, syntax errors just
  # print to $stderr. We want to capture this and handle the printing
  # ourselves, so we must temporarily swap $stderr to capture the output.
  old_stderr = $stderr
  tmp_stderr = $stderr = StringIO.new
  abs_path = File.expand_path(ruby_file)
  file_content = IO.read(abs_path)
  # We have to wrap this in a block so the user code evaluates in a
  # similar context as what Chef does normally. Otherwise RubyVM
  # will reject some common idioms, like using `return` to end evaluation
  # of a recipe. See also CHEF-5199
  wrapped_content = "Object.new.instance_eval do\n#{file_content}\nend\n"
  RubyVM::InstructionSequence.new(wrapped_content, ruby_file, abs_path, 0)
  true
rescue SyntaxError
  $stderr = old_stderr
  invalid_ruby_file(ruby_file, tmp_stderr.string)
  false
ensure
  # be paranoid about setting stderr back to the old value.
  $stderr = old_stderr if defined?(old_stderr) && old_stderr
end

#validate_ruby_filesObject



150
151
152
153
154
155
# File 'lib/chef/cookbook/syntax_check.rb', line 150

def validate_ruby_files
  untested_ruby_files.each do |ruby_file|
    return false unless validate_ruby_file(ruby_file)
    validated(ruby_file)
  end
end

#validate_template(erb_file) ⇒ Object



164
165
166
167
168
169
170
171
# File 'lib/chef/cookbook/syntax_check.rb', line 164

def validate_template(erb_file)
  Chef::Log.debug("Testing template #{erb_file} for syntax errors...")
  if validate_inline?
    validate_erb_file_inline(erb_file)
  else
    validate_erb_via_subcommand(erb_file)
  end
end

#validate_templatesObject



157
158
159
160
161
162
# File 'lib/chef/cookbook/syntax_check.rb', line 157

def validate_templates
  untested_template_files.each do |template|
    return false unless validate_template(template)
    validated(template)
  end
end

#validated(file) ⇒ Object



146
147
148
# File 'lib/chef/cookbook/syntax_check.rb', line 146

def validated(file)
  validated_files.add(checksum(file))
end

#validated?(file) ⇒ Boolean

Returns:

  • (Boolean)


142
143
144
# File 'lib/chef/cookbook/syntax_check.rb', line 142

def validated?(file)
  validated_files.include?(checksum(file))
end