puppet-check 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +11 -0
  3. data/.travis.yml +21 -0
  4. data/CHANGELOG.md +14 -0
  5. data/Dockerfile +22 -0
  6. data/Gemfile +5 -0
  7. data/LICENSE.md +20 -0
  8. data/README.md +148 -0
  9. data/Rakefile +24 -0
  10. data/Vagrantfile +77 -0
  11. data/bin/puppet-check +5 -0
  12. data/config.reek +3 -0
  13. data/images/puppetcheck_new.png +0 -0
  14. data/images/puppetcheck_old.odp +0 -0
  15. data/images/puppetcheck_old.png +0 -0
  16. data/lib/puppet-check.rb +86 -0
  17. data/lib/puppet-check/cli.rb +33 -0
  18. data/lib/puppet-check/data_parser.rb +95 -0
  19. data/lib/puppet-check/puppet_parser.rb +77 -0
  20. data/lib/puppet-check/ruby_parser.rb +93 -0
  21. data/lib/puppet-check/tasks.rb +15 -0
  22. data/lib/puppet-check/utils.rb +22 -0
  23. data/puppet-check.gemspec +25 -0
  24. data/spec/fixtures/foobarbaz +1 -0
  25. data/spec/fixtures/hieradata/good.json +7 -0
  26. data/spec/fixtures/hieradata/good.yaml +7 -0
  27. data/spec/fixtures/hieradata/style.yaml +15 -0
  28. data/spec/fixtures/hieradata/syntax.json +8 -0
  29. data/spec/fixtures/hieradata/syntax.yaml +8 -0
  30. data/spec/fixtures/lib/good.rb +1 -0
  31. data/spec/fixtures/lib/rubocop_style.rb +3 -0
  32. data/spec/fixtures/lib/style.rb +12 -0
  33. data/spec/fixtures/lib/syntax.rb +1 -0
  34. data/spec/fixtures/librarian_good/Puppetfile +5 -0
  35. data/spec/fixtures/librarian_style/Puppetfile +5 -0
  36. data/spec/fixtures/librarian_syntax/Puppetfile +7 -0
  37. data/spec/fixtures/manifests/good.pp +1 -0
  38. data/spec/fixtures/manifests/style_lint.pp +4 -0
  39. data/spec/fixtures/manifests/style_parser.pp +2 -0
  40. data/spec/fixtures/manifests/syntax.pp +1 -0
  41. data/spec/fixtures/metadata_good/metadata.json +35 -0
  42. data/spec/fixtures/metadata_style/metadata.json +35 -0
  43. data/spec/fixtures/metadata_syntax/metadata.json +15 -0
  44. data/spec/fixtures/templates/good.epp +3 -0
  45. data/spec/fixtures/templates/good.erb +1 -0
  46. data/spec/fixtures/templates/no_method_error.erb +3 -0
  47. data/spec/fixtures/templates/style.erb +3 -0
  48. data/spec/fixtures/templates/syntax.epp +3 -0
  49. data/spec/fixtures/templates/syntax.erb +1 -0
  50. data/spec/integration/integration_spec.rb +41 -0
  51. data/spec/puppet-check/cli_spec.rb +48 -0
  52. data/spec/puppet-check/data_parser_spec.rb +64 -0
  53. data/spec/puppet-check/puppet_parser_spec.rb +67 -0
  54. data/spec/puppet-check/ruby_parser_spec.rb +111 -0
  55. data/spec/puppet-check/utils_spec.rb +20 -0
  56. data/spec/puppet-check_spec.rb +82 -0
  57. data/spec/spec_helper.rb +13 -0
  58. metadata +233 -0
