kitchen-inspector 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG.md +24 -0
  5. data/Gemfile +0 -2
  6. data/README.md +268 -39
  7. data/Rakefile +41 -0
  8. data/kitchen-inspector.gemspec +21 -3
  9. data/lib/kitchen-inspector/inspector.rb +11 -4
  10. data/lib/kitchen-inspector/inspector/chef_inspector.rb +66 -0
  11. data/lib/kitchen-inspector/inspector/cli.rb +29 -3
  12. data/lib/kitchen-inspector/inspector/{error.rb → common.rb} +43 -1
  13. data/lib/kitchen-inspector/inspector/dependency.rb +26 -40
  14. data/lib/kitchen-inspector/inspector/health_bureau.rb +181 -0
  15. data/lib/kitchen-inspector/inspector/mixin/utils.rb +83 -0
  16. data/lib/kitchen-inspector/inspector/report/report.rb +182 -0
  17. data/lib/kitchen-inspector/inspector/report/status_reporter.rb +105 -0
  18. data/lib/kitchen-inspector/inspector/repository_inspector.rb +134 -0
  19. data/lib/kitchen-inspector/inspector/repository_managers/base.rb +110 -0
  20. data/lib/kitchen-inspector/inspector/repository_managers/github.rb +97 -0
  21. data/lib/kitchen-inspector/inspector/repository_managers/gitlab.rb +100 -0
  22. data/lib/kitchen-inspector/inspector/version.rb +1 -2
  23. data/spec/cli_spec.rb +46 -0
  24. data/spec/data/cookbook_deps/metadata.rb +10 -0
  25. data/spec/data/cookbook_no_deps/metadata.rb +7 -0
  26. data/spec/data/test_client.pem +27 -0
  27. data/spec/data/test_config_invalid.rb +4 -0
  28. data/spec/data/test_config_valid.rb +4 -0
  29. data/spec/dependency_inspector_spec.rb +296 -0
  30. data/spec/github_manager_spec.rb +79 -0
  31. data/spec/gitlab_manager_spec.rb +58 -0
  32. data/spec/report_spec.rb +237 -0
  33. data/spec/support/spec_helper.rb +81 -0
  34. data/spec/utils_spec.rb +29 -0
  35. metadata +129 -15
  36. data/INFO.md +0 -44
  37. data/info.css +0 -31
  38. data/lib/kitchen-inspector/inspector/dependency_inspector.rb +0 -153
  39. data/lib/kitchen-inspector/inspector/report.rb +0 -148
