puppet-check 1.0.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.
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