puppet_rake_tasks 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # PuppetModuleTask
2
+
3
+ TODO: Delete this and describe your gem
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'puppet_rake_tasks'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install puppet_rake_tasks
20
+
21
+ ## Usage
22
+
23
+ ### Simple usage:
24
+
25
+ 1. Add the ruby gem: `gem 'puppet_rake_tasks'`
26
+ 2. Require the depchecker in your `Rakefile`: `require 'puppet_rake_tasks/depchecker'`
27
+ 3. Create the task: `PuppetRakeTasks::DepChecker::Task.new`
28
+ 4. Profit: `rake exec depcheck
29
+
30
+ ### Advanced usage:
31
+
32
+ Optionally, you can also configure the task:
33
+
34
+ ```ruby
35
+ require 'puppet_rake_tasks/depchecker'
36
+ PuppetRakeTasks::DepChecker::Task.new do |checker|
37
+ checker.fail_on_error = true
38
+ checker.modulepath = [
39
+ 'site',
40
+ 'modules'
41
+ ]
42
+ checker.ignore /.*/, { name: 'foo/bar', reason: :missing }
43
+ end
44
+ ```
45
+
46
+ ## Development
47
+
48
+ After checking out the repo, run `bundler install` to install dependencies. Then, run `bundle exec rake spec` to run the tests. You can also run `bundle console` for an interactive prompt that will allow you to experiment.
49
+
50
+ ## Contributing
51
+
52
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/vStone/puppet_rake_tasks).
53
+
54
+ ## License
55
+
56
+ The gem is available as open source under the terms of the [GPL-3 License](http://opensource.org/licenses/GPL-3.0).
57
+
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
4
+ require 'yard'
5
+ require 'yard/rake/yardoc_task'
6
+
7
+ desc 'Run rubocop'
8
+ RuboCop::RakeTask.new(:rubocop) do |task|
9
+ task.formatters = %w(fuubar offenses)
10
+ task.fail_on_error = true
11
+ end
12
+
13
+ RSpec::Core::RakeTask.new(:spec)
14
+ YARD::Rake::YardocTask.new(:docs)
15
+
16
+ task default: [:rubocop, :spec]
@@ -0,0 +1,38 @@
1
+ module PuppetRakeTasks
2
+ module DepChecker
3
+ class Resolver
4
+ # Apply ignore rules to incidents and return the filtered results
5
+ module Filter
6
+ # @return [Hash] list with filtered incidents (cached).
7
+ def filtered
8
+ @filtered ||= filter_incidents
9
+ end
10
+
11
+ private
12
+
13
+ # This method calls ignores_matches_incident for each issue and removes it
14
+ # if the result of the call is true. The ignores_matches_incident will loop
15
+ # all configured ignores (global/wildcards or specific for the module)
16
+ # @param issues [Array] An array with all issues related to the module to check.
17
+ # @param modulename [String] simple module name to filter incidents for.
18
+ # @return [Array] with filtered issues.
19
+ def filter_module_incidents(modulename, issues)
20
+ ## Reject all incidents that match an ignore rule
21
+ issues.reject do |incident|
22
+ ignores_matches_incident(modulename, incident)
23
+ end
24
+ end
25
+
26
+ # Loop all incidents and apply the ignore filters for each module.
27
+ # Modules without no incidents (emtpy array) are also removed from the result.
28
+ # @return [Hash] filtered incidents.
29
+ def filter_incidents
30
+ intermediate = incidents.update(incidents) do |modulename, issues|
31
+ filter_module_incidents(modulename, issues)
32
+ end
33
+ intermediate.reject { |_m, i| i.empty? }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,70 @@
1
+ module PuppetRakeTasks
2
+ module DepChecker
3
+ # General helpers for various methods.
4
+ module Helpers
5
+ class << self
6
+ # Normalize paths split by `File::PATH_SEPARATOR` by making sure it always
7
+ # returns an array of paths.
8
+ # @param path [String,Array] A string or an array with paths.
9
+ # @return [Array] The normalized paths
10
+ def normalize_path(path)
11
+ if path.is_a?(String)
12
+ path.split(File::PATH_SEPARATOR)
13
+ else
14
+ [path].flatten
15
+ end
16
+ end
17
+
18
+ # Compare values by converting symbols to strings or by using
19
+ # regexes to match if the ignore_value is one.
20
+ # @param ignore_value [String,Regex,Symbol] Value to compare
21
+ # @param incident_value [String, Symbol] Value to compare against.
22
+ # @return [Boolean] true or false depending on the equality-ish-ness.
23
+ def compare_values(ignore_value, incident_value)
24
+ if ignore_value.is_a?(Regexp)
25
+ !ignore_value.match(incident_value.to_s).nil?
26
+ else
27
+ incident_value = incident_value.is_a?(Symbol) ? incident_value.to_s : incident_value
28
+ ignore_value = ignore_value.is_a?(Symbol) ? ignore_value.to_s : ignore_value
29
+ incident_value == ignore_value
30
+ end
31
+ end
32
+
33
+ # Swats hash keys and turns them into symbols. See example for what swatting is.
34
+ #
35
+ # @example Swatting a hash.
36
+ # swat_hash({
37
+ # 'foo' => {
38
+ # 'bar' => 'rab',
39
+ # 'foo' => {
40
+ # 'super' => 'nested'
41
+ # },
42
+ # },
43
+ # 'extra' => 'value'
44
+ # }, '_') #=>
45
+ # {
46
+ # :foo_bar => 'rab',
47
+ # :foo_foo_super => 'nested',
48
+ # :extra => 'value'
49
+ # }
50
+ #
51
+ # @param hash [Hash] hash to swat.
52
+ # @param prefix [String] prefix to add to the keys. This is normally only filled in by recursive calls.
53
+ # @param glue [String] String to use to join the keys.
54
+ # @return [Hash<Symbol, Any>] Hash without nesting.
55
+ def swat_hash(hash, glue = '.', prefix = nil)
56
+ prefix = prefix.nil? ? '' : "#{prefix}#{glue}"
57
+ intermediate = {}
58
+ hash.each do |k, v|
59
+ if v.is_a?(Hash)
60
+ intermediate.merge!(swat_hash(v, glue, "#{prefix}#{k}"))
61
+ else
62
+ intermediate["#{prefix}#{k}".to_sym] = v
63
+ end
64
+ end
65
+ intermediate
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,107 @@
1
+ require_relative 'helpers'
2
+
3
+ module PuppetRakeTasks
4
+ module DepChecker
5
+ class Resolver
6
+ # Deal with management of the ignore rules you can set.
7
+ # You can either set all rules in one go using {#ignores=} or add one by one
8
+ # using the {#ignore} method.
9
+ #
10
+ # ## Reference:
11
+ # When referring to the 'simple name' of a puppet module, use the name without `author/` in front.
12
+ # You can not have a module with the same name installed in your tree anyhow.
13
+ #
14
+ # @example Adding a rule for a single module.
15
+ # # Ignores the stdlib version mismatch on `your_module`.
16
+ # @depchecker.ignore 'your_module', { name: 'puppetlabs/stdlib', reason: :version_mismatch }
17
+ #
18
+ # @example Adding a rule for a single module with regexp values.
19
+ # # Ignores all modules from author `custom` that are missing.
20
+ # @depchecker.ignore 'your_module', { name: %r{^custom/.*}, reason: :missing }
21
+ #
22
+ # @example Adding a rule for all modules using a regexp.
23
+ # # ignores a missing module for all modules that might declare it as a dependency.
24
+ # @depchecker.ignore %r{.*}, { name: 'foobar/module', reason: :missing }
25
+ #
26
+ # @example Loading ignores from a (yaml) file
27
+ # @depchecker.ignores = YAML.load_file('.ignores.yaml')
28
+ # # .ignores.yaml
29
+ # # ---
30
+ # # !ruby/regexp /.*/:
31
+ # # - :name: always/missing
32
+ # # :reason: :missing
33
+ # # foobar:
34
+ # # - :name: !ruby/regexp /.*/
35
+ # # :reason: :version_mismatch
36
+ #
37
+ module Ignores
38
+ extend Helpers
39
+
40
+ # Configure all ignore rules at once by setting a single hash.
41
+ # This clears the cached ignore rules for modules.
42
+ # @param ignores [Hash] Hash with ignore rules.
43
+ def ignores=(ignores)
44
+ @ignores = ignores
45
+ @ignores_for_module = {}
46
+ end
47
+
48
+ # @param for_module [String,Regexp] module name or regex this rule applies to.
49
+ # @param expr [Hash] ignore expression.
50
+ def ignore(for_module, expr)
51
+ # reset cached ignores matching modules
52
+ (@ignores_for_module ||= {})[for_module] = nil
53
+
54
+ # initialize if not exists
55
+ (@ignores ||= {})[for_module] ||= []
56
+ @ignores[for_module] << expr
57
+ end
58
+
59
+ # Returns all configured ignore rules.
60
+ # @return [Hash] ignore rules.
61
+ def ignores
62
+ @ignores ||= {}
63
+ end
64
+
65
+ # Returns ignore rules that might apply to the modulename and caches the result.
66
+ # @param modulename [String] module name to get ignore rules for in its simple form.
67
+ def ignores_for_module(modulename)
68
+ if (@ignores_for_module ||= {})[modulename].nil?
69
+ @ignores_for_module[modulename] ||= collect_ignores_for_module(modulename)
70
+ end
71
+ @ignores_for_module[modulename]
72
+ end
73
+
74
+ # Loop over ignores for a modulename and check if it matches the incident.
75
+ # @param modulename [String] puppet module name.
76
+ # @param incident [Hash] The module incident to check.
77
+ def ignores_matches_incident(modulename, incident)
78
+ loop_ignores = ignores_for_module(modulename)
79
+ # Look for a match, return true immediately, otherwise, false
80
+ loop_ignores.each do |ign|
81
+ this_ignore = true
82
+ ign.keys.each do |k|
83
+ compare = Helpers.compare_values(ign[k], incident[k])
84
+ this_ignore = false unless compare
85
+ end
86
+ return true if this_ignore
87
+ end
88
+ false
89
+ end
90
+
91
+ private
92
+
93
+ # @param modulename [String] puppet module name. Must be in the 'simple' form.
94
+ # @return [Array] array with all ignores that apply to a certain puppet module.
95
+ def collect_ignores_for_module(modulename)
96
+ tmp_ignores = []
97
+ ignores.each do |k, i|
98
+ if (k.is_a?(String) && k == modulename) || (k.is_a?(Regexp) && modulename =~ k)
99
+ tmp_ignores += i
100
+ end
101
+ end
102
+ tmp_ignores
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,27 @@
1
+ module PuppetRakeTasks
2
+ module DepChecker
3
+ class Resolver
4
+ # Collect all incidents
5
+ module Incidents
6
+ # Returns all (cached) detected incidents in modules.
7
+ # @return [Hash] all incidents.
8
+ def incidents
9
+ @incidents ||= initialize_incidents
10
+ end
11
+
12
+ # Detect all incidents for the current env.
13
+ # @return [Hash] incidents.
14
+ def initialize_incidents
15
+ tmp_incidents = {}
16
+ modules.by_name.each do |name, mod|
17
+ mod.unmet_dependencies.each do |incident|
18
+ tmp_incidents[name] ||= []
19
+ tmp_incidents[name] << incident
20
+ end
21
+ end
22
+ tmp_incidents
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,79 @@
1
+ require 'pp'
2
+
3
+ module PuppetRakeTasks
4
+ module DepChecker
5
+ class Resolver
6
+ # Prints stuff to `stderr` and optionally raises an error
7
+ module Report
8
+ extend Helpers
9
+
10
+ # Make sure we don't throw a too generic error.
11
+ class DependencyError < StandardError
12
+ attr_reader :filtered
13
+ def initialize(message, filtered)
14
+ @filtered = filtered
15
+ super(message)
16
+ end
17
+ end
18
+
19
+ attr_writer :fail_on_error
20
+ attr_writer :format
21
+ attr_writer :output
22
+
23
+ def output
24
+ @output ||= $stderr
25
+ end
26
+
27
+ # Returns the format configured (or the default format)
28
+ # @return [String] string fed into sprintf.
29
+ def format
30
+ @format ||= 'ERROR: module %<module_name>s: %<reason>s dependency %<name>s. '\
31
+ "Wants: '%<version_constraint>s', got: '%<mod_details.installed_version>s'\n"
32
+ end
33
+
34
+ # Boolean indicating wether or not we will fail (raise error) when there are incidents.
35
+ # @return [Boolean]
36
+ def fail_on_error
37
+ @fail_on_error ||= false
38
+ end
39
+
40
+ # Gets all filtered incidents for all modules and reports them module by module.
41
+ # @raise [DependencyError] if there are any issues detected and `fail_on_error` is enabled (`true`)
42
+ def report
43
+ filtered.each do |module_name, module_incidents|
44
+ format_module_incidents(module_name, module_incidents)
45
+ end
46
+ raise DependencyError.new('Module errors found', filtered) unless filtered.empty? || !fail_on_error
47
+ end
48
+
49
+ # Adds extra information to the incident hash for reporting purposes.
50
+ # @param incident [Hash] Incident to enrich.
51
+ # @return [Hash] copy of the incident with extra data and a default_proc assigned.
52
+ def enrich_incident(incident)
53
+ incident = Helpers.swat_hash(incident)
54
+ # For recent enough ruby versions, this default proc will just show missing
55
+ # keys in stead of failing on them (when using sprintf).
56
+ incident.default_proc = proc { |hash, key| hash[key] = "%<#{key}>s" }
57
+ incident
58
+ end
59
+
60
+ # Format all incidents for a certain module.
61
+ # @param module_name [String] name of the module the incidents are detected for.
62
+ # @param module_incidents [Array<Hash>] All incidents.
63
+ def format_module_incidents(module_name, module_incidents)
64
+ module_incidents.each do |incident|
65
+ # Add the module_name to the incident
66
+ info_hash = incident.merge(module_name: module_name)
67
+ format_incident(info_hash)
68
+ end
69
+ end
70
+
71
+ # Format a single incident.
72
+ # @param info_hash [Hash] hash with all information for the incident.
73
+ def format_incident(info_hash)
74
+ output.puts(Kernel.format(format, enrich_incident(info_hash)))
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,69 @@
1
+ require 'puppet'
2
+ require 'puppet/module_tool'
3
+
4
+ require_relative 'helpers'
5
+ require_relative 'ignores'
6
+ require_relative 'incidents'
7
+ require_relative 'filter'
8
+ require_relative 'report'
9
+
10
+ module PuppetRakeTasks
11
+ module DepChecker
12
+ # Class to detect all puppet module dependency issues.
13
+ # It uses puppet module tool internals to detect all issues.
14
+ class Resolver
15
+ include Helpers
16
+ include Ignores
17
+ include Incidents
18
+ include Filter
19
+ include Report
20
+
21
+ def initialize(module_path = '.')
22
+ @modulepath = Helpers.normalize_path(module_path)
23
+ end
24
+
25
+ # Returns the puppet module tool environment.
26
+ # @see http://www.rubydoc.info/gems/puppet/Puppet/ModuleTool#environment_from_options-class_method
27
+ def env
28
+ @env ||= Puppet::ModuleTool.environment_from_options(modulepath: modulepath_s)
29
+ end
30
+
31
+ # Collect the installed modules using Puppet::ModuleTool.
32
+ # @see http://www.rubydoc.info/gems/puppet/Puppet/ModuleTool/InstalledModules
33
+ def modules
34
+ @modules ||= Puppet::ModuleTool::InstalledModules.new(env)
35
+ end
36
+
37
+ # Clear all cached data (env, modules, incidents)
38
+ def reset_caches
39
+ @env = nil
40
+ @modules = nil
41
+ @incidents = nil
42
+ end
43
+
44
+ # Returns the configured module paths.
45
+ # @return [Array<String>] configured module paths as an array.
46
+ def modulepath
47
+ @modulepath ||= ['.']
48
+ end
49
+
50
+ # Return modulepath as a string
51
+ # @return [String] module paths joined with `File::PATH_SEPARATOR`
52
+ def modulepath_s
53
+ @modulepath.flatten.join(File::PATH_SEPARATOR)
54
+ end
55
+
56
+ # Set the modulepath and normalize using {Helpers#normalize_path}
57
+ # When the module path changes, cached values are {#reset_caches cleared}
58
+ # @param [Array<String>,String] path Module paths
59
+ # @return [Array<String>] configured and normalized module paths.
60
+ def modulepath=(path)
61
+ unless path == @modulepath
62
+ # reset the env and loaded modules when changing the modulepath
63
+ reset_caches
64
+ end
65
+ @modulepath = Helpers.normalize_path(path)
66
+ end
67
+ end
68
+ end
69
+ end