├── .circleci └── config.yml ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .reek.yml ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── Vagrantfile ├── bin └── puppet-check ├── images ├── puppetcheck_new.png ├── puppetcheck_old.odp └── puppetcheck_old.png ├── lib ├── puppet-check │ ├── cli.rb │ ├── data_parser.rb │ ├── output_results.rb │ ├── puppet_parser.rb │ ├── regression_check.rb │ ├── rspec_puppet_support.rb │ ├── ruby_parser.rb │ ├── tasks.rb │ └── utils.rb └── puppet_check.rb ├── puppet-check.gemspec └── spec ├── fixtures ├── foobarbaz ├── hieradata │ ├── good.eyaml │ ├── good.json │ ├── good.yaml │ ├── style.eyaml │ ├── style.yaml │ ├── syntax.eyaml │ ├── syntax.json │ └── syntax.yaml ├── keys │ ├── private_key.pkcs7.pem │ └── public_key.pkcs7.pem ├── lib │ ├── good.rb │ ├── rubocop_style.rb │ ├── style.rb │ └── syntax.rb ├── librarian_good │ └── Puppetfile ├── librarian_style │ └── Puppetfile ├── librarian_syntax │ └── Puppetfile ├── manifests │ ├── .puppet-lint.rc │ ├── eof_syntax.pp │ ├── good.pp │ ├── style_lint.pp │ ├── style_parser.pp │ └── syntax.pp ├── metadata.json ├── metadata_good │ └── metadata.json ├── metadata_style │ └── metadata.json ├── metadata_style_two │ └── metadata.json ├── metadata_syntax │ └── metadata.json ├── plans │ ├── good.pp │ ├── style.pp │ └── syntax.pp ├── spec │ ├── acceptance │ │ └── .empty │ ├── facter │ │ └── facter_spec.rb │ └── fixtures │ │ └── do_not_parse_me ├── task_metadata │ ├── task_bad.json │ └── task_good.json └── templates │ ├── good.epp │ ├── good.erb │ ├── no_method_error.erb │ ├── style.erb │ ├── syntax.epp │ └── syntax.erb ├── octocatalog-diff ├── facts.yaml ├── hiera.yaml ├── manifests │ └── site.pp └── octocatalog_diff.cfg.rb ├── puppet-check ├── cli_spec.rb ├── data_parser_spec.rb ├── output_results_spec.rb ├── puppet_parser_spec.rb ├── regression_check_spec.rb ├── rspec_puppet_support_spec.rb ├── ruby_parser_spec.rb ├── tasks_spec.rb └── utils_spec.rb ├── puppet_check_spec.rb ├── spec_helper.rb └── system └── system_spec.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | 4 | jobs: 5 | test: 6 | working_directory: /tmp/project 7 | parameters: 8 | ruby-version: 9 | type: string 10 | docker: 11 | - image: ruby:<< parameters.ruby-version >>-slim 12 | resource_class: small 13 | steps: 14 | - checkout 15 | - restore_cache: 16 | keys: 17 | - puppet-check-ruby-<< parameters.ruby-version >> 18 | - run: 19 | name: package prereqs install 20 | command: apt-get update && apt-get install -y cmake pkg-config 21 | - run: 22 | name: bundler install 23 | command: bundle install --retry=3 24 | - save_cache: 25 | paths: 26 | - ~/.bundle 27 | key: puppet-check-ruby-<< parameters.ruby-version >> 28 | - run: 29 | name: execute tests 30 | command: bundle exec rake unit system 31 | 32 | workflows: 33 | execute_tests: 34 | jobs: 35 | - test: 36 | matrix: 37 | parameters: 38 | ruby-version: 39 | - '2.7' 40 | - '3.4' 41 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ci 3 | 4 | on: 5 | push: 6 | branches: [master] 7 | paths: 8 | - 'lib/**' 9 | - 'spec/**/*_spec.rb' 10 | pull_request: 11 | branches: [master] 12 | paths: 13 | - 'lib/**' 14 | - 'spec/**/*_spec.rb' 15 | 16 | jobs: 17 | test: 18 | strategy: 19 | matrix: 20 | ruby_version: 21 | - '2.7' 22 | - '3.4' 23 | runs-on: ubuntu-latest 24 | container: ruby:${{ matrix.ruby_version }}-slim 25 | steps: 26 | - name: checkout 27 | uses: actions/checkout@v4 28 | - name: install some prerequisite packages 29 | run: apt-get update && apt-get install -y cmake pkg-config 30 | - name: bundler install 31 | run: bundle install --retry=3 32 | - name: run tests 33 | run: bundle exec rake unit system 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | -------------------------------------------------------------------------------- /.reek.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - spec/fixtures 4 | - bin/puppet 5 | 6 | detectors: 7 | TooManyStatements: 8 | enabled: false 9 | 10 | NestedIterators: 11 | enabled: false 12 | 13 | LongParameterList: 14 | enabled: false 15 | 16 | UncommunicativeVariableName: 17 | enabled: false 18 | 19 | DuplicateMethodCall: 20 | enabled: false 21 | 22 | NilCheck: 23 | enabled: false 24 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | require: rubocop-performance 3 | 4 | AllCops: 5 | NewCops: enable 6 | Include: 7 | - 'lib/**/*.rb' 8 | - 'bin/*.rb' 9 | - 'spec/**/*.rb' 10 | - 'spec/**/Puppetfile' 11 | - puppet-check.gemspec 12 | - Gemfile 13 | - Rakefile 14 | - Vagrantfile 15 | Exclude: 16 | - 'spec/fixtures/**/*' 17 | - 'bin/puppet' 18 | 19 | Metrics: 20 | Enabled: false 21 | 22 | Layout/LineLength: 23 | Enabled: false 24 | 25 | Layout/EmptyLineAfterGuardClause: 26 | Enabled: false 27 | 28 | Naming/RescuedExceptionsVariableName: 29 | Enabled: false 30 | 31 | # false triggers for this cop 32 | Style/EvalWithLocation: 33 | Enabled: false 34 | 35 | # probably can remove after rails cops removed from core 36 | Style/FrozenStringLiteralComment: 37 | Enabled: false 38 | 39 | Style/ClassAndModuleChildren: 40 | Enabled: false 41 | 42 | Gemspec/DevelopmentDependencies: 43 | Enabled: false 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 2.3.1 (Next) 2 | - No input target paths now defaults to current working directory instead of error. 3 | - Additionally check `Vagrantfile` and `gemspec` files. 4 | 5 | ### 2.3.0 6 | - Minimum Ruby version increased to 2.7. 7 | - Support Puppet 8. 8 | - Upgrade EYAML validation functionality from experimental to beta. 9 | - Code execution optimization. 10 | 11 | ### 2.2.2 12 | - Workaround Ruby passbyref issue mutating an immutable. 13 | 14 | ### 2.2.1 15 | - Improved output formatting for all formats. 16 | - Update and improve Rake task interfacing. 17 | 18 | ### 2.2.0 19 | - Add Enable Pending Cops to base RuboCop configuration. 20 | - Support checking plans. 21 | - Fix Puppet >= 6.5 error message capture when line/col info. 22 | - Minimum Ruby version increased to 2.6. 23 | - Minimum Puppet version increased to 5.4. 24 | 25 | ### 2.1.0 26 | - Minimum supported version of Puppet bumped to 5.0. 27 | - Minimum Ruby version bumped to 2.4. 28 | - Official support for Puppet 7, Rubocop 1, and Reek 6. 29 | - Fix Puppet message string transform conditionals. 30 | 31 | ### 2.0.1 32 | - Check for existence of executables for dependency module retrieval. 33 | - Beta support for Puppet 7, Rubocop 1, and Reek 6. 34 | 35 | ### 2.0.0 36 | - Bump minimum version of Puppet to 4.0.0 and remove < 4 support code. 37 | - Official support for Puppet 6. 38 | - Minimum Ruby version bumped to 2.2.0. 39 | - Bumped Rubocop and Reek minimum versions and fully migrated SPDX to Rubygems. 40 | - Add rubocop-performance extension to Rubocop. 41 | - Update manifest validation for breaking change in 6.5 API. 42 | - Fix check on specified dependencies in metadata. 43 | - Enable parallel module dependency retrieval. 44 | 45 | ### 1.6.1 46 | - Removed check for hieradata nil/undef value for Hiera >= 5. 47 | - Add rudimentary checks for task metadata. 48 | - Preliminary support for Puppet 6. 49 | - RSpec Puppet fixed its default `spec_helper`. Revert to loading it instead of generating a working one. 50 | 51 | ### 1.6.0 52 | - Minimum Ruby version increased to 2.1. 53 | - Minimum Puppet version increased to 3.7. 54 | - Minimum Puppet-Lint and Rubocop increased to 2.0 and 0.51.0. 55 | - Correctly capturing new Puppet >= 5.4 parser validator output format. 56 | - Refixing style checker error for empty hieradata. 57 | 58 | ### 1.5.1 59 | - Slight cleanup and optimization. 60 | - Fixed check for no spec directories during RSpec Puppet helper. 61 | - Fixed check for semantic versioning in `metadata.json` for numbering > 9. 62 | - Accounted for Puppet syntax validation output bugfix in 5.3. 63 | - Fix bad symlink for module fixture during RSpec Puppet. 64 | - Updating Beaker Rake task usage. 65 | 66 | ### 1.5.0 67 | - Maximum Puppet version increased from 4 to 5. 68 | - Added capability to check EYAML (experimental). 69 | - Test Kitchen frontend interface. 70 | - Updated Puppet error output for Puppet 5 differences. 71 | - Slight optimization for smaller test sets. 72 | - Suppress constant redefinition warnings from Octocatalog-Diff's Puppet code reuse. 73 | - Changed FileName cop to reflect change in RuboCop >= 0.50. 74 | - Entire module is now symlinked into `spec/fixtures/modules` during RSpec Puppet testing (formerly specific components). 75 | 76 | ### 1.4.1 77 | - Support for using SVN to download external module dependencies for RSpec Puppet. 78 | - Better handled situations with uninstalled optional dependencies. 79 | - Code cleanup and optimization. 80 | - Added option to fail on warnings. 81 | - Added additional error info for failed smoke checks. 82 | 83 | ### 1.4.0 84 | - Optimized and fixed checks on dependencies and requirements in `metadata.json`. 85 | - Optional octocatalog-diff smoke testing. 86 | - Optional octocatalog-diff config file support. 87 | 88 | ### 1.3.2 89 | - For the Puppet Forge method of downloading external module dependencies as spec fixtures, the module is now updated if it is already present. Previously, a fresh forced install was always attempted. 90 | - A good `spec_helper` for RSpec Puppet is now generated if one is missing. This is instead of the buggy one that `rspec-puppet-init` generates. 91 | - Fix blocking of hieradata checks on `hiera.yaml`. 92 | - A check was added for '---' appearing more than once in YAML hieradata as Hiera attempts to parse these additional lines as data. 93 | - Additional `metadata.json` warnings for `version_requirement` nested key. 94 | 95 | ### 1.3.1 96 | - For the git and mercurial methods of downloading external module dependencies as spec fixtures, the module is now updated if it is already present and previously retrieved with git or mercurial respectively. Previously, a fresh clone was always attempted. 97 | - Additional syntax and style checks within the `operatingsystem_support`, `requirements`, and `dependencies` hashes in `metadata.json`. 98 | - Reek is now required dependency for all Ruby versions and locked to 3.11 for Ruby 2.0. 99 | - Slight code cleanup and optimization. 100 | 101 | ### 1.3.0 102 | - Minimum Ruby version increased from 1.9.3 to 2.0.0. 103 | - Minimum Puppet version increased from 3.2 to 3.4. 104 | - Fixed issue where invalid arguments to PuppetLint were not displayed in error message. 105 | - Support for outputting the results in YAML or JSON formats. 106 | - Additional style check for `metadata.json`. 107 | - Slight code cleanup and optimization. 108 | - Block hieradata checks from executing on `hiera.yaml`. 109 | 110 | ### 1.2.1 111 | - Code and output cleanup. 112 | - Add arguments support to external module download methods. 113 | - PuppetLint dependency version updated for 2.0 release. 114 | 115 | ### 1.2.0 116 | - Support for external module dependencies with RSpecPuppet. 117 | - Support for nested hash parsing in Hieradata checks. 118 | - Support for `.puppet-lint.rc` config files in manually specified paths. 119 | - A few bug fixes for RSpecPuppet support. 120 | 121 | ### 1.1.1 (yanked from rubygems.org) 122 | - Rewrote and optimized RSpecPuppet module support. 123 | - A variety of minor fixes, cleanup, and improvements (e.g. ignored files now outputs in cyan and not blue) 124 | 125 | ### 1.1.0 126 | - Support for RSpec, RSpecPuppet, and Beaker. 127 | - Empty hieradata file bug fix. 128 | 129 | ### 1.0.0 130 | - Initial release. 131 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Matt Schuchard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Puppet Check 2 | - [Description](#description) 3 | - [Usage](#usage) 4 | - [CLI](#cli) 5 | - [Rake](#rake) 6 | - [API](#api) 7 | - [Docker](#docker) 8 | - [Vagrant](#vagrant) 9 | - [Exit Codes](#exit-codes) 10 | - [Optional Dependencies](#optional-dependencies) 11 | - [Contributing](#contributing) 12 | 13 | ## Description 14 | 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. 15 | 16 | **IMPORTANT**: The current support for encrypted yaml validation is experimental and should be considered a beta feature as of 2.3.0. 17 | 18 | ### Former Method for Code and Data Checks 19 | ![Old](https://raw.githubusercontent.com/mschuchard/puppet-check/master/images/puppetcheck_old.png) 20 | 21 | ### Puppet Check Method for Code and Data Checks 22 | ![New](https://raw.githubusercontent.com/mschuchard/puppet-check/master/images/puppetcheck_new.png) 23 | 24 | ### Example Output 25 | ``` 26 | The following files have errors: 27 | -- manifests/syntax.pp: 28 | This Variable has no effect. A value was produced and then forgotten (one or more preceding expressions may have the wrong form) at 1:1 29 | Illegal variable name, The given name '' does not conform to the naming rule /^((::)?[a-z]\w*)*((::)?[a-z_]\w*)$/ at 1:1 30 | Found 2 errors. Giving up 31 | 32 | -- templates/syntax.epp: 33 | This Name has no effect. A value was produced and then forgotten (one or more preceding expressions may have the wrong form) at 2:4 34 | 35 | -- lib/syntax.rb: 36 | (eval):1: syntax error, unexpected =>, expecting end-of-input 37 | BEGIN {throw :good}; i => am : a '' ruby.file { with } &bad syntax 38 | ^ 39 | 40 | -- templates/syntax.erb: 41 | (erb):1: syntax error, unexpected tIDENTIFIER, expecting ')' 42 | ... am "; _erbout.concat(( @a ruby ).to_s); _erbout.concat " te... 43 | ... ^ 44 | 45 | -- hieradata/syntax.yaml: 46 | block sequence entries are not allowed in this context at line 2 column 4 47 | 48 | -- hieradata/syntax.json: 49 | 743: unexpected token at '{ 50 | 51 | -- metadata_syntax/metadata.json: 52 | Required field 'version' not found. 53 | Field 'requirements' is not an array of hashes. 54 | Duplicate dependencies on puppetlabs/nothing. 55 | Deprecated field 'checksum' found. 56 | Summary exceeds 144 characters. 57 | 58 | -- librarian_syntax/Puppetfile: 59 | (eval):3: syntax error, unexpected ':', expecting end-of-input 60 | librarian: 'puppet' 61 | ^ 62 | 63 | The following files have warnings: 64 | -- manifests/style_lint.pp: 65 | 2:8: double quoted string containing no variables 66 | 2:5: indentation of => is not properly aligned (expected in column 8, but found it in column 5) 67 | 68 | -- manifests/style_parser.pp: 69 | Unrecognized escape sequence '\[' at 2:77 70 | Unrecognized escape sequence '\]' at 2:77 71 | 2:45: double quoted string containing no variables 72 | 73 | -- lib/style.rb: 74 | 1:1: W: Useless assignment to variable - `hash`. 75 | 1:10: C: Use the new Ruby 1.9 hash syntax. 76 | 2:1: C: Do not introduce global variables. 77 | 3:6: C: Prefer single-quoted strings when you don't need string interpolation or special symbols. 78 | [7]:Attribute: Issue#foobarbaz is a writable attribute [https://github.com/troessner/reek/blob/master/docs/Attribute.md] 79 | [6]:IrresponsibleModule: Issue has no descriptive comment [https://github.com/troessner/reek/blob/master/docs/Irresponsible-Module.md] 80 | 81 | -- templates/style.erb: 82 | 3: already initialized constant TEMPLATE 83 | 2: previous definition of TEMPLATE was here 84 | 85 | -- hieradata/style.yaml: 86 | Value(s) missing in key 'value'. 87 | Value(s) missing in key 'and'. 88 | The string --- appears more than once in this data and Hiera will fail to parse it correctly. 89 | 90 | -- metadata_style/metadata.json: 91 | Recommended field 'operatingsystem_support' not found. 92 | 'pe' is missing an upper bound. 93 | License identifier 'Imaginary' is not in the SPDX list: http://spdx.org/licenses/ 94 | 95 | -- metadata_style_two/metadata.json: 96 | Recommended field 'operatingsystem' not found. 97 | Recommended field 'operatingsystemrelease' not found. 98 | 'puppetlabs/one' has non-semantic versioning in its 'version_requirement' key. 99 | 'puppetlabs/two' is missing an upper bound. 100 | 101 | -- librarian_style/Puppetfile: 102 | 2:3: C: Align the parameters of a method call if they span more than one line. 103 | 5:13: C: Use the new Ruby 1.9 hash syntax. 104 | 105 | The following files have no errors or warnings: 106 | -- manifests/good.pp 107 | -- templates/good.epp 108 | -- spec/facter/facter_spec.rb 109 | -- lib/good.rb 110 | -- templates/no_method_error.erb 111 | -- templates/good.erb 112 | -- hieradata/good.yaml 113 | -- metadata.json 114 | -- hieradata/good.json 115 | -- metadata_good/metadata.json 116 | -- librarian_good/Puppetfile 117 | 118 | The following files have unrecognized formats and therefore were not processed: 119 | -- foobarbaz 120 | ``` 121 | 122 | ### What About Puppet Development Kit? 123 | The fairly recent release of the Puppet Development Kit (PDK) will hopefully eventually bring about the capability to test and validate your Puppet code and data in a streamlined, efficient, comprehensive, and accurate fashion comparable to Puppet Check. Unfortunately, the PDK has not yet achieved feature or efficiency parity with Puppet Check. The goal is for the PDK to one day replace Puppet Check and for Puppet Check to enter maintenance mode, but for now Puppet Check is still needed to lead Puppet testing. 124 | 125 | ### What About PDK now? 126 | As of version 2.4.0 of the PDK, the PDK has essentially more or less achieved feature parity with Puppet Check. Although the PDK is not as efficient (including especially that Puppet Check executes significantly faster), it is still supported by Puppetlabs. Therefore, if you need an efficient and comprehensive Puppet validation solution, then you can still utilize Puppet Check, but the PDK is a recommended alternative for the future. 127 | 128 | ## Usage 129 | Please see the [Gemspec](puppet-check.gemspec) for dependency information. All other dependencies should be fine with various versions. Puppet Check can be used with a CLI, Rake tasks, or API, from your system, rbenv, rvm, Docker, or Vagrant. Please note all interfaces (API by default, but can be modified) will ignore any directories named `fixtures`, or specified paths with that directory during file checks and spec tests. 130 | 131 | ### CLI 132 | ``` 133 | usage: puppet-check [options] paths 134 | --version Display the current version. 135 | --fail-on-warnings Fail on warnings 136 | -s, --style Enable style checks 137 | --smoke Enable smoke testing 138 | -r, --regression Enable regression testing (in progress, do not use) 139 | --public cert.pem Public key for EYAML checks 140 | --private cert.pem Private key for EYAML checks 141 | -o, --output format Format for results output (default is text): text, json, or yaml 142 | --octoconfig config_file Octocatalog-diff configuration file to use 143 | -n node1.example.com,node2.example.com, 144 | --octonodes Octocatalog-diff nodes to test catalog on 145 | --puppet-lint arg_one,arg_two 146 | Arguments for PuppetLint ignored checks 147 | -c, --config file Load PuppetLint options from file 148 | --rubocop arg_one,arg_two Arguments for Rubocop disabled cops 149 | ``` 150 | 151 | The command line interface enables the ability to select additional style checks besides the syntax checks, and to specify PuppetLint and Rubocop checks to ignore. If you require a more robust interface to PuppetLint, Rubocop, and Reek, then please use `.puppet-lint.rc`, `.rubocop.yml` and `*.reek` config files. The `.puppet-lint.rc` can be specified with the `-c` argument. If it is not specified, then PuppetLint will automatically load one from `.puppet-lint.rc`, `~/.puppet-lint.rc`, or `/etc/puppet-lint.rc`, in that order of preference. The nearest `.rubocop.yml` and `*.reek` will be automatically respected. 152 | 153 | Example: 154 | ``` 155 | puppet-check -s --puppet-lint no-hard_tabs-check,no-140chars-check --rubocop Metrics/LineLength,Style/Encoding -o yaml path/to/code_and_data 156 | ``` 157 | 158 | ### Rake 159 | Interfacing with Puppet-Check via `rake` requires a `require puppet-check/tasks` in your Rakefile. This generates the following `rake` commands: 160 | 161 | ``` 162 | rake puppetcheck # Execute all Puppet-Check checks 163 | rake puppetcheck:file # Execute Puppet-Check file checks 164 | rake puppetcheck:spec # Execute RSpec and RSpec-Puppet tests 165 | rake puppetcheck:beaker # Execute Beaker acceptance tests 166 | rake puppetcheck:kitchen:* # Execute Test Kitchen acceptance tests 167 | ``` 168 | 169 | #### puppetcheck:file 170 | You can add style, smoke, and regression checks to the `rake puppetcheck:file`, or change the output format, by adding the following after the require: 171 | 172 | ```ruby 173 | # example of modifying Puppet Check behavior and creating a custom task 174 | settings = {} 175 | settings[:fail_on_warnings] = true # default false 176 | settings[:style] = true # default false 177 | settings[:smoke] = true # default false 178 | settings[:regression] = true # in progress, do not use; default false 179 | settings[:public] = 'public.pem' # default nil 180 | settings[:private] = 'private.pem' # default nil 181 | settings[:output_format] = 'yaml' # also 'json'; default 'text' 182 | settings[:octoconfig] = '$HOME/octocatalog-diff.cfg.rb' # default '.octocatalog-diff.cfg.rb' 183 | settings[:octonodes] = %w(server.example.com) # default: %w(localhost.localdomain) 184 | settings[:puppetlint_args] = ['--puppetlint-arg-one', '--puppetlint-arg-two'] # default [] 185 | settings[:rubocop_args] = ['--except', 'rubocop-arg-one,rubocop-arg-two'] # default [] 186 | 187 | desc 'Execute custom Puppet-Check file checks' 188 | task :file_custom do 189 | Rake::Task[:'puppetcheck:file'].invoke(settings) 190 | end 191 | ``` 192 | 193 | Please note that `rspec` does not support yaml output and therefore would still use the default 'progress' formatter even if `yaml` is specified as the format option to Puppet Check. 194 | 195 | The style checks from within `rake puppetcheck:file` are directly interfaced to `puppet-lint`, `rubocop`, and `reek`. This means that all arguments and options should be specified from within your `.puppet-lint.rc`, `.rubocop.yml`, and `*.reek`. However, you can alternatively utilize the hashes listed above. 196 | 197 | #### puppetcheck:spec 198 | The spec tests will be executed against everything that matches the pattern `**/{classes, defines, facter, functions, hosts, puppet, unit, types}/**/*_spec.rb`. Any of these directories inside of a `fixtures` directory will be ignored. This means everything in the current path that appears to be a Puppet module spec test for your module (not dependencies) will be regarded as such and executed during this rake task. 199 | 200 | Please note it is perfectly acceptable to only execute standard RSpec tests in your modules and not use the extended RSpec Puppet matchers. If no Puppet module directories are identified during directory parsing, then no RSpec Puppet related actions (including those described below) will be performed. 201 | 202 | Prior to executing the spec tests, Puppet Check will parse everything in the current path and identify all `spec` directories not within `fixtures` directories. It will then execute RSpec Puppet setup actions inside all directories one level above that contain a `manifests` directory. This is assumed to be a Puppet module directory. These setup actions include creating all of the necessary directories inside of `spec/fixtures`, creating a blank `site.pp` if it is missing, symlinking everything from the module that is needed into fixtures (automatically replaces functionality of self module symlink in `.fixtures.yaml` from Puppetlabs Spec Helper), and creates the `spec_helper.rb` if it is missing. Note these setup actions can replace `rspec-puppet-init` from RSpec Puppet and currently are both faster and more accurate. 203 | 204 | Puppet Check will also automatically download specified external module dependencies for and during RSpec Puppet testing. Currently `git`, `puppet forge`, `svn`, and `hg` commands are supported. They can be implemented in the following way in your modules' `metadata.json`: 205 | 206 | ```json 207 | "dependencies": [ 208 | { 209 | "name": "module-name", 210 | "forge": "forge-name", 211 | "args": "puppet module install optional-arguments" 212 | }, 213 | { 214 | "name": "module-name", 215 | "git": "git-url", 216 | "args": "git clone optional-arguments" 217 | }, 218 | { 219 | "name": "module-name", 220 | "hg": "hg-url", 221 | "args": "hg clone optional-arguments" 222 | }, 223 | { 224 | "name": "module-name", 225 | "svn": "svn-url", 226 | "args": "svn co optional arguments" 227 | } 228 | ] 229 | ``` 230 | 231 | Example: 232 | 233 | ```json 234 | "dependencies": [ 235 | { 236 | "name": "puppetlabs/stdlib", 237 | "forge": "puppetlabs-stdlib", 238 | "args": "--do-something-cool" 239 | }, 240 | { 241 | "name": "puppetlabs/lvm", 242 | "git": "https://github.com/puppetlabs/puppetlabs-lvm.git" 243 | } 244 | ] 245 | ``` 246 | 247 | Note that `args` will be ignored during `git pull`, `svn update`, and `hg pull/hg update` when the modules are updated instead of freshly cloned. 248 | 249 | #### puppetcheck:beaker 250 | This task serves as a frontend to the `beaker_quickstart:run_test[hypervisor]` rake task that Beaker provides. It merely provides a convenient unified frontend for the task and automated as part of the `puppetcheck` tasks. Note that you should still provide a hypervisor argument to the rake task when executed individually (e.g. `rake puppetcheck:beaker[vagrant]`). The Vagrant hypervisor will be selected by default when executed as part of the `puppetcheck` task. Vagrant will also be selected by default if no hypervisor argument is provided to the individual task. 251 | 252 | #### puppetcheck:kitchen 253 | This task serves as a frontend to the `kitchen:all` rake task that Test Kitchen provides. It merely provides a convenient unified frontend for the task and automated as part of the `puppetcheck` tasks. 254 | 255 | ### API 256 | 257 | If you are performing your Puppet testing from within a Ruby script or your own custom Rakefile tasks, and want to execute Puppet Check intrinsically from the Ruby script or Rakefile, then you can call its API in the following simple way: 258 | 259 | ```ruby 260 | # file checks 261 | require 'puppet-check' 262 | 263 | settings = {} 264 | settings[:fail_on_warnings] = true # default false 265 | settings[:style] = true # default false 266 | settings[:smoke] = true # default false 267 | settings[:regression] = true # in progress, do not use; default false 268 | settings[:public] = 'public.pem' # default nil 269 | settings[:private] = 'private.pem' # default nil 270 | settings[:output_format] = 'yaml' # also 'json'; default 'text' 271 | settings[:octoconfig] = '$HOME/octocatalog-diff.cfg.rb' # default '.octocatalog-diff.cfg.rb' 272 | settings[:octonodes] = %w(server.example.com) # default: %w(localhost.localdomain) 273 | settings[:puppetlint_args] = ['--puppetlint-arg-one', '--puppetlint-arg-two'] # default [] 274 | settings[:rubocop_args] = ['--except', 'rubocop-arg-one,rubocop-arg-two'] # default [] 275 | 276 | PuppetCheck.new.run(settings, [dirs, files]) 277 | 278 | # rspec checks (as part of a RSpec::Core::RakeTask.new block with |task|) 279 | require 'puppet-check/rspec_puppet_support' 280 | 281 | RSpecPuppetSupport.run 282 | task.pattern = Dir.glob('**/{classes,defines,facter,functions,hosts,puppet,unit,types}/**/*_spec.rb').grep_v(/fixtures/) 283 | ``` 284 | 285 | ### Docker 286 | 287 | A supported [Docker image](https://hub.docker.com/r/matthewschuchard/puppet-check) of Puppet-Check is now available from the public Docker Hub registry. Please consult the repository documentation for further usage information. 288 | 289 | ### Vagrant 290 | 291 | As an alternative to Docker, you can also use Vagrant for quick and disposable testing, but it is not as portable as Docker for these testing purposes. Below is an example `Vagrantfile` for this purpose. 292 | 293 | ```ruby 294 | Vagrant.configure(2) do |config| 295 | # a reliable and small box at the moment 296 | config.vm.box = 'fedora/35-cloud-base' 297 | 298 | config.vm.provision 'shell', inline: <<-SHELL 299 | # cd to '/vagrant' 300 | cd /vagrant 301 | # you need ruby and any other extra dependencies that come from packages; in this example we install git to use it for downloading external module dependencies 302 | sudo dnf install ruby rubygems git -y 303 | # you need puppet-check and any other extra dependencies that come from gems; in this example we install rspec-puppet and rake for extra testing 304 | sudo gem install --no-document puppet-check rspec-puppet rake 305 | # this is needed for the ruby json parser to not flip out on fresh os installs for some reason (change encoding value as necessary) 306 | export LANG='en_US.UTF-8' 307 | # execute your tests; in this example we are executing the full suite of tests 308 | rake puppetcheck 309 | SHELL 310 | end 311 | ``` 312 | 313 | To overcome the lack of convenient portability, you could try spinning up the Vagrant instance at the top level of your Puppet code and data and then descend into directories to execute tests as necessary. Cleverness or patience will be necessary if you decide to use Vagrant for testing and desire portability. 314 | 315 | ### Exit Codes 316 | - 0: PuppetCheck exited with no internal exceptions or errors in your code and data. 317 | - 1: PuppetCheck exited with an internal exception (takes preference over other non-zero exit codes) or failed spec test(s). 318 | - 2: PuppetCheck exited with one or more errors in your code and data. Alternatively, PuppetCheck exited with one or more warnings in your code and data, and you specified to fail on warnings. 319 | 320 | ### Optional Dependencies 321 | - **rake** (gem): install this if you want to use Puppet Check with `rake` tasks in addition to the CLI. 322 | - **rspec** (gem): install this if you want to use Puppet Check to execute the spec tests for your Ruby files during `rake`. 323 | - **rspec-puppet** (gem): install this if you want to use Puppet Check to execute the spec tests for your Puppet files during `rake`. 324 | - **octocatalog-diff** (gem): install a version `>= 1.0` of this if you want to use Puppet Check to execute smoke or regression tests for your Puppet catalog. 325 | - **beaker** (gem): install this if you want to use Puppet Check to execute the Beaker acceptance tests during `rake`. 326 | - **test-kitchen** (gem): install this if you want to use Puppet Check to execute the Test Kitchen acceptance tests during `rake`. 327 | - **git** (pkg): install this if you want to use Puppet Check to download external module dependencies with `git` commands during RSpec Puppet testing. 328 | - **mercurial** (pkg): install this if you want to use Puppet Check to download external module dependencies with `hg` commands during RSpec Puppet testing. 329 | - **subversion** (pkg): install this if you want to use Puppet Check to download external module dependencies with `svn` commands during RSpec Puppet testing. 330 | 331 | ## Contributing 332 | Code should pass all spec tests. New features should involve new spec tests. Adherence to Rubocop and Reek is expected where not overly onerous or where the check is of dubious cost/benefit. 333 | 334 | A [Dockerfile](Dockerfile) is provided for easy rake testing. A [Vagrantfile](Vagrantfile) is provided for easy gem building, installation, and post-installation testing. 335 | 336 | Please consult the GitHub Project for the current development roadmap. 337 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | require 'rubocop/rake_task' 3 | require 'reek/rake/task' 4 | 5 | task default: %i[rubocop reek unit system] 6 | 7 | RuboCop::RakeTask.new(:rubocop) do |task| 8 | task.formatters = ['simple'] 9 | task.requires << 'rubocop-performance' 10 | task.fail_on_error = false 11 | end 12 | 13 | Reek::Rake::Task.new do |task| 14 | task.fail_on_error = false 15 | end 16 | 17 | desc 'Execute unit spec tests' 18 | RSpec::Core::RakeTask.new(:unit) do |task| 19 | task.pattern = 'spec/{puppet-check_spec.rb, puppet-check/*_spec.rb}' 20 | end 21 | 22 | desc 'Execute system spec tests' 23 | RSpec::Core::RakeTask.new(:system) do |task| 24 | task.pattern = 'spec/system/*_spec.rb' 25 | end 26 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # PuppetCheck: testing gem build, install, and execution 2 | Vagrant.configure(2) do |config| 3 | config.vm.box = 'debian/bookworm64' 4 | 5 | config.vm.provision 'shell', inline: <<-SHELL 6 | cd /vagrant 7 | apt-get install -y ruby-dev make gcc 8 | gem build puppet-check.gemspec 9 | gem install --no-document puppet-check*.gem 10 | rm -f puppet-check*.gem 11 | cd spec/fixtures 12 | /usr/local/bin/puppet-check -s . 13 | echo $? 14 | SHELL 15 | end 16 | -------------------------------------------------------------------------------- /bin/puppet-check: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../lib/puppet-check/cli' 3 | 4 | exit PuppetCheck::CLI.run(ARGV) 5 | -------------------------------------------------------------------------------- /images/puppetcheck_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschuchard/puppet-check/683ba2b2609d1b26576544835657b6303551318b/images/puppetcheck_new.png -------------------------------------------------------------------------------- /images/puppetcheck_old.odp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschuchard/puppet-check/683ba2b2609d1b26576544835657b6303551318b/images/puppetcheck_old.odp -------------------------------------------------------------------------------- /images/puppetcheck_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschuchard/puppet-check/683ba2b2609d1b26576544835657b6303551318b/images/puppetcheck_old.png -------------------------------------------------------------------------------- /lib/puppet-check/cli.rb: -------------------------------------------------------------------------------- 1 | require_relative '../puppet_check' 2 | 3 | # the command line interface for PuppetCheck 4 | class PuppetCheck::CLI 5 | # run method for the cli 6 | def self.run(args) 7 | # gather the user arguments 8 | settings = parse(args) 9 | 10 | # target cwd if no paths input as variadic 11 | args = [Dir.pwd] if args.empty? 12 | 13 | # run PuppetCheck with specified paths 14 | PuppetCheck.new.run(settings, args) 15 | end 16 | 17 | # parse the user arguments 18 | def self.parse(args) 19 | private_class_method :method 20 | require 'optparse' 21 | 22 | # show help message if no args specified 23 | args = %w[-h] if args.empty? 24 | 25 | # initialize settings hash 26 | settings = {} 27 | 28 | opt_parser = OptionParser.new do |opts| 29 | # usage 30 | opts.banner = 'usage: puppet-check [options] paths' 31 | 32 | # base options 33 | opts.on('--version', 'Display the current version.') do 34 | require 'rubygems' 35 | 36 | puts Gem::Specification.load("#{File.dirname(__FILE__)}/../../puppet-check.gemspec").version 37 | exit 0 38 | end 39 | 40 | # bool options 41 | opts.on('--fail-on-warnings', 'Fail on warnings') { settings[:fail_on_warnings] = true } 42 | opts.on('-s', '--style', 'Enable style checks') { settings[:style] = true } 43 | opts.on('--smoke', 'Enable smoke testing') { settings[:smoke] = true } 44 | opts.on('-r', '--regression', 'Enable regression testing (in progress, do not use)') { settings[:regression] = true } 45 | 46 | # ssl key options for eyaml checks 47 | opts.on('--public cert.pem', String, 'Public key for EYAML checks') { |arg| settings[:public] = arg } 48 | opts.on('--private cert.pem', String, 'Private key for EYAML checks') { |arg| settings[:private] = arg } 49 | 50 | # formatting options 51 | opts.on('-o', '--output format', String, 'Format for results output (default is text): text, json, or yaml') { |arg| settings[:output_format] = arg } 52 | 53 | # octocatalog-diff options 54 | opts.on('--octoconfig config_file', String, 'Octocatalog-diff configuration file to use') { |arg| settings[:octoconfig] = arg } 55 | opts.on('-n', '--octonodes node1.example.com,node2.example.com', Array, 'Octocatalog-diff nodes to test catalog on') { |arg| settings[:octonodes] = arg } 56 | 57 | # arguments to style checkers 58 | opts.on('--puppet-lint arg_one,arg_two', Array, 'Arguments for PuppetLint ignored checks') do |puppetlint_args| 59 | settings[:puppetlint_args] = puppetlint_args.map { |arg| "--#{arg}" } 60 | end 61 | opts.on('-c', '--config file', String, 'Load PuppetLint options from file') do |file| 62 | settings[:puppetlint_args] = File.read(file).split("\n") 63 | end 64 | opts.on('--rubocop arg_one,arg_two', String, 'Arguments for Rubocop disabled cops') { |arg| settings[:rubocop_args] = ['--except', arg] } 65 | end 66 | 67 | # remove matched args and return settings 68 | opt_parser.parse!(args) 69 | settings 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/puppet-check/data_parser.rb: -------------------------------------------------------------------------------- 1 | require_relative '../puppet_check' 2 | 3 | # executes diagnostics on data files 4 | class DataParser 5 | require 'yaml' 6 | 7 | # checks yaml (.yaml/.yml) 8 | def self.yaml(files) 9 | files.each do |file| 10 | # check yaml syntax 11 | parsed = YAML.load_file(file) 12 | rescue StandardError => err 13 | PuppetCheck.files[:errors][file] = err.to_s.gsub("(#{file}): ", '').split("\n") 14 | else 15 | warnings = [] 16 | 17 | # perform some rudimentary hiera checks if data exists and is hieradata 18 | warnings = hiera(parsed, file) if parsed && (File.basename(file) != 'hiera.yaml') 19 | 20 | next PuppetCheck.files[:warnings][file] = warnings unless warnings.empty? 21 | PuppetCheck.files[:clean].push(file.to_s) 22 | end 23 | end 24 | 25 | # checks eyaml (.eyaml/.eyml) 26 | def self.eyaml(files, public, private) 27 | require 'openssl' 28 | 29 | # keys specified? 30 | if public.nil? || private.nil? 31 | PuppetCheck.files[:ignored].concat(files) 32 | return warn 'Public X509 and/or Private RSA PKCS7 certs were not specified. EYAML checks will not be executed.' 33 | end 34 | 35 | # keys exist? 36 | unless File.file?(public) && File.file?(private) 37 | PuppetCheck.files[:ignored].concat(files) 38 | return warn 'Specified Public X509 and/or Private RSA PKCS7 certs do not exist. EYAML checks will not be executed.' 39 | end 40 | 41 | # setup decryption 42 | rsa = OpenSSL::PKey::RSA.new(File.read(private)) 43 | x509 = OpenSSL::X509::Certificate.new(File.read(public)) 44 | 45 | files.each do |file| 46 | # check encoded yaml syntax 47 | parsed = YAML.load_file(file) 48 | 49 | # extract encoded values 50 | # ENC[PKCS7] 51 | 52 | # decrypt the encoded yaml 53 | # decrypted = OpenSSL::PKCS7.new(File.read(file)).decrypt(rsa, x509) 54 | 55 | # check decoded eyaml syntax 56 | # decoded = YAML.safe_load(decrypted) 57 | 58 | # merge data hashes 59 | # parsed = merge(parsed, decoded) 60 | rescue StandardError => err 61 | PuppetCheck.files[:errors][file] = err.to_s.gsub("(#{file}): ", '').split("\n") 62 | else 63 | warnings = [] 64 | 65 | # perform some rudimentary hiera checks if data exists and is hieradata 66 | warnings = hiera(parsed, file) if parsed 67 | 68 | next PuppetCheck.files[:warnings][file] = warnings unless warnings.empty? 69 | PuppetCheck.files[:clean].push(file.to_s) 70 | end 71 | end 72 | 73 | # metadata consts 74 | REQUIRED_KEYS = %w[name version author license summary source dependencies].freeze 75 | REQ_DEP_KEYS = %w[requirements dependencies].freeze 76 | DEPRECATED_KEYS = %w[types checksum].freeze 77 | TASK_INPUTS = %w[environment stdin powershell].freeze 78 | 79 | # checks json (.json) 80 | def self.json(files) 81 | require 'json' 82 | 83 | files.each do |file| 84 | # check json syntax 85 | parsed = JSON.parse(File.read(file)) 86 | rescue JSON::ParserError => err 87 | PuppetCheck.files[:errors][file] = err.to_s.lines.first.strip.split("\n") 88 | else 89 | warnings = [] 90 | 91 | # check metadata.json 92 | if File.basename(file) == 'metadata.json' 93 | # metadata-json-lint has issues and is essentially no longer maintained, so here is an improved and leaner version of it 94 | require 'rubygems/util/licenses' 95 | 96 | # check for errors 97 | errors = [] 98 | 99 | # check for required keys 100 | REQUIRED_KEYS.each do |key| 101 | errors.push("Required field '#{key}' not found.") unless parsed.key?(key) 102 | end 103 | 104 | # check requirements and dependencies keys 105 | REQ_DEP_KEYS.each do |key| 106 | # skip if key is missing or value is an empty string, array, or hash 107 | next if !parsed.key?(key) || parsed[key].empty? 108 | 109 | # check that dependencies and requirements are an array of hashes 110 | next errors.push("Field '#{key}' is not an array of hashes.") unless (parsed[key].is_a? Array) && (parsed[key][0].is_a? Hash) 111 | 112 | # check dependencies and requirements values 113 | names = [] 114 | parsed[key].each do |req_dep| 115 | # check for duplicate dependencies and requirements 116 | name = req_dep['name'] 117 | next errors.push("Duplicate #{key} on #{name}.") if names.include?(name) 118 | names << name 119 | 120 | # warn and skip if key is missing 121 | next warnings.push("'#{req_dep['name']}' is missing a 'version_requirement' key.") if req_dep['version_requirement'].nil? 122 | 123 | # warn and skip if no upper bound 124 | next warnings.push("'#{req_dep['name']}' is missing an upper bound.") unless req_dep['version_requirement'].include?('<') 125 | 126 | # check for semantic versioning 127 | if key == 'dependencies' && req_dep['version_requirement'] !~ /\d+\.\d+\.\d+.*\d+\.\d+\.\d+/ 128 | warnings.push("'#{req_dep['name']}' has non-semantic versioning in its 'version_requirement' key.") 129 | end 130 | end 131 | end 132 | 133 | # check for deprecated fields 134 | DEPRECATED_KEYS.each do |key| 135 | errors.push("Deprecated field '#{key}' found.") if parsed.key?(key) 136 | end 137 | 138 | # check for summary under 144 character 139 | errors.push('Summary exceeds 144 characters.') if parsed.key?('summary') && parsed['summary'].size > 144 140 | 141 | next PuppetCheck.files[:errors][file] = errors unless errors.empty? 142 | 143 | # check for warnings 144 | # check for operatingsystem_support hash array 145 | if parsed.key?('operatingsystem_support') 146 | # check if operatingsystem_support array is actually empty 147 | if !(parsed['operatingsystem_support'].is_a? Array) || parsed['operatingsystem_support'].empty? || (!parsed['operatingsystem_support'].empty? && !(parsed['operatingsystem_support'][0].is_a? Hash)) 148 | warnings.push('Recommended field \'operatingsystem\' not found.') 149 | warnings.push('Recommended field \'operatingsystemrelease\' not found.') 150 | else 151 | # check for operatingsystem string 152 | if parsed['operatingsystem_support'][0].key?('operatingsystem') 153 | warnings.push('Field \'operatingsystem\' is not a string.') unless parsed['operatingsystem_support'][0]['operatingsystem'].is_a? String 154 | else 155 | warnings.push('Recommended field \'operatingsystem\' not found.') 156 | end 157 | 158 | # check for operatingsystemrelease string array 159 | if parsed['operatingsystem_support'][0].key?('operatingsystemrelease') 160 | warnings.push('Field \'operatingsystemrelease\' is not a string array.') unless parsed['operatingsystem_support'][0]['operatingsystemrelease'][0].is_a? String 161 | else 162 | warnings.push('Recommended field \'operatingsystemrelease\' not found.') 163 | end 164 | end 165 | else 166 | warnings.push('Recommended field \'operatingsystem_support\' not found.') 167 | end 168 | 169 | # check for spdx license 170 | if parsed.key?('license') && !Gem::Licenses.match?(parsed['license']) && parsed['license'] !~ /[pP]roprietary/ 171 | warnings.push("License identifier '#{parsed['license']}' is not in the SPDX list: http://spdx.org/licenses/") 172 | end 173 | # assume this is task metadata if it has this key 174 | elsif parsed.key?('description') 175 | # check that description is a string 176 | warnings.push('description value is not a String') unless parsed['description'].is_a?(String) 177 | # check that input_method is one of three possible values 178 | if parsed.key?('input_method') 179 | if parsed['input_method'].is_a?(String) 180 | warnings.push('input_method value is not one of environment, stdin, or powershell') unless TASK_INPUTS.include?(parsed['input_method']) 181 | else 182 | warnings.push('input_method value is not a String') 183 | end 184 | end 185 | # check that parameters is a hash 186 | if parsed.key?('parameters') && !parsed['parameters'].is_a?(Hash) 187 | warnings.push('parameters value is not a Hash') 188 | end 189 | # check that puppet_task_version is an integer 190 | if parsed.key?('puppet_task_version') && !parsed['puppet_task_version'].is_a?(Integer) 191 | warnings.push('puppet_task_version value is not an Integer') 192 | end 193 | # check that supports_noop is a boolean 194 | if parsed.key?('supports_noop') && !(parsed['supports_noop'].is_a?(TrueClass) || parsed['supports_noop'].is_a?(FalseClass)) 195 | warnings.push('supports_noop value is not a Boolean') 196 | end 197 | # assume this is hieradata and ensure it is non-empty 198 | elsif parsed 199 | # perform some rudimentary hiera checks if data exists 200 | warnings = hiera(parsed, file) 201 | end 202 | next PuppetCheck.files[:warnings][file] = warnings unless warnings.empty? 203 | PuppetCheck.files[:clean].push(file.to_s) 204 | end 205 | end 206 | 207 | # checks hieradata 208 | def self.hiera(data, file) 209 | private_class_method :method 210 | warnings = [] 211 | 212 | # disregard nil/undef value data check if default values (common) 213 | unless /^common/.match?(file) 214 | data.each do |key, value| 215 | # check for nil values in the data (nil keys are fine) 216 | if (value.is_a?(Hash) && value.values.any?(&:nil?)) || value.nil? 217 | warnings.push("Value(s) missing in key '#{key}'.") 218 | end 219 | end 220 | end 221 | 222 | # check that '---' does not show up more than once in the hieradata 223 | warnings.push('The string --- appears more than once in this data and Hiera may fail to parse it correctly.') if File.read(file).scan('---').count >= 2 224 | 225 | warnings 226 | end 227 | end 228 | -------------------------------------------------------------------------------- /lib/puppet-check/output_results.rb: -------------------------------------------------------------------------------- 1 | require_relative '../puppet_check' 2 | 3 | # class to handle outputting diagnostic results in desired format 4 | class OutputResults 5 | HEADER = { 6 | errors: "\033[31mThe following files have errors:\033[0m\n", 7 | warnings: "\033[33mThe following files have warnings:\033[0m\n", 8 | clean: "\033[32mThe following files have no errors or warnings:\033[0m\n-- ", 9 | ignored: "\033[36mThe following files have unrecognized formats and therefore were not processed:\033[0m\n-- " 10 | }.freeze 11 | 12 | # output the results in various formats 13 | def self.run(files, format) 14 | # remove empty entries 15 | files.delete_if { |_, sorted_files| sorted_files.empty? } 16 | 17 | # output hash according to specified format 18 | case format 19 | when 'text' 20 | text(files) 21 | when 'yaml' 22 | require 'yaml' 23 | # maintain filename format consistency among output formats 24 | files.transform_keys!(&:to_s) 25 | puts Psych.dump(files, indentation: 2) 26 | when 'json' 27 | require 'json' 28 | puts JSON.pretty_generate(files) 29 | else 30 | raise "puppet-check: Unsupported output format '#{format}' was specified." 31 | end 32 | end 33 | 34 | # output the results as text 35 | def self.text(files) 36 | private_class_method :method 37 | 38 | # output text for each of four file categories 39 | %i[errors warnings clean ignored].each do |category| 40 | # immediately return if category is empty 41 | next unless files.key?(category) 42 | 43 | # display heading, files, and file messages per category for text formatting 44 | category_files = files[category] 45 | 46 | # display category heading 47 | print HEADER[category] 48 | 49 | # display files and optionally messages 50 | case category_files 51 | when Hash then category_files.each { |file, messages| puts "-- #{file}:\n#{messages.join("\n")}" } 52 | when Array then puts category_files.join("\n-- ") 53 | else raise "puppet-check: The files category was of unexpected type #{category_files.class}. Please file an issue with this log message, category heading, and information about the parsed files." 54 | end 55 | 56 | # newline between categories for easier visual parsing 57 | puts '' 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/puppet-check/puppet_parser.rb: -------------------------------------------------------------------------------- 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, style, pl_args) 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 | 13 | # prepare the PuppetLint object for style checks 14 | if style 15 | require 'puppet-lint' 16 | require 'puppet-lint/optparser' 17 | puppet_lint = PuppetLint.new 18 | end 19 | 20 | files.each do |file| 21 | # setup error logging and collection; warnings logged for all versions, but errors for only puppet < 6.5 22 | errors = [] 23 | Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(errors)) 24 | 25 | # check puppet syntax 26 | begin 27 | # initialize message 28 | messages = [] 29 | # specify tasks attribute for parser validation if this looks like a plan or not 30 | Puppet[:tasks] = file.match?(%r{plans/\w+\.pp$}) 31 | # in puppet >= 6.5 the return of this method is a hash with the error 32 | new_error = Puppet::Face[:parser, :current].validate(file) 33 | # puppet 6.5 output format is now a hash from the face api 34 | if Gem::Version.new(Puppet::PUPPETVERSION) >= Gem::Version.new('6.5.0') && new_error != {} 35 | messages.concat(new_error.values.map(&:to_s).map { |error| error.gsub(/ \(file: #{File.absolute_path(file)}(, |\))/, '') }.map { |error| error.gsub('Could not parse for environment *root*: ', '') }) 36 | end 37 | # this is the actual error that we need to rescue Puppet::Face from 38 | rescue SystemExit 39 | # puppet 5.4-6.4 has a new validator output format and eof errors have fake dir env info 40 | messages.concat(errors.map(&:to_s).join("\n").map { |error| error.gsub(/file: #{File.absolute_path(file)}(, |\))/, '') }.map { |error| error.gsub(/Could not parse.*: /, '') }) 41 | end 42 | 43 | Puppet::Util::Log.close_all 44 | 45 | # store info and continue validating files 46 | next PuppetCheck.files[:errors][file] = messages unless messages.empty? 47 | 48 | # initialize warnings with output from the parser if it exists, since the output is warnings if Puppet::Face did not trigger a SystemExit 49 | warnings = [] 50 | # weirdly puppet >= 6.5 still does not return warnings and logs them instead unlike errors 51 | unless errors.empty? 52 | # puppet >= 5.4 has a new validator output format 53 | warnings.concat(errors.map(&:to_s).join("\n").gsub("file: #{File.absolute_path(file)}, ", '').split("\n")) 54 | end 55 | 56 | # check puppet style 57 | if style 58 | # check for invalid arguments to PuppetLint 59 | begin 60 | PuppetLint::OptParser.build.parse!(pl_args.clone) 61 | rescue OptionParser::InvalidOption 62 | raise "puppet-lint: invalid option supplied among #{pl_args.join(' ')}" 63 | end 64 | 65 | # execute puppet-lint style checks 66 | puppet_lint.file = file 67 | puppet_lint.run 68 | 69 | # collect the warnings 70 | offenses = puppet_lint.problems.map { |problem| "#{problem[:line]}:#{problem[:column]} #{problem[:message]}" } 71 | warnings.concat(offenses) 72 | end 73 | next PuppetCheck.files[:warnings][file] = warnings unless warnings.empty? 74 | PuppetCheck.files[:clean].push(file.to_s) 75 | end 76 | end 77 | 78 | # checks puppet template (.epp) 79 | def self.template(files) 80 | require 'puppet/pops' 81 | 82 | files.each do |file| 83 | # check puppet template syntax 84 | # credits to gds-operations/puppet-syntax for the parser function call 85 | Puppet::Pops::Parser::EvaluatingParser::EvaluatingEppParser.new.parse_file(file) 86 | rescue StandardError => err 87 | PuppetCheck.files[:errors][file] = [err.to_s.gsub("file: #{file}, ", '')] 88 | else 89 | PuppetCheck.files[:clean].push(file.to_s) 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/puppet-check/regression_check.rb: -------------------------------------------------------------------------------- 1 | # temporarily supress warning messages for octocatalog-diff redefining puppet constants and then reactivate 2 | $VERBOSE = nil 3 | require 'octocatalog-diff' 4 | $VERBOSE = false 5 | 6 | # executes smoke and regression tests on catalogs 7 | class RegressionCheck 8 | # smoke testing 9 | def self.smoke(interface_nodes, octoconfig) 10 | options = config(octoconfig) 11 | nodes = options.key?(:node) ? [options[:node]] : interface_nodes 12 | nodes.each do |node| 13 | options[:node] = node 14 | OctocatalogDiff::API::V1.catalog(options) 15 | end 16 | end 17 | 18 | # regression testing 19 | # def self.regression(nodes, octoconfig) 20 | # options = RegressionCheck.config(octoconfig) 21 | # nodes.each { |node| stuff } 22 | # end 23 | 24 | # config file loading 25 | def self.config(octoconfig) 26 | private_class_method :method 27 | OctocatalogDiff::API::V1.config(filename: octoconfig) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/puppet-check/rspec_puppet_support.rb: -------------------------------------------------------------------------------- 1 | # class to prepare spec directory for rspec puppet testing 2 | class RSpecPuppetSupport 3 | # code diagram: 4 | # 'puppetcheck:spec' task invokes 'run' 5 | # 'run' invokes 'file_setup' always and 'dependency_setup' if metadata.json exists 6 | # 'dependency_setup' invokes 'git/forge/svn/hg' if dependencies exist and git/forge/svn/hg is download option 7 | # 'git/forge/svn/hg' downloads module fixture appropriately 8 | 9 | # prepare the spec fixtures directory for rspec-puppet testing 10 | def self.run 11 | # ensure this method does not do anything inside module dependencies 12 | specdirs = Dir.glob('**/spec').reject { |dir| dir.include?('fixtures') } 13 | return if specdirs.empty? 14 | 15 | # setup fixtures for rspec-puppet testing 16 | specdirs.each do |specdir| 17 | # skip to next specdir if it does not seem like a puppet module 18 | next unless File.directory?("#{specdir}/../manifests") 19 | 20 | # change to module directory 21 | Dir.chdir("#{specdir}/..") 22 | 23 | # grab the module name from the directory name of the module to pass to file_setup 24 | file_setup(File.basename(Dir.pwd)) 25 | 26 | # invoke dependency_setup for module dependencies if metadata.json present 27 | dependency_setup if File.file?('metadata.json') 28 | end 29 | end 30 | 31 | # setup the files, directories, and symlinks for rspec-puppet testing 32 | def self.file_setup(module_name) 33 | private_class_method :method 34 | # create all the necessary fixture dirs that are missing 35 | ['spec/fixtures', 'spec/fixtures/manifests', 'spec/fixtures/modules'].each do |dir| 36 | Dir.mkdir(dir) unless File.directory?(dir) 37 | end 38 | 39 | # create empty site.pp if missing 40 | File.write('spec/fixtures/manifests/site.pp', '') unless File.file?('spec/fixtures/manifests/site.pp') 41 | 42 | # symlink the module into spec/fixtures/modules 43 | if File.exist?("spec/fixtures/modules/#{module_name}") 44 | # check if target is a symlink 45 | if File.symlink?("spec/fixtures/modules/#{module_name}") 46 | # check if target is correct 47 | warn "spec/fixtures/modules/#{module_name} is not a symlink to the correct source! Your tests may fail because of this!" unless File.readlink("spec/fixtures/modules/#{module_name}") == File.expand_path("../../../../#{module_name}") 48 | else 49 | warn "spec/fixtures/modules/#{module_name} is not a symlink! Your tests may fail because of this!" 50 | end 51 | else 52 | File.symlink("../../../../#{module_name}", "spec/fixtures/modules/#{module_name}") 53 | end 54 | 55 | # create spec_helper if missing 56 | return if File.file?('spec/spec_helper.rb') 57 | File.open('spec/spec_helper.rb', 'w') { |file| file.puts "require 'rspec-puppet/spec_helper'\n" } 58 | end 59 | 60 | # setup the module dependencies for rspec-puppet testing 61 | def self.dependency_setup 62 | private_class_method :method 63 | require 'json' 64 | 65 | # parse the metadata.json (assumes DataParser.json has already given it a pass) 66 | parsed = JSON.parse(File.read('metadata.json')) 67 | 68 | # grab dependencies if they exist 69 | return unless parsed.key?('dependencies') 70 | parsed['dependencies'].each do |dependency_hash| 71 | # determine how the user wants to download the module dependency 72 | if dependency_hash.key?('git') 73 | git(dependency_hash['git'], dependency_hash['args']) 74 | elsif dependency_hash.key?('forge') 75 | forge(dependency_hash['forge'], dependency_hash['args']) 76 | elsif dependency_hash.key?('svn') 77 | svn(dependency_hash['svn'], dependency_hash['args']) 78 | elsif dependency_hash.key?('hg') 79 | hg(dependency_hash['hg'], dependency_hash['args']) 80 | else 81 | warn "#{dependency_hash['name']} has an unspecified, or specified but unsupported, download method." 82 | end 83 | end 84 | Process.waitall 85 | end 86 | 87 | # download external module dependency with git 88 | def self.git(git_url, args = '') 89 | private_class_method :method 90 | # establish path to clone module to 91 | path = "spec/fixtures/modules/#{File.basename(git_url, '.git')}" 92 | # is the module present and already cloned with git? do a pull; otherwise, do a clone 93 | begin 94 | File.directory?("#{path}/.git") ? spawn("git -C #{path} pull") : spawn("git clone #{args} #{git_url} #{path}") 95 | rescue Errno::ENOENT 96 | warn 'git is not installed and cannot be used to retrieve dependency modules' 97 | end 98 | end 99 | 100 | # download external module dependency with forge 101 | def self.forge(forge_name, args = '') 102 | private_class_method :method 103 | # is the module present? do an upgrade; otherwise, do an install 104 | subcommand = File.directory?("spec/fixtures/modules/#{forge_name}") ? 'upgrade' : 'install' 105 | spawn("puppet module #{subcommand} --modulepath spec/fixtures/modules/ #{args} #{forge_name}") 106 | end 107 | 108 | # download external module dependency with svn 109 | def self.svn(svn_url, args = '') 110 | private_class_method :method 111 | # establish path to checkout module to 112 | path = "spec/fixtures/modules/#{File.basename(svn_url)}" 113 | # is the module present and already checked out with svn? do an update; otherwise, do a checkout 114 | begin 115 | File.directory?("#{path}/.svn") ? spawn("svn update #{path}") : spawn("svn co #{args} #{svn_url} #{path}") 116 | rescue Errno::ENOENT 117 | warn 'subversion is not installed and cannot be used to retrieve dependency modules' 118 | end 119 | end 120 | 121 | # download external module dependency with hg 122 | def self.hg(hg_url, args = '') 123 | private_class_method :method 124 | # establish path to clone module to 125 | path = "spec/fixtures/modules/#{File.basename(hg_url)}" 126 | # is the module present and already cloned with hg? do a pull and update; otherwise do a clone 127 | begin 128 | File.directory?("#{path}/.hg") ? spawn("hg --cwd #{path} pull; hg --cwd #{path} update") : spawn("hg clone #{args} #{hg_url} #{path}") 129 | rescue Errno::ENOENT 130 | warn 'mercurial is not installed and cannot be used to retrieve dependency modules' 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/puppet-check/ruby_parser.rb: -------------------------------------------------------------------------------- 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, style, rc_args) 8 | # prepare rubocop object for style checks 9 | if style 10 | require 'json' 11 | require 'rubocop' 12 | require 'reek' 13 | require 'reek/cli/application' 14 | 15 | rubocop_cli = RuboCop::CLI.new 16 | end 17 | 18 | files.each do |file| 19 | # check ruby syntax 20 | # prevents ruby code from actually executing the input ruby file 21 | catch(:good) { instance_eval("BEGIN {throw :good}; #{File.read(file)} #BEGIN {throw :good}; ruby_file_content", file) } 22 | rescue ScriptError, StandardError => err 23 | PuppetCheck.files[:errors][file] = err.to_s.gsub("#{file}:", '').split("\n") 24 | else 25 | # check ruby style 26 | if style 27 | # check RuboCop and parse warnings' JSON output 28 | rubocop_warnings = Utils.capture_stdout { rubocop_cli.run(rc_args + ['--enable-pending-cops', '--require', 'rubocop-performance', '--format', 'json', file]) } 29 | rubocop_offenses = JSON.parse(rubocop_warnings)['files'][0]['offenses'].map { |warning| "#{warning['location']['line']}:#{warning['location']['column']} #{warning['message']}" } 30 | 31 | # check Reek and parse warnings' JSON output 32 | reek_warnings = Utils.capture_stdout { Reek::CLI::Application.new(['-f', 'json', file]).execute } 33 | reek_offenses = JSON.parse(reek_warnings).map { |warning| "#{warning['lines'].join(',')}: #{warning['context']} #{warning['message']}" } 34 | 35 | # assign warnings from combined offenses 36 | warnings = rubocop_offenses + reek_offenses 37 | 38 | # return warnings 39 | next PuppetCheck.files[:warnings][file] = warnings unless warnings.empty? 40 | end 41 | PuppetCheck.files[:clean].push(file.to_s) 42 | end 43 | end 44 | 45 | # checks ruby template (.erb) 46 | def self.template(files) 47 | require 'erb' 48 | 49 | files.each do |file| 50 | # check ruby template syntax 51 | begin 52 | # need to eventually have this associated with a different binding during each iteration 53 | # older usage throws extra warning and mixes with valid warnings confusingly 54 | warnings = Utils.capture_stderr { ERB.new(File.read(file), trim_mode: '-').result } 55 | # warnings = ERB.new(File.read(file), trim_mode: '-').result(RubyParser.new.bind) 56 | rescue NameError, TypeError 57 | # empty out warnings since it would contain an error if this pass triggers 58 | warnings = '' 59 | rescue ScriptError => err 60 | next PuppetCheck.files[:errors][file] = err.to_s.gsub('(erb):', '').split("\n") 61 | end 62 | # return warnings from the check if there were any 63 | next PuppetCheck.files[:warnings][file] = warnings.to_s.gsub('warning: ', '').delete("\n").split('(erb):').compact unless warnings == '' 64 | PuppetCheck.files[:clean].push(file.to_s) 65 | end 66 | end 67 | 68 | # checks librarian puppet (Puppetfile/Modulefile) and misc ruby (Rakefile/Gemfile) 69 | def self.librarian(files, style, rc_args) 70 | # efficient var assignment prior to iterator 71 | if style 72 | require 'json' 73 | require 'rubocop' 74 | 75 | rubocop_cli = RuboCop::CLI.new 76 | 77 | # RuboCop is grumpy about non-snake_case filenames so disable the FileName check 78 | rc_args.include?('--except') ? rc_args[rc_args.index('--except') + 1] = "#{rc_args[rc_args.index('--except') + 1]},Naming/FileName" : rc_args.push('--except', 'Naming/FileName') 79 | end 80 | 81 | files.each do |file| 82 | # check librarian puppet syntax 83 | # prevents ruby code from actually executing the input ruby file 84 | catch(:good) { instance_eval("BEGIN {throw :good}; #{File.read(file)} # BEGIN {throw :good}; ruby_file_content", file) } 85 | rescue SyntaxError, LoadError, ArgumentError => err 86 | PuppetCheck.files[:errors][file] = err.to_s.gsub("#{file}:", '').split("\n") 87 | # check librarian puppet style 88 | else 89 | if style 90 | warnings = Utils.capture_stdout { rubocop_cli.run(rc_args + ['--enable-pending-cops', '--require', 'rubocop-performance', '--format', 'json', file]) } 91 | offenses = JSON.parse(warnings)['files'][0]['offenses'].map { |warning| "#{warning['location']['line']}:#{warning['location']['column']} #{warning['message']}" } 92 | 93 | # collect style warnings 94 | next PuppetCheck.files[:warnings][file] = offenses unless offenses.empty? 95 | end 96 | PuppetCheck.files[:clean].push(file.to_s) 97 | end 98 | end 99 | 100 | # potentially for unique erb bindings 101 | def bind 102 | binding 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/puppet-check/tasks.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'rake/tasklib' 3 | rescue LoadError 4 | raise 'Rake is not installed and you are attempting to execute Rake tasks with Puppet Check. Please install Rake before continuing.' 5 | end 6 | require_relative '../puppet_check' 7 | 8 | # the rake interface for PuppetCheck 9 | class PuppetCheck::Tasks < Rake::TaskLib 10 | def initialize 11 | super 12 | 13 | desc 'Execute all Puppet-Check checks' 14 | task puppetcheck: %w[puppetcheck:file puppetcheck:spec puppetcheck:beaker puppetcheck:kitchen] 15 | 16 | namespace :puppetcheck do 17 | desc 'Execute Puppet-Check file checks' 18 | task :file, [:settings] do |_, wrapped_settings| 19 | PuppetCheck.new.run(wrapped_settings[:settings], Dir.glob('*')) 20 | end 21 | 22 | # rspec and rspec-puppet tasks 23 | begin 24 | require 'rspec/core/rake_task' 25 | require_relative 'rspec_puppet_support' 26 | 27 | desc 'Execute RSpec and RSpec-Puppet tests' 28 | RSpec::Core::RakeTask.new(:spec) do |task| 29 | RSpecPuppetSupport.run 30 | # generate tasks for all recognized directories and ensure spec tests inside module dependencies are ignored 31 | spec_dirs = Dir.glob('**/{classes,defines,facter,functions,hosts,puppet,unit,types}/**/*_spec.rb').grep_v(/fixtures/) 32 | task.pattern = spec_dirs.empty? ? 'skip_rspec' : spec_dirs 33 | end 34 | rescue LoadError 35 | desc 'RSpec is not installed.' 36 | task :spec do 37 | puts 'RSpec is not installed. The RSpec/RSpec-Puppet tasks will not be available.' 38 | end 39 | end 40 | 41 | # beaker tasks 42 | begin 43 | require 'beaker/tasks/quick_start' 44 | 45 | desc 'Execute Beaker acceptance tests' 46 | task :beaker, %i[hypervisor] do |_, args| 47 | args[:hypervisor] = 'vagrant' if args[:hypervisor].nil? 48 | Rake::Task["beaker_quickstart:run_test[#{args[:hypervisor]}]"].invoke 49 | end 50 | rescue LoadError 51 | desc 'Beaker is not installed.' 52 | task :beaker do 53 | puts 'Beaker is not installed. The Beaker tasks will not be available.' 54 | end 55 | end 56 | 57 | # test kitchen tasks 58 | begin 59 | require 'kitchen/rake_tasks' 60 | 61 | desc 'Execute Test Kitchen acceptance tests' 62 | task :kitchen do 63 | Rake::Task['kitchen:all'].invoke 64 | end 65 | rescue LoadError 66 | desc 'Test Kitchen is not installed.' 67 | task :kitchen do 68 | puts 'Test Kitchen is not installed. The Kitchen tasks will not be available.' 69 | end 70 | end 71 | end 72 | end 73 | end 74 | 75 | PuppetCheck::Tasks.new 76 | -------------------------------------------------------------------------------- /lib/puppet-check/utils.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /lib/puppet_check.rb: -------------------------------------------------------------------------------- 1 | require_relative 'puppet-check/puppet_parser' 2 | require_relative 'puppet-check/ruby_parser' 3 | require_relative 'puppet-check/data_parser' 4 | require_relative 'puppet-check/output_results' 5 | 6 | # interfaces from CLI/tasks and to individual parsers 7 | class PuppetCheck 8 | # initialize files hash 9 | @files = { 10 | errors: {}, 11 | warnings: {}, 12 | clean: [], 13 | ignored: [] 14 | } 15 | 16 | # allow the parser methods to write to the files 17 | class << self 18 | attr_accessor :files 19 | end 20 | 21 | # main runner for PuppetCheck 22 | def run(settings = {}, paths = []) 23 | # settings defaults 24 | settings = self.class.defaults(settings) 25 | 26 | # grab all of the files to be processed 27 | files = self.class.parse_paths(paths) 28 | 29 | # parse the files 30 | parsed_files = execute_parsers(files, settings[:style], settings[:puppetlint_args], settings[:rubocop_args], settings[:public], settings[:private]) 31 | 32 | # output the diagnostic results 33 | OutputResults.run(parsed_files.clone, settings[:output_format]) 34 | 35 | # progress to regression checks if no errors in file checks 36 | if parsed_files[:errors].empty? && (!settings[:fail_on_warning] || parsed_files[:warnings].empty?) 37 | begin 38 | require_relative 'puppet-check/regression_check' 39 | # if octocatalog-diff is not installed then return immediately 40 | rescue LoadError 41 | warn 'octocatalog-diff is not installed, and therefore the regressions check will be skipped' 42 | return 0 43 | end 44 | 45 | # perform smoke checks if there were no errors and the user desires 46 | begin 47 | catalog = RegressionCheck.smoke(settings[:octonodes], settings[:octoconfig]) if settings[:smoke] 48 | # smoke check failure? output message and return 2 49 | rescue OctocatalogDiff::Errors::CatalogError => err 50 | puts 'There was a smoke check error:' 51 | puts err 52 | puts catalog.error_message unless catalog.valid? 53 | 2 54 | end 55 | # perform regression checks if there were no errors and the user desires 56 | # begin 57 | # catalog = RegressionCheck.regression(settings[:octonodes], settings[:octoconfig]) if settings[:regression] 58 | # rescue OctocatalogDiff::Errors::CatalogError => err 59 | # puts 'There was a catalog compilation error during the regression check:' 60 | # puts err 61 | # puts catalog.error_message unless catalog.valid? 62 | # 2 63 | # end 64 | # code to output differences in catalog? 65 | # everything passed? return 0 66 | 0 67 | else 68 | # error files? return 2 69 | 2 70 | end 71 | end 72 | 73 | # establish default settings 74 | def self.defaults(settings = {}) 75 | private_class_method :method 76 | 77 | # return settings with defaults where unspecified 78 | { 79 | # initialize fail on warning, style check, and regression check bools 80 | fail_on_warning: false, 81 | style: false, 82 | smoke: false, 83 | regression: false, 84 | # initialize ssl keys for eyaml checks 85 | public: nil, 86 | private: nil, 87 | # initialize output format option 88 | output_format: 'text', 89 | # initialize octocatalog-diff options 90 | octoconfig: '.octocatalog-diff.cfg.rb', 91 | octonodes: %w[localhost.localdomain], 92 | # initialize style arg arrays 93 | puppetlint_args: [], 94 | rubocop_args: [] 95 | }.merge(settings) 96 | end 97 | 98 | # parse the paths and return the array of files 99 | def self.parse_paths(paths = []) 100 | private_class_method :method 101 | files = [] 102 | 103 | # traverse the unique paths and return all files not explicitly in fixtures 104 | paths.uniq.each do |path| 105 | if File.directory?(path) 106 | # glob all files in directory and concat them 107 | files.concat(Dir.glob("#{path}/**/*").select { |subpath| File.file?(subpath) && !subpath.include?('fixtures') }) 108 | elsif File.file?(path) && !path.include?('fixtures') 109 | files.push(path) 110 | else 111 | warn "puppet-check: #{path} is not a directory, file, or symlink, and will not be considered during parsing" 112 | end 113 | end 114 | 115 | # check that at least one file was found, and remove double slashes from returned array 116 | raise "puppet-check: no files found in supplied paths '#{paths.join(', ')}'." if files.empty? 117 | files.map { |file| file.gsub('//', '/') }.uniq 118 | end 119 | 120 | private 121 | 122 | # categorize and pass the files out to the parsers to determine their status 123 | def execute_parsers(files, style, puppetlint_args, rubocop_args, public, private) 124 | # check manifests 125 | manifests, files = files.partition { |file| File.extname(file) == '.pp' } 126 | PuppetParser.manifest(manifests, style, puppetlint_args) unless manifests.empty? 127 | # check puppet templates 128 | templates, files = files.partition { |file| File.extname(file) == '.epp' } 129 | PuppetParser.template(templates) unless templates.empty? 130 | # check ruby files 131 | rubies, files = files.partition { |file| File.extname(file) == '.rb' } 132 | RubyParser.ruby(rubies, style, rubocop_args) unless rubies.empty? 133 | # check ruby templates 134 | templates, files = files.partition { |file| File.extname(file) == '.erb' } 135 | RubyParser.template(templates) unless templates.empty? 136 | # check yaml data 137 | yamls, files = files.partition { |file| File.extname(file) =~ /\.ya?ml$/ } 138 | DataParser.yaml(yamls) unless yamls.empty? 139 | # check json data 140 | jsons, files = files.partition { |file| File.extname(file) == '.json' } 141 | DataParser.json(jsons) unless jsons.empty? 142 | # check eyaml data; block this for now 143 | eyamls, files = files.partition { |file| File.extname(file) =~ /\.eya?ml$/ } 144 | DataParser.eyaml(eyamls, public, private) unless eyamls.empty? 145 | # check misc ruby 146 | librarians, files = files.partition { |file| File.basename(file) =~ /(?:Puppet|Module|Rake|Gem|Vagrant)file|.gemspec$/ } 147 | RubyParser.librarian(librarians, style, rubocop_args) unless librarians.empty? 148 | # ignore everything else 149 | files.each { |file| self.class.files[:ignored].push(file.to_s) } 150 | # return PuppetCheck.files to mitigate singleton write accessor side effects 151 | PuppetCheck.files 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /puppet-check.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = 'puppet-check' 3 | spec.version = '2.3.1' 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 = Dir['bin/**/*', 'lib/**/*', 'spec/**/*', 'CHANGELOG.md', 'LICENSE.md', 'README.md', 'puppet-check.gemspec'] 11 | spec.executables = spec.files.grep(%r{^bin/}) { |file| File.basename(file) } 12 | spec.require_paths = Dir['lib'] 13 | 14 | spec.required_ruby_version = '>= 2.7.0' 15 | spec.add_dependency 'puppet', '>= 5.5', '< 9' 16 | spec.add_dependency 'puppet-lint', '~> 4.0' 17 | spec.add_dependency 'reek', '~> 6.0' 18 | spec.add_dependency 'rubocop', '~> 1.0' 19 | spec.add_dependency 'rubocop-performance', '~> 1.0' 20 | spec.add_development_dependency 'octocatalog-diff', '~> 1.0' 21 | spec.add_development_dependency 'rake', '~> 13.0' 22 | spec.add_development_dependency 'rspec', '~> 3.0' 23 | end 24 | -------------------------------------------------------------------------------- /spec/fixtures/foobarbaz: -------------------------------------------------------------------------------- 1 | foobarbaz 2 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/good.eyaml: -------------------------------------------------------------------------------- 1 | --- 2 | i: 3 | - am 4 | - a 5 | 6 | good: 7 | eyaml: ENC[PKCS7,MIIBeQYJKoZIhvcNAQcDoIIBajCCAWYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAE55MeA/kW1uoyJV8WJPU+2q3DklRBKcRBhJtofuDwkKO7SQ+T30A/vDAP+A2IRFXDC0xOCM1iwQvbVIUht9Godb6qeT+8wLHmMuekN7jZpjYnAZHECy8QcWxlLaGP6LSJrvq5VN//4djiwdne5064tmTpXxwVffL+z7QyKI24t7YO3zjR40L2tm4mduyRnK2KuLpfI9hGAWwKWFAQ8GywEtZaFmN+7M7BYvCnR6WclZSkokVraAJk1v31blTCs/2E2NrTFbjDvAlJIYLdU8Q1rcifW06r8Rx5I0Ufzbd+vcCGyKjCWJ1Rzjgs29UaF2hogJbEsFS/BUx8AEbffL4DjA8BgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBuhvUa47kIQ2/nHGwfju9ogBAusaL97ZJBWahoLgSEY+gT] 8 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/good.json: -------------------------------------------------------------------------------- 1 | { 2 | "i": "am", 3 | "a": { 4 | "good": "json" 5 | }, 6 | "file": "." 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/good.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | i: 3 | - am 4 | - a 5 | 6 | good: 7 | yaml: file 8 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/style.eyaml: -------------------------------------------------------------------------------- 1 | --- 2 | i: 'am' 3 | 4 | :a: 5 | good: 6 | - eyaml 7 | - ENC[PKCS7,MIIBeQYJKoZIhvcNAQcDoIIBajCCAWYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAE55MeA/kW1uoyJV8WJPU+2q3DklRBKcRBhJtofuDwkKO7SQ+T30A/vDAP+A2IRFXDC0xOCM1iwQvbVIUht9Godb6qeT+8wLHmMuekN7jZpjYnAZHECy8QcWxlLaGP6LSJrvq5VN//4djiwdne5064tmTpXxwVffL+z7QyKI24t7YO3zjR40L2tm4mduyRnK2KuLpfI9hGAWwKWFAQ8GywEtZaFmN+7M7BYvCnR6WclZSkokVraAJk1v31blTCs/2E2NrTFbjDvAlJIYLdU8Q1rcifW06r8Rx5I0Ufzbd+vcCGyKjCWJ1Rzjgs29UaF2hogJbEsFS/BUx8AEbffL4DjA8BgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBuhvUa47kIQ2/nHGwfju9ogBAusaL97ZJBWahoLgSEY+gT] 8 | 9 | but: 'i am missing a' 10 | 11 | value: 12 | 13 | and: 14 | also: 'a' 15 | subvalue: 16 | --- 17 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/style.yaml: -------------------------------------------------------------------------------- 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: 16 | --- 17 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/syntax.eyaml: -------------------------------------------------------------------------------- 1 | --- 2 | i: - am :an: 3 | - :eyaml: 4 | - file: 5 | 6 | with: 7 | : ENC[PKCS7,MIIBeQYJKoZIhvcNAQcDoIIBajCCAWYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAPW3B3OFO48lQZcrcFPj1oGl1Qx9It8JN6neAfxPIV5lWx5o//gBVpy0tcc0rq83A1UxYJ9wdamgqUoSCd9oYBXMjjbkd4OCe6z3XCDBGnAkYy0jvwg6/1SUgmMgo5MC1GEg/ADe9LfxxbRosV8MUAozoevvWoWQyZ6Sr4s0PDjhvIf8F4P2uUmIw0lsUyfgl8H1qTWy+RQFkjZ9ArURMuzNs0ru5HvzHtQt8o/PzW31RqEGm4k/4B0DNhWmd5jvjr+VCuCn8gn7ypj6HbfTRA/mMenm/p2dU9yJA7oROsV55kMTHD7Uav4CMTMLXmfF5oiO9mRw91KkmNjB7FGlxJDA8BgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBzcD656/BIbPWtkc1i0t2KgBAD+oVqa/H8lMynix7pN4eL] 8 | :syntax: - 9 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/syntax.json: -------------------------------------------------------------------------------- 1 | { 2 | "i": "am" 3 | "a": { 4 | "json": "file", 5 | } 6 | "with": "bad", 7 | "syntax": 8 | } 9 | -------------------------------------------------------------------------------- /spec/fixtures/hieradata/syntax.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | i: - am :a: 3 | - :yaml: 4 | - file: 5 | 6 | with: 7 | : bad 8 | :syntax: - 9 | -------------------------------------------------------------------------------- /spec/fixtures/keys/private_key.pkcs7.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEArcVImrrizOvxggoj9pQsJ78zweoxp0EbTSQXJVLQ+fBWxNDm 3 | LYGeRpxeBu2laHG/rVtuokiVdVjRfGIxhMW5n+WE0+tg7xQD8M9gtoIzZcCfPO68 4 | TLb/SF7pSL7kZJqlV62VsdyzAYSmQTYeAqIVg/1y9AH4Bm8UPOpHOpCX2BODa18S 5 | cBKTkCdXbRPIdsvJ1xNAag0yD4erEtz1/In9AKzL1urxFILoSU4w3IBQigMLT6r9 6 | eo5xd91PIXZwSMO3ypN9ZQCVkDBoyO6g5HaqPHeSP6ZMWzKBCGdi2T613mZgK3u8 7 | AaHtHcTdryDFLsYpgwcBFFsiwf2b2GT2TNPrrQIDAQABAoIBAC5xiBIy2ONEPGwx 8 | IaOJvrNw2vh9C4BUWmOnvbfwBoKGxeocmyiWeCMPL+JSKd9cZ4qB4XPmSmvurS69 9 | X9bIsfAWfAXX5zR++P6nroWZDvunG+hBNnqafmhtVZRn0MtCueZRMq0y+3Tway8K 10 | s8KoZ2+7kbm9vPibP6F4TElO1Aaf303gmnXFENzZasaHP6qxdFCqAdFw4/AXpXE4 11 | wSWprUPwJRqF0MlWK4IuANblIQ3q1SL6rq4TSylp/OvdYP0Ps85sVyNGIjVwXA5u 12 | SRdVyhog3kbzpHj7hpv7Kl2gHKV50pbp90aOpRNqiGqP9WzSIOTjb5G855QqYNk/ 13 | uPL6tlECgYEA5wZWeeB0aIichWCzNrKO0SEJMY7JtPnjtwWL/GYGHUiTDijNR3j5 14 | njS+58DwVW0adZKpkNvqZ9QJQnw4Fg0iwb16o2FfiZ3whV9XBJbgYyglue/U3WWq 15 | E20/sPZdkFBGKkhmgCcuGvVRGyzKZCQ3xFU8O51Ha3qK6NYr2jsYlQcCgYEAwI5s 16 | 2fGp4Vc/4RLWOA8rJ8ywKt4HuLNBxnCb8hHTg2LJG7RjEM2nCJlcUnLB2UctaDqy 17 | 8hMf1vGubMlWt9n9MxyWSSdjdzppQbo75HqAVXZBtC+FCJXGKzOnR9OkuTQ8RU9Y 18 | I7OMQ07d3mECXvvUo9z+j5sGBFCZCOSiMKqcoKsCgYAqIOa+HOc9dMQOMn8b113h 19 | 1wNlLSOtQ/B5nKJVaYlt56EhcOFWSkJCzl2Nx4pMGbQ8gn03dFL/khQKMrrLkqOm 20 | 7tWhW41ffBSzTZqBtL3Adz7B9HE92l80YbS+oX6YZXsWFNPURNDMi7W5neZecphU 21 | zjJIsLqoZ9VS9lNS3XnzGwKBgHnutoccPNLxCQZhgz1kfZNqTnQWvRT8jj42uMmE 22 | +EdOmsLsa38MeyU61/dtq8sHA33pDb+01Iir6iQBKuSpKWjgRVp1ksrBNj3kzGBW 23 | IkX76IdNQBS7ow1gXCFjp/+PUEsjf1C8Nam5m01iaLn1BiXtn1UYearpzr6O6RWg 24 | NCaZAoGAIPGk+dcgKFf5lVWj0XkZfzVbj/OgwVBsUxP/jQ/monB7iHjwoYPK4A4y 25 | sBy4FHFBfQKKMrn52uQFcrXq6/EK+pASswkrxsIUrASsPaTNOq+SngT+NWqHC5Fj 26 | 7vaA1e59QlSQWSAf6IZbsr2qu/NKPN6JkA1SLbtF8gF3T7QphKo= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /spec/fixtures/keys/public_key.pkcs7.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC2TCCAcGgAwIBAgIBATANBgkqhkiG9w0BAQUFADAAMCAXDTE3MDgyOTIzMzYz 3 | OFoYDzIwNjcwODE3MjMzNjM4WjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 4 | CgKCAQEArcVImrrizOvxggoj9pQsJ78zweoxp0EbTSQXJVLQ+fBWxNDmLYGeRpxe 5 | Bu2laHG/rVtuokiVdVjRfGIxhMW5n+WE0+tg7xQD8M9gtoIzZcCfPO68TLb/SF7p 6 | SL7kZJqlV62VsdyzAYSmQTYeAqIVg/1y9AH4Bm8UPOpHOpCX2BODa18ScBKTkCdX 7 | bRPIdsvJ1xNAag0yD4erEtz1/In9AKzL1urxFILoSU4w3IBQigMLT6r9eo5xd91P 8 | IXZwSMO3ypN9ZQCVkDBoyO6g5HaqPHeSP6ZMWzKBCGdi2T613mZgK3u8AaHtHcTd 9 | ryDFLsYpgwcBFFsiwf2b2GT2TNPrrQIDAQABo1wwWjAPBgNVHRMBAf8EBTADAQH/ 10 | MB0GA1UdDgQWBBSNRJcp7zds5OoHwILPQ54USW6UdjAoBgNVHSMEITAfgBSNRJcp 11 | 7zds5OoHwILPQ54USW6UdqEEpAIwAIIBATANBgkqhkiG9w0BAQUFAAOCAQEAK0Hu 12 | Q9oB1+w4OnilleLyARiCz64RDdbAPCmQtg18L7LG4Y19qmlThF7o+3sPzy4XHsGN 13 | R96II4LWwzvYnoWbb9YDspdoR3S9NeR5nEQ0cHhCvCjJabciJb0Ua5s7B6n/OZI2 14 | yLiksSDVjqLz3MxRSDGm4BU/6THJtktmxHqFkZpIyi+Ph/zlifB3tkB+EUqZ4x3T 15 | zzKi+zVDI2fZHZbpwrXbCShagvTc466hEji3JZAXjr3G2KJBsCvbMVuYtAWX4XQE 16 | +/F8BoNpZkIOd1xgRcDR07bLjM7AfTjxec88fUebd2FYYX498AqC6VuzCeFk56nS 17 | 7p9qXtWyXoV/G9Xdeg== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /spec/fixtures/lib/good.rb: -------------------------------------------------------------------------------- 1 | puts 'i am a good ruby file' 2 | -------------------------------------------------------------------------------- /spec/fixtures/lib/rubocop_style.rb: -------------------------------------------------------------------------------- 1 | hash = { :i => 'am' } 2 | $a = 'ruby' 3 | puts "file with bad style" 4 | -------------------------------------------------------------------------------- /spec/fixtures/lib/style.rb: -------------------------------------------------------------------------------- 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 13 | -------------------------------------------------------------------------------- /spec/fixtures/lib/syntax.rb: -------------------------------------------------------------------------------- 1 | i => am : a '' ruby.file { with } &bad syntax 2 | -------------------------------------------------------------------------------- /spec/fixtures/librarian_good/Puppetfile: -------------------------------------------------------------------------------- 1 | mod 'i', 2 | am: 'a', 3 | good: 'librarian' 4 | 5 | mod 'puppet', 'file' 6 | -------------------------------------------------------------------------------- /spec/fixtures/librarian_style/Puppetfile: -------------------------------------------------------------------------------- 1 | mod 'i', 2 | am: 'a', 3 | librarian: 'puppet' 4 | 5 | mod 'file', :with => 'bad style' 6 | -------------------------------------------------------------------------------- /spec/fixtures/librarian_syntax/Puppetfile: -------------------------------------------------------------------------------- 1 | mod 'i', 2 | am: 'a' 3 | librarian: 'puppet' 4 | 5 | mod 'file', with: 6 | 7 | mod 'bad', :'syntax' 8 | -------------------------------------------------------------------------------- /spec/fixtures/manifests/.puppet-lint.rc: -------------------------------------------------------------------------------- 1 | --puppetlint-arg-one 2 | --puppetlint-arg-two 3 | -------------------------------------------------------------------------------- /spec/fixtures/manifests/eof_syntax.pp: -------------------------------------------------------------------------------- 1 | # syntax error at 'end of file' instead of line or line and col 2 | class foo { 3 | file { 'bar': ensure => file 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixtures/manifests/good.pp: -------------------------------------------------------------------------------- 1 | notify { 'i am a good manifest': } 2 | -------------------------------------------------------------------------------- /spec/fixtures/manifests/style_lint.pp: -------------------------------------------------------------------------------- 1 | notify { 'i am': 2 | a => "manifest", 3 | with => 'bad lint style', 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixtures/manifests/style_parser.pp: -------------------------------------------------------------------------------- 1 | #test for warnings 2 | notify { 'i am a manifest with': message => "bad parser \[\] and lint style" } 3 | -------------------------------------------------------------------------------- /spec/fixtures/manifests/syntax.pp: -------------------------------------------------------------------------------- 1 | $"iam${amanifest}$withbadsyntax" 2 | -------------------------------------------------------------------------------- /spec/fixtures/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppetlabs-awesomesauce", 3 | "version": "1.1.1", 4 | "author": "puppetlabs", 5 | "summary": "Makes sauce that is awesome.", 6 | "license": "Proprietary", 7 | "source": "https://github.com/puppetlabs/puppetlabs-awesomesauce", 8 | "project_page": "https://github.com/puppetlabs/puppetlabs-awesomesauce", 9 | "issues_url": "https://tickets.puppetlabs.com/browse/MODULES", 10 | "operatingsystem_support": [ 11 | { 12 | "operatingsystem": "RedHat", 13 | "operatingsystemrelease": [ 14 | "100", 15 | "5", 16 | "6", 17 | "7" 18 | ] 19 | } 20 | ], 21 | "requirements": [ 22 | { 23 | "name": "pe", 24 | "version_requirement": ">= 3.0.0 < 2035.1.0" 25 | }, 26 | { 27 | "name": "puppet", 28 | "version_requirement": ">=2.7.20 <5.0.0" 29 | } 30 | ], 31 | "description": "Makes sauce that is awesome. What part of that is confusing?", 32 | "dependencies": [ 33 | { 34 | "name": "puppetlabs/stdlib", 35 | "version_requirement": ">= 4.1.0 < 5.0.0", 36 | "forge": "puppetlabs-stdlib" 37 | }, 38 | { 39 | "name": "puppetlabs/lvm", 40 | "version_requirement": ">= 0.5.0 < 1.0.0", 41 | "git": "https://github.com/puppetlabs/puppetlabs-lvm.git" 42 | }, 43 | { 44 | "name": "puppetlabs/foo", 45 | "version_requirement": ">= 0.0.0 < 1.0.0", 46 | "svn": "https://www.doesnotexist.com/foo" 47 | }, 48 | { 49 | "name": "puppetlabs/gruntmaster", 50 | "version_requirement": ">= 6000.0.0 < 9000.0.0", 51 | "software": "upgradeable" 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /spec/fixtures/metadata_good/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppetlabs-stdlib", 3 | "version": "4.11.0", 4 | "author": "puppetlabs", 5 | "summary": "Standard library of resources for Puppet modules.", 6 | "license": "Apache-2.0", 7 | "source": "https://github.com/puppetlabs/puppetlabs-stdlib", 8 | "project_page": "https://github.com/puppetlabs/puppetlabs-stdlib", 9 | "issues_url": "https://tickets.puppetlabs.com/browse/MODULES", 10 | "operatingsystem_support": [ 11 | { 12 | "operatingsystem": "RedHat", 13 | "operatingsystemrelease": [ 14 | "4", 15 | "5", 16 | "6", 17 | "7" 18 | ] 19 | } 20 | ], 21 | "requirements": [ 22 | { 23 | "name": "pe", 24 | "version_requirement": ">= 3.0.0 < 2015.4.0" 25 | }, 26 | { 27 | "name": "puppet", 28 | "version_requirement": ">=2.7.20 <5.0.0" 29 | } 30 | ], 31 | "description": "Standard Library for Puppet Modules", 32 | "dependencies": [] 33 | } 34 | -------------------------------------------------------------------------------- /spec/fixtures/metadata_style/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppetlabs-stdlib", 3 | "version": "4.11.0", 4 | "author": "puppetlabs", 5 | "summary": "Standard library of resources for Puppet modules.", 6 | "license": "Imaginary", 7 | "source": "https://github.com/puppetlabs/puppetlabs-stdlib", 8 | "project_page": "https://github.com/puppetlabs/puppetlabs-stdlib", 9 | "issues_url": "https://tickets.puppetlabs.com/browse/MODULES", 10 | "requirements": [ 11 | { 12 | "name": "pe", 13 | "version_requirement": ">= 3.0.0" 14 | }, 15 | { 16 | "name": "puppet", 17 | "version_requirement": ">=2.7.20 <5.0.0" 18 | } 19 | ], 20 | "description": "Standard Library for Puppet Modules", 21 | "dependencies": [] 22 | } 23 | -------------------------------------------------------------------------------- /spec/fixtures/metadata_style_two/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppetlabs-stdlib", 3 | "version": "4.11.0", 4 | "author": "puppetlabs", 5 | "summary": "Standard library of resources for Puppet modules.", 6 | "license": "Apache-2.0", 7 | "source": "https://github.com/puppetlabs/puppetlabs-stdlib", 8 | "project_page": "https://github.com/puppetlabs/puppetlabs-stdlib", 9 | "issues_url": "https://tickets.puppetlabs.com/browse/MODULES", 10 | "operatingsystem_support": [], 11 | "requirements": [ 12 | { 13 | "name": "pe", 14 | "version_requirement": ">= 3.0.0 < 2016.4" 15 | }, 16 | { 17 | "name": "puppet", 18 | "version_requirement": ">=2.7.20 <5.0.0" 19 | } 20 | ], 21 | "description": "Standard Library for Puppet Modules", 22 | "dependencies": [ 23 | { "name": "puppetlabs/one", "version_requirement": ">= 3.2.0 <= 5.0" }, 24 | { "name": "puppetlabs/two", "version_requirement": ">= 0.0.4" } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /spec/fixtures/metadata_syntax/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppetlabs-stdlib", 3 | "author": "puppetlabs", 4 | "summary": "This is the song that never ends. Yes it goes on and on my friends. Some people, started singing it not knowing what it was. And they will keep singing it forever just because.", 5 | "license": "Apache-2.0", 6 | "source": "https://github.com/puppetlabs/puppetlabs-stdlib", 7 | "project_page": "https://github.com/puppetlabs/puppetlabs-stdlib", 8 | "issues_url": "https://tickets.puppetlabs.com/browse/MODULES", 9 | "description": "Standard Library for Puppet Modules", 10 | "dependencies": [ 11 | {"name": "puppetlabs/nothing", "version_requirement": ">=1.1.1 < 2.2.2"}, 12 | {"name": "puppetlabs/nothing", "version_requirement": ">=1.1.1 < 2.2.2"} 13 | ], 14 | "requirements": ["not an array of hashes"], 15 | "checksum": "foo" 16 | } 17 | -------------------------------------------------------------------------------- /spec/fixtures/plans/good.pp: -------------------------------------------------------------------------------- 1 | plan my_plan::good_plan( 2 | TargetSpec $servers 3 | ) {} 4 | -------------------------------------------------------------------------------- /spec/fixtures/plans/style.pp: -------------------------------------------------------------------------------- 1 | plan my_plan::style_plan( 2 | TargetSpec $servers 3 | ) { 4 | $foo = 'bar' 5 | $bar = "$bar baz" 6 | } 7 | -------------------------------------------------------------------------------- /spec/fixtures/plans/syntax.pp: -------------------------------------------------------------------------------- 1 | plan my_plan::syntax_plan( 2 | TargetSpec $servers 3 | ) { 4 | get_targets($servers).each |Target $server| {) 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/spec/acceptance/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschuchard/puppet-check/683ba2b2609d1b26576544835657b6303551318b/spec/fixtures/spec/acceptance/.empty -------------------------------------------------------------------------------- /spec/fixtures/spec/facter/facter_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../spec_helper' 2 | 3 | describe 'I am a standard RSpec test' do 4 | it 'that does a dumb check to prove this test is executing during the puppetcheck:spec task' do 5 | expect(true).to eq(true) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/spec/fixtures/do_not_parse_me: -------------------------------------------------------------------------------- 1 | i am a file that should not be processed 2 | -------------------------------------------------------------------------------- /spec/fixtures/task_metadata/task_bad.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": 1, 3 | "supports_noop": "false", 4 | "input_method": "unsupported", 5 | "puppet_task_version": "1", 6 | "parameters": [ 7 | "first", 8 | "second" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /spec/fixtures/task_metadata/task_good.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Does the things.", 3 | "supports_noop": false, 4 | "input_method": "stdin", 5 | "puppet_task_version": 1, 6 | "parameters": { 7 | "first": { 8 | "description": "First param.", 9 | "type": "Optional[String[1]]" 10 | }, 11 | "second": { 12 | "description": "Second param.", 13 | "type": "Optional[String[1]]" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /spec/fixtures/templates/good.epp: -------------------------------------------------------------------------------- 1 | <%# i am %> 2 | <% $a = 'good' -%> 3 | puppet template 4 | -------------------------------------------------------------------------------- /spec/fixtures/templates/good.erb: -------------------------------------------------------------------------------- 1 | i am a good <%= @ruby -%> template 2 | -------------------------------------------------------------------------------- /spec/fixtures/templates/no_method_error.erb: -------------------------------------------------------------------------------- 1 | i am a 2 | <% @ruby.upcase @template.downcase %> 3 | with nomethoderror 4 | -------------------------------------------------------------------------------- /spec/fixtures/templates/style.erb: -------------------------------------------------------------------------------- 1 | i am a ruby 2 | <% TEMPLATE = 'with' %> 3 | <% TEMPLATE = 'bad style' %> 4 | -------------------------------------------------------------------------------- /spec/fixtures/templates/syntax.epp: -------------------------------------------------------------------------------- 1 | i am a 2 | <% puppet template %> 3 | < $with = 'bad syntax %> 4 | -------------------------------------------------------------------------------- /spec/fixtures/templates/syntax.erb: -------------------------------------------------------------------------------- 1 | i am <%= @a ruby %> template <%= @with -> bad syntax 2 | -------------------------------------------------------------------------------- /spec/octocatalog-diff/facts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: good.example.com 3 | values: 4 | domain: example.com 5 | -------------------------------------------------------------------------------- /spec/octocatalog-diff/hiera.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | :backends: yaml 3 | :yaml: 4 | :datadir: "hieradata" 5 | :hierarchy: 6 | - "common" 7 | -------------------------------------------------------------------------------- /spec/octocatalog-diff/manifests/site.pp: -------------------------------------------------------------------------------- 1 | node good.example.com { 2 | notify { 'hello world': } 3 | } 4 | -------------------------------------------------------------------------------- /spec/octocatalog-diff/octocatalog_diff.cfg.rb: -------------------------------------------------------------------------------- 1 | # This is a configuration file for octocatalog-diff (https://github.com/github/octocatalog-diff). 2 | module OctocatalogDiff 3 | # Configuration class. 4 | class Config 5 | def self.config 6 | settings = {} 7 | octocatalog_diff_dir = "#{File.dirname(__FILE__)}/" 8 | 9 | settings[:hiera_config] = "#{octocatalog_diff_dir}hiera.yaml" 10 | settings[:hiera_path] = "#{octocatalog_diff_dir}hieradata" 11 | settings[:fact_file] = "#{octocatalog_diff_dir}facts.yaml" 12 | settings[:puppet_binary] = '/usr/local/bin/puppet' 13 | settings[:bootstrapped_to_dir] = octocatalog_diff_dir 14 | 15 | settings 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/puppet-check/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | require_relative '../../lib/puppet-check/cli' 3 | 4 | describe PuppetCheck::CLI do 5 | context '.run' do 6 | it 'targets the current working directory if no paths were specified' do 7 | expect { PuppetCheck::CLI.run(%w[--fail-on-warnings]) }.not_to raise_exception 8 | expect(PuppetCheck.files[:clean].length).to eql(30) 9 | expect(PuppetCheck.files[:ignored].length).to eql(7) 10 | end 11 | end 12 | 13 | context '.parse' do 14 | it 'raises an error if an invalid option was specified' do 15 | expect { PuppetCheck::CLI.parse(%w[-s -asdf foo]) }.to raise_error(OptionParser::InvalidOption) 16 | end 17 | 18 | it 'allows fail on warnings, style, smoke, and regression checks to be enabled' do 19 | expect(PuppetCheck::CLI.parse(%w[--fail-on-warnings -s -r --smoke foo])).to include(fail_on_warnings: true, style: true, smoke: true, regression: true) 20 | end 21 | 22 | it 'correctly parser EYAML options' do 23 | expect(PuppetCheck::CLI.parse(%w[--public pub.pem --private priv.pem])).to include(public: 'pub.pem', private: 'priv.pem') 24 | end 25 | 26 | it 'correctly parses a formatting option' do 27 | expect(PuppetCheck::CLI.parse(%w[-o text])).to include(output_format: 'text') 28 | end 29 | 30 | it 'correctly parses octocatalog-diff options' do 31 | expect(PuppetCheck::CLI.parse(%w[--octoconfig config.cfg.rb --octonodes server1,server2])).to include(octoconfig: 'config.cfg.rb', octonodes: %w[server1 server2]) 32 | end 33 | 34 | it 'correctly parses PuppetLint arguments' do 35 | expect(PuppetCheck::CLI.parse(%w[--puppet-lint puppetlint-arg-one,puppetlint-arg-two foo])).to include(puppetlint_args: ['--puppetlint-arg-one', '--puppetlint-arg-two']) 36 | end 37 | 38 | it 'correctly loads a .puppet-lint.rc' do 39 | expect(PuppetCheck::CLI.parse(%W[-c #{fixtures_dir}/manifests/.puppet-lint.rc])).to include(puppetlint_args: ['--puppetlint-arg-one', '--puppetlint-arg-two']) 40 | end 41 | 42 | it 'correctly parses Rubocop arguments' do 43 | expect(PuppetCheck::CLI.parse(%w[--rubocop rubocop-arg-one,rubocop-arg-two foo])).to include(rubocop_args: ['--except', 'rubocop-arg-one,rubocop-arg-two']) 44 | end 45 | 46 | it 'correctly parses multiple sets of arguments' do 47 | expect(PuppetCheck::CLI.parse(%w[-s --puppet-lint puppetlint-arg-one,puppetlint-arg-two --rubocop rubocop-arg-one,rubocop-arg-two foo])).to include(style: true, puppetlint_args: ['--puppetlint-arg-one', '--puppetlint-arg-two'], rubocop_args: ['--except', 'rubocop-arg-one,rubocop-arg-two']) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/puppet-check/data_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | require_relative '../../lib/puppet-check/data_parser' 3 | 4 | describe DataParser do 5 | before(:each) do 6 | PuppetCheck.files = { 7 | errors: {}, 8 | warnings: {}, 9 | clean: [], 10 | ignored: [] 11 | } 12 | end 13 | 14 | context '.yaml' do 15 | it 'puts a bad syntax yaml file in the error files hash' do 16 | DataParser.yaml(["#{fixtures_dir}hieradata/syntax.yaml"]) 17 | expect(PuppetCheck.files[:errors].keys).to eql(["#{fixtures_dir}hieradata/syntax.yaml"]) 18 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}hieradata/syntax.yaml"].join("\n")).to match(%r{^block sequence entries are not allowed}) 19 | expect(PuppetCheck.files[:warnings]).to eql({}) 20 | expect(PuppetCheck.files[:clean]).to eql([]) 21 | end 22 | it 'puts a good yaml file with potential hiera issues in the warning files array' do 23 | DataParser.yaml(["#{fixtures_dir}hieradata/style.yaml"]) 24 | expect(PuppetCheck.files[:errors]).to eql({}) 25 | expect(PuppetCheck.files[:warnings].keys).to eql(["#{fixtures_dir}hieradata/style.yaml"]) 26 | expect(PuppetCheck.files[:warnings]["#{fixtures_dir}hieradata/style.yaml"].join("\n")).to match(%r{^Value\(s\) missing in key.*\nValue\(s\) missing in key.*\nThe string --- appears more than once in this data and Hiera may fail to parse it correctly}) 27 | expect(PuppetCheck.files[:clean]).to eql([]) 28 | end 29 | it 'puts a good yaml file in the clean files array' do 30 | DataParser.yaml(["#{fixtures_dir}hieradata/good.yaml"]) 31 | expect(PuppetCheck.files[:errors]).to eql({}) 32 | expect(PuppetCheck.files[:warnings]).to eql({}) 33 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}hieradata/good.yaml"]) 34 | end 35 | end 36 | 37 | context '.eyaml' do 38 | it 'returns a warning if a public key was not specified' do 39 | expect { DataParser.eyaml(['foo.eyaml'], nil, 'private.pem') }.to output("Public X509 and/or Private RSA PKCS7 certs were not specified. EYAML checks will not be executed.\n").to_stderr 40 | end 41 | it 'returns a warning if a private key was not specified' do 42 | expect { DataParser.eyaml(['foo.eyaml'], 'public.pem', nil) }.to output("Public X509 and/or Private RSA PKCS7 certs were not specified. EYAML checks will not be executed.\n").to_stderr 43 | end 44 | it 'returns a warning if the public key or private key are not existing files' do 45 | expect { DataParser.eyaml(['foo.eyaml'], 'public.pem', 'private.pem') }.to output("Specified Public X509 and/or Private RSA PKCS7 certs do not exist. EYAML checks will not be executed.\n").to_stderr 46 | end 47 | it 'puts a bad syntax eyaml file in the error files hash' do 48 | DataParser.eyaml(["#{fixtures_dir}hieradata/syntax.eyaml"], "#{fixtures_dir}keys/public_key.pkcs7.pem", "#{fixtures_dir}keys/private_key.pkcs7.pem") 49 | expect(PuppetCheck.files[:errors].keys).to eql(["#{fixtures_dir}hieradata/syntax.eyaml"]) 50 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}hieradata/syntax.eyaml"].join("\n")).to match(%r{^block sequence entries are not allowed}) 51 | expect(PuppetCheck.files[:warnings]).to eql({}) 52 | expect(PuppetCheck.files[:clean]).to eql([]) 53 | end 54 | it 'puts a good eyaml file with potential hiera issues in the warning files array' do 55 | DataParser.eyaml(["#{fixtures_dir}hieradata/style.eyaml"], "#{fixtures_dir}keys/public_key.pkcs7.pem", "#{fixtures_dir}keys/private_key.pkcs7.pem") 56 | expect(PuppetCheck.files[:errors]).to eql({}) 57 | expect(PuppetCheck.files[:warnings].keys).to eql(["#{fixtures_dir}hieradata/style.eyaml"]) 58 | expect(PuppetCheck.files[:warnings]["#{fixtures_dir}hieradata/style.eyaml"].join("\n")).to match(%r{^Value\(s\) missing in key.*\nValue\(s\) missing in key.*\nThe string --- appears more than once in this data and Hiera may fail to parse it correctly}) 59 | expect(PuppetCheck.files[:clean]).to eql([]) 60 | end 61 | it 'puts a good eyaml file in the clean files array' do 62 | DataParser.eyaml(["#{fixtures_dir}hieradata/good.eyaml"], "#{fixtures_dir}keys/public_key.pkcs7.pem", "#{fixtures_dir}keys/private_key.pkcs7.pem") 63 | expect(PuppetCheck.files[:errors]).to eql({}) 64 | expect(PuppetCheck.files[:warnings]).to eql({}) 65 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}hieradata/good.eyaml"]) 66 | end 67 | end 68 | 69 | context '.json' do 70 | it 'puts a bad syntax json file in the error files hash' do 71 | DataParser.json(["#{fixtures_dir}hieradata/syntax.json"]) 72 | expect(PuppetCheck.files[:errors].keys).to eql(["#{fixtures_dir}hieradata/syntax.json"]) 73 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}hieradata/syntax.json"].join("\n")).to match(%r{^.*unexpected token}) 74 | expect(PuppetCheck.files[:warnings]).to eql({}) 75 | expect(PuppetCheck.files[:clean]).to eql([]) 76 | end 77 | it 'puts a bad metadata json file in the error files hash' do 78 | DataParser.json(["#{fixtures_dir}metadata_syntax/metadata.json"]) 79 | expect(PuppetCheck.files[:errors].keys).to eql(["#{fixtures_dir}metadata_syntax/metadata.json"]) 80 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}metadata_syntax/metadata.json"].join("\n")).to match(%r{^Required field.*\nField 'requirements'.*\nDuplicate dependencies.*\nDeprecated field.*\nSummary exceeds}) 81 | expect(PuppetCheck.files[:warnings]).to eql({}) 82 | expect(PuppetCheck.files[:clean]).to eql([]) 83 | end 84 | it 'puts a bad style metadata json file in the warning files array' do 85 | DataParser.json(["#{fixtures_dir}metadata_style/metadata.json"]) 86 | expect(PuppetCheck.files[:errors]).to eql({}) 87 | expect(PuppetCheck.files[:warnings].keys).to eql(["#{fixtures_dir}metadata_style/metadata.json"]) 88 | expect(PuppetCheck.files[:warnings]["#{fixtures_dir}metadata_style/metadata.json"].join("\n")).to match(%r{^'pe' is missing an upper bound.\n.*operatingsystem_support.*\nLicense identifier}) 89 | expect(PuppetCheck.files[:clean]).to eql([]) 90 | end 91 | it 'puts another bad style metadata json file in the warning files array' do 92 | DataParser.json(["#{fixtures_dir}metadata_style_two/metadata.json"]) 93 | expect(PuppetCheck.files[:errors]).to eql({}) 94 | expect(PuppetCheck.files[:warnings].keys).to eql(["#{fixtures_dir}metadata_style_two/metadata.json"]) 95 | expect(PuppetCheck.files[:warnings]["#{fixtures_dir}metadata_style_two/metadata.json"].join("\n")).to match(%r{^'puppetlabs/one' has non-semantic versioning.*\n'puppetlabs/two' is missing an upper bound\.\n.*operatingsystem.*\n.*operatingsystemrelease}) 96 | expect(PuppetCheck.files[:clean]).to eql([]) 97 | end 98 | it 'puts a bad task metadata json file in the warning files array' do 99 | DataParser.json(["#{fixtures_dir}task_metadata/task_bad.json"]) 100 | expect(PuppetCheck.files[:errors]).to eql({}) 101 | expect(PuppetCheck.files[:warnings].keys).to eql(["#{fixtures_dir}task_metadata/task_bad.json"]) 102 | expect(PuppetCheck.files[:warnings]["#{fixtures_dir}task_metadata/task_bad.json"].join("\n")).to match(%r{^description value is not a String\ninput_method value is not one of environment, stdin, or powershell\nparameters value is not a Hash\npuppet_task_version value is not an Integer\nsupports_noop value is not a Boolean}) 103 | expect(PuppetCheck.files[:clean]).to eql([]) 104 | end 105 | it 'puts a good json file in the clean files array' do 106 | DataParser.json(["#{fixtures_dir}hieradata/good.json"]) 107 | expect(PuppetCheck.files[:errors]).to eql({}) 108 | expect(PuppetCheck.files[:warnings]).to eql({}) 109 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}hieradata/good.json"]) 110 | end 111 | it 'puts a good metadata json file in the clean files array' do 112 | DataParser.json(["#{fixtures_dir}metadata_good/metadata.json"]) 113 | expect(PuppetCheck.files[:errors]).to eql({}) 114 | expect(PuppetCheck.files[:warnings]).to eql({}) 115 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}metadata_good/metadata.json"]) 116 | end 117 | it 'puts a good task metadata json file in the clean files array' do 118 | DataParser.json(["#{fixtures_dir}task_metadata/task_good.json"]) 119 | expect(PuppetCheck.files[:errors]).to eql({}) 120 | expect(PuppetCheck.files[:warnings]).to eql({}) 121 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}task_metadata/task_good.json"]) 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /spec/puppet-check/output_results_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | require_relative '../../lib/puppet-check/output_results' 3 | 4 | describe OutputResults do 5 | context '.text' do 6 | it 'outputs files with errors' do 7 | files = { errors: { 'foo' => ['i had an error'] } } 8 | expect { OutputResults.text(files) }.to output("\033[31mThe following files have errors:\033[0m\n-- foo:\ni had an error\n\n").to_stdout 9 | end 10 | it 'outputs files with warnings' do 11 | files = { warnings: { 'foo' => ['i had a warning'] } } 12 | expect { OutputResults.text(files) }.to output("\033[33mThe following files have warnings:\033[0m\n-- foo:\ni had a warning\n\n").to_stdout 13 | end 14 | it 'outputs files with no errors or warnings' do 15 | files = { clean: ['foo'] } 16 | expect { OutputResults.text(files) }.to output("\033[32mThe following files have no errors or warnings:\033[0m\n-- foo\n\n").to_stdout 17 | end 18 | it 'outputs files that were not processed' do 19 | files = { ignored: ['foo'] } 20 | expect { OutputResults.text(files) }.to output("\033[36mThe following files have unrecognized formats and therefore were not processed:\033[0m\n-- foo\n\n").to_stdout 21 | end 22 | end 23 | 24 | context '.run' do 25 | it 'redirects to text output formatting as expected' do 26 | expect { OutputResults.run({}, 'text') }.to output('').to_stdout 27 | end 28 | it 'outputs files with errors as yaml' do 29 | files = { errors: { 'foo' => ['i had an error'] } } 30 | expect { OutputResults.run(files, 'yaml') }.to output("---\nerrors:\n foo:\n - i had an error\n").to_stdout 31 | end 32 | it 'outputs files with warnings as yaml' do 33 | files = { warnings: { 'foo' => ['i had a warning'] } } 34 | expect { OutputResults.run(files, 'yaml') }.to output("---\nwarnings:\n foo:\n - i had a warning\n").to_stdout 35 | end 36 | it 'outputs files with no errors or warnings as yaml' do 37 | files = { clean: ['foo'] } 38 | expect { OutputResults.run(files, 'yaml') }.to output("---\nclean:\n- foo\n").to_stdout 39 | end 40 | it 'outputs files that were not processed as yaml' do 41 | files = { ignored: ['foo'] } 42 | expect { OutputResults.run(files, 'yaml') }.to output("---\nignored:\n- foo\n").to_stdout 43 | end 44 | it 'outputs files with errors as json' do 45 | files = { errors: { 'foo' => ['i had an error'] } } 46 | expect { OutputResults.run(files, 'json') }.to output("{\n \"errors\": {\n \"foo\": [\n \"i had an error\"\n ]\n }\n}\n").to_stdout 47 | end 48 | it 'outputs files with warnings as json' do 49 | files = { warnings: { 'foo' => ['i had a warning'] } } 50 | expect { OutputResults.run(files, 'json') }.to output("{\n \"warnings\": {\n \"foo\": [\n \"i had a warning\"\n ]\n }\n}\n").to_stdout 51 | end 52 | it 'outputs files with no errors or warnings as json' do 53 | files = { clean: ['foo'] } 54 | expect { OutputResults.run(files, 'json') }.to output("{\n \"clean\": [\n \"foo\"\n ]\n}\n").to_stdout 55 | end 56 | it 'outputs files that were not processed as json' do 57 | files = { ignored: ['foo'] } 58 | expect { OutputResults.run(files, 'json') }.to output("{\n \"ignored\": [\n \"foo\"\n ]\n}\n").to_stdout 59 | end 60 | it 'raises an error for an unsupported output format' do 61 | expect { OutputResults.run({}, 'awesomesauce') }.to raise_error(RuntimeError, 'puppet-check: Unsupported output format \'awesomesauce\' was specified.') 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/puppet-check/puppet_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | require_relative '../../lib/puppet-check/puppet_parser' 3 | 4 | describe PuppetParser do 5 | before(:each) do 6 | PuppetCheck.files = { 7 | errors: {}, 8 | warnings: {}, 9 | clean: [], 10 | ignored: [] 11 | } 12 | end 13 | 14 | context '.manifest' do 15 | it 'puts a bad syntax Puppet manifest in the error files hash' do 16 | PuppetParser.manifest(["#{fixtures_dir}manifests/syntax.pp"], false, []) 17 | expect(PuppetCheck.files[:errors].keys).to eql(["#{fixtures_dir}manifests/syntax.pp"]) 18 | if Gem::Version.new(Puppet::PUPPETVERSION) >= Gem::Version.new('6.5.0') 19 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}manifests/syntax.pp"].join("\n")).to match(%r{^Language validation logged 2 errors}) 20 | else 21 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}hieradata/syntax.yaml"].join("\n")).to match(%r{^This Variable has no effect.*\nIllegal variable name}) 22 | end 23 | expect(PuppetCheck.files[:warnings]).to eql({}) 24 | expect(PuppetCheck.files[:clean]).to eql([]) 25 | end 26 | # puppet 5 api has output issues for this fixture 27 | unless Gem::Version.new(Puppet::PUPPETVERSION) < Gem::Version.new('6.0.0') 28 | it 'puts a bad syntax at eof Puppet manifest in the error files hash' do 29 | PuppetParser.manifest(["#{fixtures_dir}manifests/eof_syntax.pp"], false, []) 30 | expect(PuppetCheck.files[:errors].keys).to eql(["#{fixtures_dir}manifests/eof_syntax.pp"]) 31 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}manifests/eof_syntax.pp"].join("\n")).to match(%r{^Syntax error at end of input}) 32 | expect(PuppetCheck.files[:warnings]).to eql({}) 33 | expect(PuppetCheck.files[:clean]).to eql([]) 34 | end 35 | end 36 | it 'puts a bad syntax Puppet plan in the error files hash' do 37 | PuppetParser.manifest(["#{fixtures_dir}plans/syntax.pp"], false, []) 38 | expect(PuppetCheck.files[:errors].keys).to eql(["#{fixtures_dir}plans/syntax.pp"]) 39 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}plans/syntax.pp"].join("\n")).to match(%r{^Syntax error at '\)'}) 40 | expect(PuppetCheck.files[:warnings]).to eql({}) 41 | expect(PuppetCheck.files[:clean]).to eql([]) 42 | end 43 | it 'puts a bad parser style and lint style Puppet manifest in the warning files array' do 44 | PuppetParser.manifest(["#{fixtures_dir}manifests/style_parser.pp"], true, []) 45 | expect(PuppetCheck.files[:errors]).to eql({}) 46 | expect(PuppetCheck.files[:warnings].keys).to eql(["#{fixtures_dir}manifests/style_parser.pp"]) 47 | expect(PuppetCheck.files[:warnings]["#{fixtures_dir}manifests/style_parser.pp"].join("\n")).to match(%r{^Unrecognized escape sequence.*\nUnrecognized escape sequence.*\n.*double quoted string containing}) 48 | expect(PuppetCheck.files[:clean]).to eql([]) 49 | end 50 | it 'puts a bad lint style Puppet manifest in the warning files array' do 51 | PuppetParser.manifest(["#{fixtures_dir}manifests/style_lint.pp"], true, []) 52 | expect(PuppetCheck.files[:errors]).to eql({}) 53 | expect(PuppetCheck.files[:warnings].keys).to eql(["#{fixtures_dir}manifests/style_lint.pp"]) 54 | expect(PuppetCheck.files[:warnings]["#{fixtures_dir}manifests/style_lint.pp"].join("\n")).to match(%r{(?:indentation of|double quoted string containing).*\n.*(?:indentation of|double quoted string containing)}) 55 | expect(PuppetCheck.files[:clean]).to eql([]) 56 | end 57 | it 'puts a bad style Puppet manifest in the clean files array when puppetlint_args ignores its warnings' do 58 | PuppetParser.manifest(["#{fixtures_dir}manifests/style_lint.pp"], true, ['--no-double_quoted_strings-check', '--no-arrow_alignment-check']) 59 | expect(PuppetCheck.files[:errors]).to eql({}) 60 | expect(PuppetCheck.files[:warnings]).to eql({}) 61 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}manifests/style_lint.pp"]) 62 | end 63 | it 'puts a bad style Puppet plan in the warning files array' do 64 | PuppetParser.manifest(["#{fixtures_dir}plans/style.pp"], true, []) 65 | expect(PuppetCheck.files[:errors]).to eql({}) 66 | expect(PuppetCheck.files[:warnings].keys).to eql(["#{fixtures_dir}plans/style.pp"]) 67 | expect(PuppetCheck.files[:warnings]["#{fixtures_dir}plans/style.pp"].join("\n")).to match(%r{variable not enclosed in {}}) 68 | expect(PuppetCheck.files[:clean]).to eql([]) 69 | end 70 | it 'puts a good Puppet manifest in the clean files array' do 71 | PuppetParser.manifest(["#{fixtures_dir}manifests/good.pp"], true, []) 72 | expect(PuppetCheck.files[:errors]).to eql({}) 73 | expect(PuppetCheck.files[:warnings]).to eql({}) 74 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}manifests/good.pp"]) 75 | end 76 | it 'puts a good Puppet plan in the clean files array' do 77 | PuppetParser.manifest(["#{fixtures_dir}plans/good.pp"], true, []) 78 | expect(PuppetCheck.files[:errors]).to eql({}) 79 | expect(PuppetCheck.files[:warnings]).to eql({}) 80 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}plans/good.pp"]) 81 | end 82 | it 'throws a well specified error for an invalid PuppetLint argument' do 83 | expect { PuppetParser.manifest(["#{fixtures_dir}manifests/style_lint.pp"], true, ['--non-existent', '--does-not-exist']) }.to raise_error(RuntimeError, 'puppet-lint: invalid option supplied among --non-existent --does-not-exist') 84 | end 85 | end 86 | 87 | context '.template' do 88 | it 'puts a bad syntax Puppet template in the error files hash' do 89 | PuppetParser.template(["#{fixtures_dir}templates/syntax.epp"]) 90 | expect(PuppetCheck.files[:errors].keys).to eql(["#{fixtures_dir}templates/syntax.epp"]) 91 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}templates/syntax.epp"].join("\n")).to match(%r{^This Name has no effect}) 92 | expect(PuppetCheck.files[:warnings]).to eql({}) 93 | expect(PuppetCheck.files[:clean]).to eql([]) 94 | end 95 | it 'puts a good Puppet template in the clean files array' do 96 | PuppetParser.template(["#{fixtures_dir}templates/good.epp"]) 97 | expect(PuppetCheck.files[:errors]).to eql({}) 98 | expect(PuppetCheck.files[:warnings]).to eql({}) 99 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}templates/good.epp"]) 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/puppet-check/regression_check_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | require_relative '../../lib/puppet-check/regression_check' 3 | 4 | # once a good config is loaded, bad configs would have no effect so failures are tested first; config is also tested before compilation so that the good config can be loaded at the end and used for compilation and regression tests 5 | describe RegressionCheck do 6 | context '.config' do 7 | # json gem got messed up for the EOL Ruby versions 8 | it 'raise an appropriate error if the file is malformed' do 9 | expect { RegressionCheck.config("#{fixtures_dir}metadata.json") }.to raise_error(OctocatalogDiff::Errors::ConfigurationFileContentError, 'Configuration must define OctocatalogDiff::Config!') 10 | end 11 | it 'loads in a good octocatalog-diff config file' do 12 | expect { RegressionCheck.config("#{octocatalog_diff_dir}octocatalog_diff.cfg.rb") }.not_to raise_exception 13 | end 14 | it 'loads in the settings from the file correctly' do 15 | end 16 | end 17 | 18 | context '.smoke' do 19 | # octocatalog-diff is returning a blank error for these tests 20 | unless ENV['CIRCLECI'] == 'true' || ENV['GITHUB_ACTIONS'] == 'true' 21 | it 'returns a pass for a successful catalog compilation' do 22 | expect { RegressionCheck.smoke(['good.example.com'], "#{octocatalog_diff_dir}octocatalog_diff.cfg.rb") }.not_to raise_exception 23 | end 24 | it 'returns a failure for a catalog with an error' do 25 | expect { RegressionCheck.smoke(['does_not_exist.example.com'], "#{octocatalog_diff_dir}octocatalog_diff.cfg.rb") }.to raise_error(OctocatalogDiff::Errors::CatalogError) 26 | end 27 | end 28 | it 'returns a failure for a good and bad catalog' do 29 | # RegressionCheck.smoke(['good.example.com', 'syntax_error.example.com'], "#{fixtures_dir}octocatalog_diff.cfg.rb") 30 | end 31 | end 32 | 33 | context '.regression' do 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/puppet-check/rspec_puppet_support_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | require_relative '../../lib/puppet-check/rspec_puppet_support' 3 | require 'fileutils' 4 | 5 | describe RSpecPuppetSupport do 6 | after(:all) do 7 | # cleanup rspec_puppet_setup 8 | File.delete("#{fixtures_dir}/spec/spec_helper.rb") 9 | %w[manifests modules].each { |dir| FileUtils.rm_r("#{fixtures_dir}/spec/fixtures/#{dir}") } 10 | end 11 | 12 | context '.run' do 13 | let(:rspec_puppet_setup) { RSpecPuppetSupport.run } 14 | before(:each) { Dir.chdir(fixtures_dir) } 15 | 16 | it 'creates missing directories, missing site.pp, missing symlinks, and a missing spec_helper' do 17 | # circle ci and gh actions 18 | if ENV['CIRCLECI'] == 'true' || ENV['GITHUB_ACTIONS'] == 'true' 19 | expect { rspec_puppet_setup }.to output("git is not installed and cannot be used to retrieve dependency modules\nsubversion is not installed and cannot be used to retrieve dependency modules\npuppetlabs/gruntmaster has an unspecified, or specified but unsupported, download method.\n").to_stderr 20 | else 21 | expect { rspec_puppet_setup }.to output("subversion is not installed and cannot be used to retrieve dependency modules\npuppetlabs/gruntmaster has an unspecified, or specified but unsupported, download method.\n").to_stderr 22 | end 23 | 24 | # .file_setup 25 | expect(File.directory?('spec/fixtures/manifests')).to be true 26 | expect(File.directory?('spec/fixtures/modules')).to be true 27 | expect(File.file?('spec/fixtures/manifests/site.pp')).to be true 28 | expect(File.symlink?('spec/fixtures/modules/fixtures')).to be true 29 | expect(File.file?('spec/spec_helper.rb')).to be true 30 | 31 | # .dependency_setup 32 | expect(File.directory?('spec/fixtures/modules/puppetlabs-lvm')).to be true unless ENV['CIRCLECI'] == 'true' || ENV['GITHUB_ACTIONS'] == 'true' 33 | expect(File.directory?('spec/fixtures/modules/stdlib')).to be true 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/puppet-check/ruby_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | require_relative '../../lib/puppet-check/ruby_parser' 3 | 4 | describe RubyParser do 5 | before(:each) do 6 | PuppetCheck.files = { 7 | errors: {}, 8 | warnings: {}, 9 | clean: [], 10 | ignored: [] 11 | } 12 | end 13 | 14 | context '.ruby' do 15 | it 'puts a bad syntax ruby file in the error files hash' do 16 | RubyParser.ruby(["#{fixtures_dir}lib/syntax.rb"], false, []) 17 | expect(PuppetCheck.files[:errors].keys).to eql(["#{fixtures_dir}lib/syntax.rb"]) 18 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}lib/syntax.rb"].join("\n")).to match(%r{^.*syntax error}) 19 | expect(PuppetCheck.files[:warnings]).to eql({}) 20 | expect(PuppetCheck.files[:clean]).to eql([]) 21 | end 22 | it 'puts a bad style ruby file in the warning files array' do 23 | RubyParser.ruby(["#{fixtures_dir}lib/style.rb"], true, []) 24 | expect(PuppetCheck.files[:errors]).to eql({}) 25 | expect(PuppetCheck.files[:warnings].keys).to eql(["#{fixtures_dir}lib/style.rb"]) 26 | expect(PuppetCheck.files[:warnings]["#{fixtures_dir}lib/style.rb"].join("\n")).to match(%r{Useless assignment.*\n.*Use the new.*\n.*Do not introduce.*\n.*Prefer single.*\n.*Remove unnecessary empty.*\n.*Source code comment is empty.*\n.*is a writable attribute.*\n.*Issue has no descriptive comment}) 27 | expect(PuppetCheck.files[:clean]).to eql([]) 28 | end 29 | it 'puts a bad style ruby file in the clean files array when rubocop_args ignores its warnings' do 30 | RubyParser.ruby(["#{fixtures_dir}lib/rubocop_style.rb"], true, ['--except', 'Lint/UselessAssignment,Style/HashSyntax,Style/GlobalVars,Style/StringLiterals']) 31 | expect(PuppetCheck.files[:errors]).to eql({}) 32 | expect(PuppetCheck.files[:warnings]).to eql({}) 33 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}lib/rubocop_style.rb"]) 34 | end 35 | it 'puts a good ruby file in the clean files array' do 36 | RubyParser.ruby(["#{fixtures_dir}lib/good.rb"], true, []) 37 | expect(PuppetCheck.files[:errors]).to eql({}) 38 | expect(PuppetCheck.files[:warnings]).to eql({}) 39 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}lib/good.rb"]) 40 | end 41 | end 42 | 43 | context '.template' do 44 | it 'puts a bad syntax ruby template file in the error files hash' do 45 | RubyParser.template(["#{fixtures_dir}templates/syntax.erb"]) 46 | expect(PuppetCheck.files[:errors].keys).to eql(["#{fixtures_dir}templates/syntax.erb"]) 47 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7') 48 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}templates/syntax.erb"].join("\n")).to match(%r{1: syntax error, unexpected.*\n.*ruby}) 49 | else 50 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}templates/syntax.erb"].join("\n")).to match(%r{syntax error, unexpected tIDENTIFIER}) 51 | end 52 | expect(PuppetCheck.files[:warnings]).to eql({}) 53 | expect(PuppetCheck.files[:clean]).to eql([]) 54 | end 55 | it 'puts a bad style ruby template file in the warning files array' do 56 | RubyParser.template(["#{fixtures_dir}templates/style.erb"]) 57 | expect(PuppetCheck.files[:errors]).to eql({}) 58 | expect(PuppetCheck.files[:warnings].keys).to eql(["#{fixtures_dir}templates/style.erb"]) 59 | expect(PuppetCheck.files[:warnings]["#{fixtures_dir}templates/style.erb"].join("\n")).to match(%r{already initialized constant.*\n.*previous definition of}) 60 | expect(PuppetCheck.files[:clean]).to eql([]) 61 | end 62 | it 'puts a ruby template file with ignored errors in the clean files array' do 63 | RubyParser.template(["#{fixtures_dir}templates/no_method_error.erb"]) 64 | expect(PuppetCheck.files[:errors]).to eql({}) 65 | expect(PuppetCheck.files[:warnings]).to eql({}) 66 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}templates/no_method_error.erb"]) 67 | end 68 | it 'puts a good ruby template file in the clean files array' do 69 | RubyParser.template(["#{fixtures_dir}templates/good.erb"]) 70 | expect(PuppetCheck.files[:errors]).to eql({}) 71 | expect(PuppetCheck.files[:warnings]).to eql({}) 72 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}templates/good.erb"]) 73 | end 74 | end 75 | 76 | context '.librarian' do 77 | it 'puts a bad syntax librarian Puppet file in the error files hash' do 78 | RubyParser.librarian(["#{fixtures_dir}librarian_syntax/Puppetfile"], false, []) 79 | expect(PuppetCheck.files[:errors].keys).to eql(["#{fixtures_dir}librarian_syntax/Puppetfile"]) 80 | expect(PuppetCheck.files[:errors]["#{fixtures_dir}librarian_syntax/Puppetfile"].join("\n")).to match(%r{^.*syntax error}) 81 | expect(PuppetCheck.files[:warnings]).to eql({}) 82 | expect(PuppetCheck.files[:clean]).to eql([]) 83 | end 84 | it 'puts a bad style librarian Puppet file in the warning files array' do 85 | RubyParser.librarian(["#{fixtures_dir}librarian_style/Puppetfile"], true, []) 86 | expect(PuppetCheck.files[:errors]).to eql({}) 87 | expect(PuppetCheck.files[:warnings].keys).to eql(["#{fixtures_dir}librarian_style/Puppetfile"]) 88 | expect(PuppetCheck.files[:warnings]["#{fixtures_dir}librarian_style/Puppetfile"].join("\n")).to match(%r{Align the arguments.*\n.*Use the new}) 89 | expect(PuppetCheck.files[:clean]).to eql([]) 90 | end 91 | it 'puts a bad style librarian Puppet file in the clean files array when rubocop_args ignores its warnings' do 92 | RubyParser.librarian(["#{fixtures_dir}librarian_style/Puppetfile"], true, ['--except', 'Layout/ArgumentAlignment,Style/HashSyntax']) 93 | expect(PuppetCheck.files[:errors]).to eql({}) 94 | expect(PuppetCheck.files[:warnings]).to eql({}) 95 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}librarian_style/Puppetfile"]) 96 | end 97 | it 'puts a good librarian Puppet file in the clean files array' do 98 | RubyParser.librarian(["#{fixtures_dir}librarian_good/Puppetfile"], true, []) 99 | expect(PuppetCheck.files[:errors]).to eql({}) 100 | expect(PuppetCheck.files[:warnings]).to eql({}) 101 | expect(PuppetCheck.files[:clean]).to eql(["#{fixtures_dir}librarian_good/Puppetfile"]) 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/puppet-check/tasks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rake/task' 2 | require_relative '../spec_helper' 3 | require_relative '../../lib/puppet-check/tasks' 4 | 5 | describe PuppetCheck::Tasks do 6 | after(:all) do 7 | # cleanup rspec_puppet_setup 8 | File.delete("#{fixtures_dir}/spec/spec_helper.rb") 9 | %w[manifests modules].each { |dir| FileUtils.rm_r("#{fixtures_dir}/spec/fixtures/#{dir}") } 10 | end 11 | 12 | context 'puppetcheck:spec' do 13 | let(:spec_tasks) { Rake::Task[:'puppetcheck:spec'].invoke } 14 | 15 | it 'executes RSpec and RSpec-Puppet checks in the expected manner' do 16 | Dir.chdir(fixtures_dir) 17 | 18 | # rspec task executed 19 | expect { spec_tasks }.to output(%r{spec/facter/facter_spec.rb}).to_stdout 20 | # if this is first then the stdout is not captured for testing 21 | expect { spec_tasks }.not_to raise_exception 22 | end 23 | end 24 | 25 | context 'puppetcheck:beaker' do 26 | let(:beaker_task) { Rake::Task[:'puppetcheck:beaker'].invoke } 27 | 28 | it 'verifies the Beaker task exists' do 29 | # beaker task executed 30 | expect { beaker_task }.to output("Beaker is not installed. The Beaker tasks will not be available.\n").to_stdout 31 | end 32 | end 33 | 34 | context 'puppetcheck:kitchen' do 35 | let(:kitchen_task) { Rake::Task[:'puppetcheck:kitchen'].invoke } 36 | 37 | it 'verifies the Kitchen task exists' do 38 | # kitchen task executed 39 | expect { kitchen_task }.to output("Test Kitchen is not installed. The Kitchen tasks will not be available.\n").to_stdout 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/puppet-check/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | require_relative '../../lib/puppet-check/utils' 3 | 4 | describe Utils do 5 | context '.capture_stdout' do 6 | let(:stdout_test) { Utils.capture_stdout { puts 'hello world' } } 7 | 8 | it 'captures the stdout from a block of code' do 9 | expect(stdout_test.chomp).to eql('hello world') 10 | end 11 | end 12 | 13 | context '.capture_stderr' do 14 | let(:stderr_test) { Utils.capture_stderr { warn 'hello world' } } 15 | 16 | it 'captures the stderr from a block of code' do 17 | expect(stderr_test.chomp).to eql('hello world') 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/puppet_check_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | require_relative '../lib/puppet_check' 3 | 4 | describe PuppetCheck do 5 | context 'self' do 6 | it 'files are initialized correctly' do 7 | expect(PuppetCheck.files).to eql( 8 | { 9 | errors: {}, 10 | warnings: {}, 11 | clean: [], 12 | ignored: [] 13 | } 14 | ) 15 | end 16 | it 'files can be altered' do 17 | PuppetCheck.files = { 18 | errors: { 'foo' => ['i had an error'] }, 19 | warnings: { 'foo' => ['i had a warning'] }, 20 | clean: ['foo'], 21 | ignored: ['foo'] 22 | } 23 | expect(PuppetCheck.files).to eql( 24 | { 25 | errors: { 'foo' => ['i had an error'] }, 26 | warnings: { 'foo' => ['i had a warning'] }, 27 | clean: ['foo'], 28 | ignored: ['foo'] 29 | } 30 | ) 31 | end 32 | end 33 | 34 | context 'defaults' do 35 | it 'returns defaults correctly' do 36 | expect(PuppetCheck.defaults).to eql( 37 | { 38 | fail_on_warning: false, 39 | style: false, 40 | smoke: false, 41 | regression: false, 42 | public: nil, 43 | private: nil, 44 | output_format: 'text', 45 | octoconfig: '.octocatalog-diff.cfg.rb', 46 | octonodes: %w[localhost.localdomain], 47 | puppetlint_args: [], 48 | rubocop_args: [] 49 | } 50 | ) 51 | end 52 | 53 | it 'modifies settings correctly' do 54 | settings = { 55 | fail_on_warning: true, 56 | style: true, 57 | smoke: true, 58 | regression: true, 59 | public: 'public.pem', 60 | private: 'private.pem', 61 | output_format: 'yaml', 62 | octoconfig: '.octocatalog-diff.cfg.erb', 63 | octonodes: %w[host.domain], 64 | puppetlint_args: %w[--puppetlint-arg-one --puppetlint-arg-two], 65 | rubocop_args: %w[--rubocop-arg-one --rubocop-arg-two] 66 | } 67 | expect(PuppetCheck.defaults(settings)).to eql(settings) 68 | end 69 | end 70 | 71 | context '.parse_paths' do 72 | before(:each) { Dir.chdir(fixtures_dir) } 73 | 74 | let(:no_files) { PuppetCheck.parse_paths(%w[foo bar baz]) } 75 | let(:file) { PuppetCheck.parse_paths(['lib/good.rb']) } 76 | let(:dir) { PuppetCheck.parse_paths(['.']) } 77 | let(:multi_dir) { PuppetCheck.parse_paths(%w[hieradata lib manifests]) } 78 | let(:repeats) { PuppetCheck.parse_paths(['hieradata', 'hieradata', 'lib', 'hieradata/good.json', 'manifests/good.pp', 'manifests/good.pp']) } 79 | 80 | it 'raises an error if no files were found' do 81 | expect { no_files }.to raise_error(RuntimeError, 'puppet-check: no files found in supplied paths \'foo, bar, baz\'.') 82 | end 83 | 84 | it 'correctly parses one file and returns it' do 85 | expect(file[0]).to eql('lib/good.rb') 86 | end 87 | 88 | it 'correctly parses one directory and returns all of its files' do 89 | dir.each { |file| expect(File.file?(file)).to be true } 90 | if ENV['CIRCLECI'] == 'true' || ENV['GITHUB_ACTIONS'] == 'true' 91 | expect(dir.length).to eql(37) 92 | else 93 | expect(dir.length).to eql(40) 94 | end 95 | end 96 | 97 | it 'correctly parses multiple directories and returns all of their files' do 98 | multi_dir.each { |file| expect(File.file?(file)).to be true } 99 | expect(multi_dir.length).to eql(17) 100 | end 101 | 102 | it 'correctly parses three directories (one repeated) and three files (one repeated from directories and another repeated from files) and returns the unique files' do 103 | repeats.each { |file| expect(File.file?(file)).to be true } 104 | expect(repeats.length).to eql(13) 105 | end 106 | end 107 | 108 | context '.execute_parsers' do 109 | it 'correctly organizes a set of files and invokes the correct parsers' do 110 | # parser_output = instance_double('execute_parsers', files: %w[puppet.pp puppet_template.epp ruby.rb ruby_template.erb yaml.yaml yaml.yml json.json Puppetfile Modulefile foobarbaz], style: false, pl_args: [], rc_args: []) 111 | # expect(parser_output).to receive(:manifest).with(%w[puppet.pp]) 112 | # expect(parser_output).to receive(:template).with(%w[puppet_template.epp]) 113 | # expect(parser_output).to receive(:ruby).with(%w[ruby.rb]) 114 | # expect(parser_output).to receive(:template).with(%w[ruby_template.erb]) 115 | # expect(parser_output).to receive(:yaml).with(%w[yaml.yaml yaml.yml]) 116 | # expect(parser_output).to receive(:json).with(%w[json.json]) 117 | # expect(parser_output).to receive(:librarian).with(%w[Puppetfile Modulefile]) 118 | # expect(PuppetCheck.files[:ignored]).to eql(%w[foobarbaz]) 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | 3 | # for path to fixtures 4 | module Variables 5 | extend RSpec::SharedContext 6 | 7 | def fixtures_dir 8 | @fixtures_dir = "#{File.dirname(__FILE__)}/fixtures/" 9 | end 10 | 11 | def octocatalog_diff_dir 12 | @octocatalog_diff_dir = "#{File.dirname(__FILE__)}/octocatalog-diff/" 13 | end 14 | end 15 | 16 | RSpec.configure do |config| 17 | config.include Variables 18 | config.color = true 19 | end 20 | -------------------------------------------------------------------------------- /spec/system/system_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rake/task' 2 | require_relative '../spec_helper' 3 | require_relative '../../lib/puppet-check/cli' 4 | require_relative '../../lib/puppet-check/tasks' 5 | 6 | describe PuppetCheck do 7 | context 'executed as a system from the CLI with arguments and various files to be processed' do 8 | # see regression_check_spec 9 | if ENV['CIRCLECI'] == 'true' || ENV['GITHUB_ACTIONS'] == 'true' 10 | let(:cli) { PuppetCheck::CLI.run(%w[-s --puppet-lint no-hard_tabs-check,no-140chars-check --rubocop Layout/LineLength,Style/Encoding --public keys/public_key.pkcs7.pem --private keys/private_key.pkcs7.pem .]) } 11 | else 12 | let(:cli) { PuppetCheck::CLI.run(%w[-s --puppet-lint no-hard_tabs-check,no-140chars-check --rubocop Layout/LineLength,Style/Encoding --public keys/public_key.pkcs7.pem --private keys/private_key.pkcs7.pem --smoke -n good.example.com --octoconfig spec/octocatalog-diff/octocatalog-diff.cfg.rb .]) } 13 | end 14 | 15 | it 'outputs diagnostic results correctly after processing all of the files' do 16 | Dir.chdir(fixtures_dir) 17 | 18 | expect { cli }.not_to raise_exception 19 | 20 | expect(PuppetCheck.files[:errors].length).to eql(11) 21 | expect(PuppetCheck.files[:warnings].length).to eql(12) 22 | expect(PuppetCheck.files[:clean].length).to eql(14) 23 | expect(PuppetCheck.files[:ignored].length).to eql(3) 24 | 25 | expect(cli).to eql(2) 26 | end 27 | end 28 | 29 | context 'executed as a system from the Rakefile with arguments and various files to be processed' do 30 | it 'outputs diagnostic results correctly after processing all of the files' do 31 | # ensure rake only checks the files inside fixtures 32 | Dir.chdir(fixtures_dir) 33 | 34 | # clear out files member from previous system test 35 | PuppetCheck.files = { 36 | errors: {}, 37 | warnings: {}, 38 | clean: [], 39 | ignored: [] 40 | } 41 | settings = { style: true } 42 | # see regression_check_spec 43 | unless ENV['CIRCLECI'] == 'true' || ENV['GITHUB_ACTIONS'] == 'true' 44 | settings[:smoke] = true 45 | settings[:octonodes] = %w[good.example.com] 46 | settings[:octoconfig] = 'spec/octocatalog-diff/octocatalog-diff.cfg.rb' 47 | end 48 | 49 | # cannot re-use plan fixture between system tests 50 | expect { Rake::Task[:'puppetcheck:file'].invoke(settings) }.to raise_error(ArgumentError, /Attempt to redefine entity/) 51 | 52 | # current puppet pops limitations no longer allow testing this 53 | # expect(PuppetCheck.files[:errors].length).to eql(11) 54 | # expect(PuppetCheck.files[:warnings].length).to eql(12) 55 | # expect(PuppetCheck.files[:clean].length).to eql(14) 56 | # expect(PuppetCheck.files[:ignored].length).to eql(3) 57 | end 58 | end 59 | end 60 | --------------------------------------------------------------------------------