@@ -0,0 +1,33 @@
1
+ require 'optparse'
2
+ require_relative '../puppet-check'
3
+
4
+ # the command line interface for PuppetCheck
5
+ class PuppetCheck::CLI
6
+ # run method for the cli
7
+ def self.run(args)
8
+ parse(args)
9
+ raise 'puppet-check: no paths specified' if args.empty?
10
+
11
+ # run PuppetCheck
12
+ PuppetCheck.new.run(args)
13
+ end
14
+
15
+ # parse the user arguments
16
+ def self.parse(args)
17
+ opt_parser = OptionParser.new do |opts|
18
+ # usage
19
+ opts.banner = 'usage: puppet-check [options] paths'
20
+
21
+ # bool options
22
+ opts.on('-f', '--future', 'Enable future parser') { PuppetCheck.future_parser = true }
23
+ opts.on('-s', '--style', 'Enable style checks') { PuppetCheck.style_check = true }
24
+ # arguments to style checkers
25
+ opts.on('--puppet-lint arg_one,arg_two', Array, 'Arguments for PuppetLint ignored checks') do |puppetlint_args|
26
+ PuppetCheck.puppetlint_args = puppetlint_args.map { |arg| "--#{arg}" }
27
+ end
28
+ opts.on('--rubocop arg_one,arg_two', String, 'Arguments for Rubocop disabled cops') { |arg| PuppetCheck.rubocop_args = ['--except', arg] }
29
+ end
30
+
31
+ opt_parser.parse!(args)
32
+ end
33
+ end
@@ -0,0 +1,95 @@
1
+ require_relative '../puppet-check'
2
+
3
+ # executes diagnostics on data files
4
+ class DataParser
5
+ # checks yaml (.yaml/.yml)
6
+ def self.yaml(files)
7
+ require 'yaml'
8
+
9
+ files.each do |file|
10
+ # check yaml syntax
11
+ begin
12
+ parsed = YAML.load_file(file)
13
+ rescue Psych::SyntaxError, StandardError => err
14
+ PuppetCheck.error_files.push("-- #{file}:\n#{err.to_s.gsub("(#{file}): ", '')}")
15
+ else
16
+ warnings = hiera(parsed)
17
+ next PuppetCheck.warning_files.push("-- #{file}:\n#{warnings.join("\n")}") unless warnings.empty?
18
+ PuppetCheck.clean_files.push("-- #{file}")
19
+ end
20
+ end
21
+ end
22
+
23
+ # checks json (.json)
24
+ def self.json(files)
25
+ require 'json'
26
+
27
+ files.each do |file|
28
+ # check json syntax
29
+ begin
30
+ parsed = JSON.parse(File.read(file))
31
+ rescue JSON::ParserError => err
32
+ PuppetCheck.error_files.push("-- #{file}:\n#{err.to_s.lines.first.strip}")
33
+ else
34
+ # check metadata.json
35
+ if file =~ /.*metadata\.json$/
36
+ # metadata-json-lint has issues and is essentially no longer maintained so here is an improved and leaner version of it
37
+ require 'spdx-licenses'
38
+
39
+ # check for errors
40
+ errors = []
41
+
42
+ # check for required keys
43
+ %w(name version author license summary source dependencies).each do |key|
44
+ errors.push("Required field '#{key}' not found in metadata.json.") unless parsed.key?(key)
45
+ end
46
+
47
+ # check for duplicate dependencies and requirements
48
+ %w(requirements dependencies).each do |key|
49
+ next unless parsed.key?(key)
50
+ names = []
51
+ parsed[key].each do |req_dep|
52
+ name = req_dep['name']
53
+ errors.push("Duplicate #{key} on #{name}.") if names.include?(name)
54
+ names << name
55
+ end
56
+ end
57
+
58
+ # check for deprecated fields
59
+ %w(types checksum).each do |key|
60
+ errors.push("Deprecated field '#{key}' found.") if parsed.key?(key)
61
+ end
62
+
63
+ # check for summary under 144 character
64
+ errors.push('Summary exceeds 144 characters.') if parsed.key?('summary') && parsed['summary'].size > 144
65
+
66
+ next PuppetCheck.error_files.push("-- #{file}:\n#{errors.join("\n")}") unless errors.empty?
67
+
68
+ # check for warnings
69
+ warnings = []
70
+
71
+ # check for spdx license (rubygems/util/licenses for rubygems >= 2.5 in the far future)
72
+ if parsed.key?('license') && !SpdxLicenses.exist?(parsed['license']) && parsed['license'] !~ /[pP]roprietary/
73
+ warnings.push("License identifier '#{parsed['license']}' is not in the SPDX list: http://spdx.org/licenses/")
74
+ end
75
+
76
+ next PuppetCheck.warning_files.push("-- #{file}:\n#{warnings.join("\n")}") unless warnings.empty?
77
+ else
78
+ # check for questionable hieradata
79
+ warnings = hiera(parsed)
80
+ next PuppetCheck.warning_files.push("-- #{file}:\n#{warnings.join("\n")}") unless warnings.empty?
81
+ end
82
+ PuppetCheck.clean_files.push("-- #{file}")
83
+ end
84
+ end
85
+ end
86
+
87
+ # checks hieradata
88
+ def self.hiera(data)
89
+ warnings = []
90
+ data.each do |key, value|
91
+ warnings.push("Values missing in key '#{key}'.") if value.class.to_s == 'NilClass'
92
+ end
93
+ warnings
94
+ end
95
+ end
@@ -0,0 +1,77 @@
1
+ require 'puppet'
2
+ require_relative '../puppet-check'
3
+
4
+ # executes diagnostics on puppet files
5
+ class PuppetParser
6
+ # checks puppet (.pp)
7
+ def self.manifest(files)
8
+ require 'puppet/face'
9
+
10
+ # prepare the Puppet settings for the error checking
11
+ Puppet.initialize_settings unless Puppet.settings.app_defaults_initialized?
12
+ Puppet[:parser] = 'future' if PuppetCheck.future_parser && (Puppet::PUPPETVERSION.to_i < 4)
13
+
14
+ files.each do |file|
15
+ # setup error logging and collection
16
+ errors = []
17
+ Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(errors))
18
+
19
+ # check puppet syntax
20
+ begin
21
+ Puppet::Face[:parser, :current].validate(file)
22
+ # this is the actual error that we need to rescue Puppet::Face from
23
+ rescue SystemExit
24
+ next PuppetCheck.error_files.push("-- #{file}:\n#{errors.map(&:to_s).join("\n").gsub("#{File.absolute_path(file)}:", '')}")
25
+ end
26
+
27
+ # initialize warnings with output from the parser if it exists since they are warnings if they did not trigger a SystemExit
28
+ warnings = errors.empty? ? "-- #{file}:" : "-- #{file}:\n#{errors.map(&:to_s).join("\n").gsub("#{File.absolute_path(file)}:", '')}"
29
+ Puppet::Util::Log.close_all
30
+
31
+ # check puppet style
32
+ if PuppetCheck.style_check
33
+ require 'puppet-lint'
34
+ require 'puppet-lint/optparser'
35
+
36
+ # check for invalid arguments to PuppetLint
37
+ begin
38
+ PuppetLint::OptParser.build.parse!(PuppetCheck.puppetlint_args)
39
+ rescue OptionParser::InvalidOption
40
+ raise 'puppet-lint: invalid option'
41
+ end
42
+
43
+ # prepare the PuppetLint object for style checks
44
+ puppet_lint = PuppetLint.new
45
+ puppet_lint.file = file
46
+ puppet_lint.run
47
+
48
+ # collect the warnings
49
+ if puppet_lint.warnings?
50
+ puppet_lint.problems.each { |values| warnings += "\n#{values[:message]} at line #{values[:line]}, column #{values[:column]}" }
51
+ end
52
+ end
53
+ next PuppetCheck.warning_files.push(warnings) unless warnings == "-- #{file}:"
54
+ PuppetCheck.clean_files.push("-- #{file}")
55
+ end
56
+ end
57
+
58
+ # checks puppet template (.epp)
59
+ def self.template(files)
60
+ require 'puppet/pops'
61
+
62
+ files.each do |file|
63
+ # puppet before version 4 cannot check template syntax
64
+ next PuppetCheck.ignored_files.push("-- #{file}: ignored due to Puppet Agent < 4.0.0") if Puppet::PUPPETVERSION.to_i < 4
65
+
66
+ # check puppet template syntax
67
+ begin
68
+ # credits to gds-operations/puppet-syntax for the parser function call
69
+ Puppet::Pops::Parser::EvaluatingParser::EvaluatingEppParser.new.parse_file(file)
70
+ rescue StandardError => err
71
+ PuppetCheck.error_files.push("-- #{file}:\n#{err.to_s.gsub("#{file}:", '')}")
72
+ else
73
+ PuppetCheck.clean_files.push("-- #{file}")
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,93 @@
1
+ require_relative '../puppet-check'
2
+ require_relative 'utils'
3
+
4
+ # executes diagnostics on ruby files
5
+ class RubyParser
6
+ # checks ruby (.rb)
7
+ def self.ruby(files)
8
+ files.each do |file|
9
+ # check ruby syntax
10
+ begin
11
+ catch(:good) { instance_eval("BEGIN {throw :good}; #{File.read(file)}") }
12
+ rescue ScriptError, StandardError => err
13
+ PuppetCheck.error_files.push("-- #{file}:\n#{err}")
14
+ else
15
+ # check ruby style
16
+ if PuppetCheck.style_check
17
+ require 'rubocop'
18
+
19
+ # check RuboCop and collect warnings
20
+ rubocop_warnings = Utils.capture_stdout { RuboCop::CLI.new.run(PuppetCheck.rubocop_args + ['--format', 'emacs', file]) }
21
+ warnings = rubocop_warnings == '' ? '' : rubocop_warnings.split("#{File.absolute_path(file)}:").join('')
22
+
23
+ # check Reek
24
+ if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.1.0')
25
+ require 'reek'
26
+ require 'reek/cli/application'
27
+ reek_warnings = Utils.capture_stdout { Reek::CLI::Application.new([file]).execute }
28
+ warnings += reek_warnings.split("\n")[1..-1].map(&:strip).join("\n") unless reek_warnings == ''
29
+ end
30
+
31
+ # return warnings
32
+ next PuppetCheck.warning_files.push("-- #{file}:\n#{warnings.strip}") unless warnings == ''
33
+ end
34
+ PuppetCheck.clean_files.push("-- #{file}")
35
+ end
36
+ end
37
+ end
38
+
39
+ # checks ruby template (.erb)
40
+ def self.template(files)
41
+ require 'erb'
42
+
43
+ files.each do |file|
44
+ # check ruby template syntax
45
+ begin
46
+ # need to eventually have this associated with a different binding during each iteration
47
+ # warnings = Util.capture_stderr { ERB.new(File.read(file), nil, '-').result(RubyParser.new.binding) }
48
+ warnings = Utils.capture_stderr { ERB.new(File.read(file), nil, '-').result }
49
+ # credits to gds-operations/puppet-syntax for errors to ignore
50
+ rescue NameError, TypeError
51
+ # empty out warnings since it would contain an error if this pass triggers
52
+ warnings = ''
53
+ rescue ScriptError => err
54
+ next PuppetCheck.error_files.push("-- #{file}:\n#{err}")
55
+ end
56
+ # return warnings from the check if there were any
57
+ next PuppetCheck.warning_files.push("-- #{file}:\n#{warnings.gsub('warning: ', '').split('(erb):').join('').strip}") unless warnings == ''
58
+ PuppetCheck.clean_files.push("-- #{file}")
59
+ end
60
+ end
61
+
62
+ # checks librarian puppet (Puppetfile/Modulefile) and misc ruby (Rakefile/Gemfile)
63
+ def self.librarian(files)
64
+ files.each do |file|
65
+ begin
66
+ # check librarian puppet syntax
67
+ catch(:good) { instance_eval("BEGIN {throw :good}; #{File.read(file)}") }
68
+ rescue SyntaxError, LoadError, ArgumentError => err
69
+ PuppetCheck.error_files.push("-- #{file}:\n#{err}")
70
+ else
71
+ # check librarian puppet style
72
+ if PuppetCheck.style_check
73
+ require 'rubocop'
74
+
75
+ # check Rubocop
76
+ rubocop_args = PuppetCheck.rubocop_args.clone
77
+ # RuboCop is confused about the first 'mod' argument in librarian puppet (and Rakefiles and Gemfiles) so disable the Style/FileName check
78
+ rubocop_args.include?('--except') ? rubocop_args[rubocop_args.index('--except') + 1] = "#{rubocop_args[rubocop_args.index('--except') + 1]},Style/FileName" : rubocop_args.concat(['--except', 'Style/FileName'])
79
+ warnings = Utils.capture_stdout { RuboCop::CLI.new.run(rubocop_args + ['--format', 'emacs', file]) }
80
+
81
+ # collect style warnings
82
+ next PuppetCheck.warning_files.push("-- #{file}:\n#{warnings.split("#{File.absolute_path(file)}:").join('')}") unless warnings.empty?
83
+ end
84
+ PuppetCheck.clean_files.push("-- #{file}")
85
+ end
86
+ end
87
+ end
88
+
89
+ # potentially for unique erb bindings
90
+ def binding
91
+ binding
92
+ end
93
+ end
@@ -0,0 +1,15 @@
1
+ require 'rake'
2
+ require 'rake/tasklib'
3
+ require_relative '../puppet-check'
4
+
5
+ # the rake interface for PuppetCheck
6
+ class PuppetCheck::Tasks < ::Rake::TaskLib
7
+ def initialize
8
+ desc 'Execute Puppet-Check file checks'
9
+ task 'puppetcheck:file' do
10
+ exit PuppetCheck.new.run(Dir.glob('*'))
11
+ end
12
+ end
13
+ end
14
+
15
+ PuppetCheck::Tasks.new
@@ -0,0 +1,22 @@
1
+ # utility methods totally not edited from StackOverflow
2
+ class Utils
3
+ # captures stdout from a block: out = capture_stdout { code }
4
+ def self.capture_stdout
5
+ old_stdout = $stdout
6
+ $stdout = StringIO.new
7
+ yield
8
+ $stdout.string
9
+ ensure
10
+ $stdout = old_stdout
11
+ end
12
+
13
+ # captures stderr from a block: err = capture_stderr { code }
14
+ def self.capture_stderr
15
+ old_stderr = $stderr
16
+ $stderr = StringIO.new
17
+ yield
18
+ $stderr.string
19
+ ensure
20
+ $stderr = old_stderr
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'puppet-check'
3
+ spec.version = '1.0.0'
4
+ spec.authors = ['Matt Schuchard']
5
+ spec.description = 'Puppet Check is a gem that provides a comprehensive, streamlined, and efficient analysis of the syntax, style, and validity of your entire Puppet code and data.'
6
+ spec.summary = 'A streamlined comprehensive set of checks for your entire Puppet code and data'
7
+ spec.homepage = 'https://www.github.com/mschuchard/puppet-check'
8
+ spec.license = 'MIT'
9
+
10
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
11
+ spec.executables = spec.files.grep(%r{^bin/}) { |file| File.basename(file) }
12
+ spec.test_files = spec.files.grep(%r{^spec/})
13
+ spec.require_paths = Dir['lib']
14
+
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 1.9.3')
16
+ spec.add_dependency 'puppet', '>= 3.2', '< 5'
17
+ # spec.add_dependency 'rspec', '~> 3.0'
18
+ spec.add_dependency 'rake', '>= 9', '< 13'
19
+ spec.add_dependency 'rubocop', '~> 0'
20
+ spec.add_dependency 'puppet-lint', '~> 1.1'
21
+ # spec.add_dependency 'rspec-puppet', '~> 2.0'
22
+ # spec.add_dependency 'beaker.' '~> 2.0'
23
+ spec.add_dependency 'spdx-licenses', '~> 1.0'
24
+ spec.add_development_dependency 'rspec', '~> 3.0'
25
+ end
@@ -0,0 +1 @@
1
+ foobarbaz
@@ -0,0 +1,7 @@
1
+ {
2
+ "i": "am",
3
+ "a": {
4
+ "good": "json"
5
+ },
6
+ "file": "."
7
+ }
@@ -0,0 +1,7 @@
1
+ ---
2
+ i:
3
+ - am
4
+ - a
5
+
6
+ good:
7
+ yaml: file
@@ -0,0 +1,15 @@
1
+ ---
2
+ i: 'am'
3
+
4
+ :a:
5
+ good:
6
+ - yaml
7
+ - file
8
+
9
+ but: 'i am missing a'
10
+
11
+ value:
12
+
13
+ and:
14
+ also: 'a'
15
+ subvalue:
@@ -0,0 +1,8 @@
1
+ {
2
+ "i": "am"
3
+ "a": {
4
+ "json": "file",
5
+ }
6
+ "with": "bad",
7
+ "syntax":
8
+ }
@@ -0,0 +1,8 @@
1
+ ---
2
+ i: - am :a:
3
+ - :yaml:
4
+ - file:
5
+
6
+ with:
7
+ : bad
8
+ :syntax: -
@@ -0,0 +1 @@
1
+ puts 'i am a good ruby file'
@@ -0,0 +1,3 @@
1
+ hash = { :i => 'am' }
2
+ $a = 'ruby'
3
+ puts "file with bad style"
@@ -0,0 +1,12 @@
1
+ hash = { :i => 'am' }
2
+ $a = 'ruby'
3
+ puts "file with bad style"
4
+
5
+ # Reek
6
+ class Issue
7
+ attr_accessor :foobarbaz
8
+
9
+ def initialize
10
+ #
11
+ end
12
+ end