@@ -0,0 +1,83 @@
1
+ #
2
+ # Copyright (c) 2013 Stefano Tortarolo <stefano.tortarolo@gmail.com>
3
+ #
4
+ # MIT License
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #
25
+
26
+ module KitchenInspector
27
+ module Inspector
28
+ module Utils
29
+ def config_msg(human_name, field)
30
+ "#{human_name} not configured. Please set #{field} in your config file."
31
+ end
32
+
33
+ # Import a configuration from a file or StringIO
34
+ def read_config(config)
35
+ if config.is_a?(StringIO)
36
+ config.string
37
+ elsif File.exists?(config) && File.readable?(config)
38
+ IO.read(config)
39
+ else
40
+ raise ConfigurationError, "Unable to load the configuration: '#{config}'.\nPlease refer to README.md and check that a valid configuration was provided."
41
+ end
42
+ end
43
+
44
+ # Normalize version names to x.y.z...
45
+ def fix_version_name(version)
46
+ version.gsub(/[v][\.]*/i, "")
47
+ end
48
+
49
+ # Return from versions the best match that satisfies the given constraint
50
+ def satisfy(constraint, versions)
51
+ Solve::Solver.satisfy_best(constraint, versions).to_s
52
+ rescue Solve::Errors::NoSolutionError
53
+ nil
54
+ end
55
+
56
+ def get_latest_version(versions)
57
+ versions.collect do |v|
58
+ begin
59
+ Solve::Version.new(v)
60
+ rescue Solve::Errors::InvalidVersionFormat => e
61
+ # Skip invalid tags
62
+ Solve::Version.new("0.0.0")
63
+ end
64
+ end.max
65
+ end
66
+
67
+ # Evaluate only interesting lines
68
+ #
69
+ # Used also to ignore errors for missing
70
+ # files referenced in metadata.rb e.g., README.md
71
+ def eval_metadata(raw_response)
72
+ clean_response = raw_response.split("\n").select do |line|
73
+ line.strip!
74
+ line =~ /^depends|^name|^version/
75
+ end
76
+
77
+ metadata = Ridley::Chef::Cookbook::Metadata.new
78
+ metadata.instance_eval clean_response.join("\n")
79
+ metadata
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,182 @@
1
+ #
2
+ # Copyright (c) 2013 Stefano Tortarolo <stefano.tortarolo@gmail.com>
3
+ #
4
+ # MIT License
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #
25
+
26
+ module KitchenInspector
27
+ module Inspector
28
+ class Report
29
+ class << self
30
+ # Generates the status of dependent cookbooks in specified format
31
+ def generate(dependencies, format, opts={})
32
+ case format
33
+ when 'table'
34
+ TableReport.generate(dependencies, opts)
35
+ when'json'
36
+ JSONReport.generate(dependencies)
37
+ else
38
+ raise UnsupportedReportFormatError, "Report format '#{format}' is not supported"
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ # Reports the cookbook dependency status in a table format
45
+ #
46
+ class TableReport
47
+ class << self
48
+ # Generate a report in tabular format
49
+ #
50
+ # @param dependencies [Array<Dependency>] list of cookbook dependency objects
51
+ # @param opts [Hash] options (only flag to show remarks at this stage)
52
+ # @return [Array] printable report and global status code
53
+ def generate(dependencies, opts)
54
+ headings = ["Name", "Requirement", "Used", "Chef\nLatest", "Repository\nLatest", "Requirement\nStatus",
55
+ "Chef Server\nStatus", "Repository\nStatus"]
56
+
57
+ if opts[:remarks]
58
+ headings << "Remarks"
59
+ remarks_counter = 0
60
+ remarks = []
61
+ end
62
+
63
+ rows, remarks = generate_rows(dependencies, opts)
64
+
65
+ # Show Table
66
+ table = Terminal::Table.new headings: headings, rows: rows
67
+
68
+ # Show Status
69
+ g_status, g_status_code = StatusReporter.global_status(dependencies)
70
+
71
+ if opts[:remarks]
72
+ remarks_result = remarks.each_with_index.collect{|remark, idx| "[#{idx + 1}]: #{remark}"}.join("\n")
73
+ output = "#{table}\n#{g_status}\n\nRemarks:\n#{remarks_result}"
74
+ else
75
+ output = "#{table}\n#{g_status}"
76
+ end
77
+ [output, g_status_code]
78
+ end
79
+
80
+ # Generate table rows
81
+ def generate_rows(dependencies, opts)
82
+ rows = []
83
+ remarks = []
84
+
85
+ dependencies.select{|d| d.parents.empty?}.each do |dependency|
86
+ dep_rows, dep_remarks = display_child(dependency, remarks.size, 0, opts)
87
+
88
+ rows.push(*dep_rows)
89
+ remarks.push(*dep_remarks)
90
+ end
91
+ [rows, remarks]
92
+ end
93
+
94
+ def display_child(dependency, remarks_counter, level, opts)
95
+ row, remarks = generate_row(dependency, remarks_counter, level, opts)
96
+ remarks_counter += remarks.size
97
+ children_rows = []
98
+ children_remarks = []
99
+
100
+ dependency.dependencies.each do |child|
101
+ child_row, child_remarks = display_child(child, remarks_counter, level + 1, opts)
102
+ remarks_counter += child_remarks.size
103
+
104
+ children_rows.push(*child_row)
105
+ children_remarks.push(*child_remarks)
106
+ end
107
+
108
+
109
+ [[row, *children_rows], [remarks, children_remarks].flatten]
110
+ end
111
+
112
+ def indent_name(name, level)
113
+ level > 0 ? "#{(' ' * level) + INDENT_MARK} #{name}" : name
114
+ end
115
+
116
+ # Generate a single row and its remarks
117
+ def generate_row(dependency, remarks_counter, level, opts)
118
+ row_remarks = []
119
+
120
+ status = StatusReporter.status_to_mark(dependency.status)
121
+ chef_status = StatusReporter.status_to_mark(dependency.chef[:status])
122
+ repomanager_status = StatusReporter.status_to_mark(dependency.repomanager[:status])
123
+
124
+ name = indent_name(dependency.name.dup, level)
125
+ name = name.red if dependency.status == :err_req
126
+
127
+ row = [
128
+ name,
129
+ dependency.requirement,
130
+ dependency.chef[:version_used],
131
+ dependency.chef[:latest_version],
132
+ dependency.repomanager[:latest_metadata],
133
+ { value: status, alignment: :center },
134
+ { value: chef_status, alignment: :center },
135
+ { value: repomanager_status, alignment: :center }
136
+ ]
137
+
138
+ if opts[:remarks]
139
+ remarks_idx, remarks_counter = remarks_indices(dependency.remarks, remarks_counter)
140
+ row_remarks.push(*dependency.remarks)
141
+ row << remarks_idx
142
+ end
143
+
144
+ [row, row_remarks]
145
+ end
146
+
147
+ # Return the indices of the remarks
148
+ def remarks_indices(remarks, remarks_counter)
149
+ end_counter = remarks_counter + remarks.count
150
+
151
+ return [((remarks_counter + 1)..end_counter).to_a.join(', '), end_counter] unless remarks.empty?
152
+ return ['', end_counter]
153
+ end
154
+ end
155
+ end
156
+
157
+ # Return Kitchen's status in JSON format
158
+ class JSONReport
159
+ class << self
160
+ # @param dependencies [Array<Dependency>] list of dependencies
161
+ # @return [Array] JSON report and global status code
162
+ def generate(dependencies)
163
+ # Show Status
164
+ g_status, g_status_code = StatusReporter.global_status(dependencies)
165
+
166
+ [JSON.pretty_generate(dependencies_hash(dependencies)), g_status_code]
167
+ end
168
+
169
+ # Converts the dependency objects to JSON object
170
+ #
171
+ # @param dependencies [Array<Dependency>] list of dependencies
172
+ def dependencies_hash(dependencies)
173
+ {}.tap do |hash|
174
+ dependencies.each do |dependency|
175
+ hash[dependency.name] = dependency.to_hash
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,105 @@
1
+ #
2
+ # Copyright (c) 2013 Stefano Tortarolo <stefano.tortarolo@gmail.com>
3
+ #
4
+ # MIT License
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #
25
+
26
+ module KitchenInspector
27
+ module Inspector
28
+ # Provides functions to report single or global status
29
+ class StatusReporter
30
+ class << self
31
+ # Return a global status
32
+ def global_status(dependencies)
33
+ result = nil
34
+ dependencies.each do |dep|
35
+ status = single_status(dep)
36
+
37
+ if status
38
+ result = status
39
+ break
40
+ end
41
+ end
42
+
43
+ unless result
44
+ result = {:mark => TICK_MARK, :color => :green, :code => :up_to_date}
45
+ end
46
+
47
+ ["Status: #{result[:code]} (#{result[:mark]})".send(result[:color]), result[:code]]
48
+ end
49
+
50
+ # Return a dependency local status if different from up-to-date
51
+ def single_status(dep)
52
+ return mark_structure(:err_req) if dep.status == :err_req
53
+
54
+ return mark_structure(:err_repo) if dep.repomanager[:status] == :err_repo
55
+
56
+ return mark_structure(:warn_outofdate_repo) if dep.repomanager[:status] == :warn_outofdate_repo
57
+
58
+ return mark_structure(:warn_req) if dep.status == :warn_req
59
+
60
+ return mark_structure(:warn_mismatch_repo) if dep.repomanager[:status] == :warn_mismatch_repo
61
+
62
+ return mark_structure(:warn_chef) if dep.chef[:status] == :warn_chef
63
+
64
+ return mark_structure(:warn_notunique_repo) if dep.repomanager[:status] == :warn_notunique_repo
65
+
66
+ nil
67
+ end
68
+
69
+ # Given a status return instructions on how to draw it
70
+ def mark_structure(status)
71
+ case status
72
+ when :err_req, :err_chef
73
+ {:mark => STATUSES[status], :color => :red, :code => status }
74
+ when :err_repo
75
+ {:mark => STATUSES[status], :color => :yellow, :code => status }
76
+ when :warn_outofdate_repo
77
+ {:mark => STATUSES[status], :color => :light_red, :code => status, :style => :bold }
78
+ when :warn_req
79
+ {:mark => STATUSES[status], :color => :yellow, :code => status, :style => :bold }
80
+ when :warn_mismatch_repo
81
+ {:mark => STATUSES[status], :color => :light_red, :code => status, :style => :bold }
82
+ when :warn_chef
83
+ {:mark => STATUSES[status], :color => :blue, :code => status, :style => :bold }
84
+ when :warn_notunique_repo
85
+ {:mark => STATUSES[status], :color => :light_red, :code => status }
86
+ when :up_to_date
87
+ {:mark => STATUSES[status], :color => :green, :code => status }
88
+ else
89
+ raise StandardError, "Unknown status #{status}"
90
+ end
91
+ end
92
+
93
+ # Given a status draw a mark
94
+ def status_to_mark(status)
95
+ mark = mark_structure(status)
96
+
97
+ result = mark[:mark]
98
+ result = result.send(mark[:style]) if mark[:style]
99
+ result = result.send(mark[:color])
100
+ result
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,134 @@
1
+ #
2
+ # Copyright (c) 2013 Stefano Tortarolo <stefano.tortarolo@gmail.com>
3
+ #
4
+ # MIT License
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #
25
+
26
+ module KitchenInspector
27
+ module Inspector
28
+ class RepositoryInspector
29
+ include Utils
30
+
31
+ attr_accessor :manager
32
+
33
+ def initialize(config)
34
+ begin
35
+ require "kitchen-inspector/inspector/repository_managers/#{config[:type].downcase}"
36
+ manager_cls = "KitchenInspector::Inspector::#{config[:type]}Manager".constantize
37
+ rescue LoadError, NameError => e
38
+ raise RepositoryManagerError, "Repository Manager '#{config[:type]}' not supported"
39
+ end
40
+
41
+ @manager = manager_cls.new config
42
+ end
43
+
44
+ # Given a dependency and a version provided by Chef Server,
45
+ # analyze that dependency and its transitive dependencies (if recursive)
46
+ #
47
+ # It also detects whether multiple projects exist with the same name
48
+ # e.g., different users or forks
49
+ #
50
+ # @return [Array] containing pairs [dependency, repository_information]
51
+ def investigate(dependency, version_used, recursive)
52
+ repo_dependencies = []
53
+ projects = @manager.projects_by_name(dependency.name)
54
+
55
+ if projects.empty?
56
+ repo_dependencies << [dependency, {}]
57
+ else
58
+ projects.each do |project|
59
+ repo_info = analyze_repository(project)
60
+ repo_info[:not_unique] = projects.size > 1
61
+
62
+ # Inherit only shallow information from dependency
63
+ prj_dependency = Dependency.new(dependency.name, dependency.requirement)
64
+ prj_dependency.parents = dependency.parents
65
+
66
+ prj_dependency.parents.each do |parent|
67
+ parent.dependencies << prj_dependency
68
+ end
69
+
70
+ repo_dependencies << [prj_dependency, repo_info]
71
+
72
+ # Analyze its dependencies based on Repository Manager
73
+ if recursive && repo_info[:tags]
74
+ reference_version = get_reference_version(version_used, repo_info)
75
+
76
+ children = @manager.project_dependencies(project,
77
+ repo_info[:tags][reference_version]).collect do |dep|
78
+ dep.parents << prj_dependency
79
+ investigate(dep, version_used, recursive)
80
+ end.flatten!(1)
81
+
82
+ repo_dependencies.push(*children)
83
+ end
84
+ end
85
+ end
86
+
87
+ repo_dependencies
88
+ end
89
+
90
+ # Return the reference version to be used for recursive analysis
91
+ #
92
+ # It's the version used, if present. The latest available tag on the Repository
93
+ # Manager otherwise.
94
+ def get_reference_version(version_used, repo_info)
95
+ reference_version = nil
96
+ reference_version = version_used if version_used && repo_info[:tags].include?(version_used)
97
+ reference_version = repo_info[:latest_tag].to_s unless reference_version
98
+ reference_version
99
+ end
100
+
101
+ # Check whether tag and metadata's version match
102
+ def consistent_version?(info)
103
+ !(info[:latest_tag] &&
104
+ info[:latest_metadata] &&
105
+ info[:latest_tag] != info[:latest_metadata])
106
+ end
107
+
108
+ # Return an url pointing to the diff between startRev and endRev
109
+ def get_changelog(repo_info, startRev, endRev)
110
+ return unless repo_info[:tags]
111
+
112
+ url = @manager.changelog(repo_info[:source_url],
113
+ repo_info[:tags][startRev],
114
+ repo_info[:tags][endRev])
115
+ "Changelog: #{url}" if url
116
+ end
117
+
118
+ # Retrieve project info from Repository Manager
119
+ def analyze_repository(project)
120
+ tags = @manager.tags(project)
121
+ latest_tag = get_latest_version(tags.keys)
122
+
123
+ latest_metadata = @manager.project_metadata_version(project, tags[latest_tag.to_s])
124
+ latest_metadata = Solve::Version.new(latest_metadata) if latest_metadata
125
+
126
+ {:tags => tags,
127
+ :latest_tag => latest_tag,
128
+ :latest_metadata => latest_metadata,
129
+ :source_url => @manager.source_url(project)
130
+ }
131
+ end
132
+ end
133
+ end
134
+ end