knife-inspect 0.6.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/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ !.gitkeep
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in health_inspector.gemspec
4
+ gemspec
data/HISTORY.md ADDED
@@ -0,0 +1,88 @@
1
+ ## 0.6.0 ( 2012-11-1 )
2
+
3
+ * Add knife plugins for all existing functionality:
4
+ - knife inspect
5
+ - knife cookbook inspect [COOKBOOK]
6
+ - knife data bag inspect [BAG] [ITEM]
7
+ - knife environment inspect [ENVIRONMENT]
8
+ - knife role inspect [ROLE]
9
+
10
+ * Lost support for quiet-sucess option (We can add that back, or make a quiet
11
+ options that just returns exit status).
12
+
13
+ ## 0.5.2 ( 2012-10-19 )
14
+
15
+ * Make loading of Chef Config a little more robust.
16
+
17
+ ## 0.5.1 ( 2012-10-15 )
18
+
19
+ * Ignore _default environment if it only exists on the server.
20
+
21
+ ## 0.5.0 ( 2012-10-14 )
22
+
23
+ * Switch to RSpec
24
+ * Add some test coverage (still needs much more).
25
+ * Add option to suppress terminal output on successful checks.
26
+ * Add option to not use ansi color output.
27
+ * Make cookbook version comparison use Chef's native version class.
28
+
29
+ ## 0.4.1 ( 2012-09-28 )
30
+
31
+ * Fix a bug I created in last release when passing no component.
32
+
33
+ ## 0.4.0 ( 2012-09-28 )
34
+
35
+ * Make `inspect` the default task
36
+ * Add ability to specify individual components:
37
+
38
+ health_inspector inspect cookbooks
39
+
40
+ ## 0.3.1 ( 2012-09-27 )
41
+
42
+ * Stop shelling out for knife commands, use Chef API directly for everything.
43
+
44
+ ## 0.3.0 ( 2012-09-26 )
45
+
46
+ * Add new check for cookbooks: checksum comparison for each file.
47
+
48
+ ## 0.2.1 ( 2012-09-26 )
49
+
50
+ * Fix 1.8.7 incompatibility introduced in last release (String#prepend).
51
+
52
+ ## 0.2.0 ( 2012-09-25 )
53
+
54
+ * Add a better diff output.
55
+ * Add diff output to data bag items.
56
+ * Switch to yajl-ruby to fix JSON parsing issues (Chef uses this also).
57
+
58
+ ## 0.1.0 ( 2012-09-24 )
59
+
60
+ * Bump Chef dependency version up to 10.14
61
+ * Add support for JSON environments.
62
+ * Add support for JSON roles.
63
+ * Display the diff between JSONs when JSON data doesn't match.
64
+
65
+ ## 0.0.6 ( 2012-05-23 )
66
+
67
+ * Depend on Chef 0.10.8, since it depends on a later version of the json gem.
68
+ An earlier version of the json gem was throwing incorrect parse errors.
69
+
70
+ ## 0.0.5 ( 2012-04-13 )
71
+
72
+ * Fix #2, exception when a data bag item json file doesn't exist locally.
73
+
74
+ ## 0.0.4 ( 2012-04-09 )
75
+
76
+ * Add checks for data bags, data bag items, environments, and roles.
77
+
78
+ ## 0.0.3 ( 2012-03-27 )
79
+
80
+ * Read cookbook paths from knife config file instead of hardcoding /cookbooks.
81
+
82
+ ## 0.0.2 ( 2012-03-27 )
83
+
84
+ * Make sure we iterate over actual cookbooks in cookbooks folder.
85
+
86
+ ## 0.0.1 ( 2012-03-27 )
87
+
88
+ * Initial release.
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2012 Ben Marini
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ [![Build Status](https://secure.travis-ci.org/bmarini/health_inspector.png)](http://travis-ci.org/bmarini/health_inspector)
2
+
3
+ ## Summary
4
+
5
+ `health_inspector` is a knife plugin that inspects your chef repo as it
6
+ compares to what is on your chef server. You can inspect your entire repo,
7
+ or individual components.
8
+
9
+ ## Usage
10
+
11
+ $ gem install health_inspector
12
+ $ cd [chef repo]
13
+
14
+ ## Knife Commands
15
+
16
+ knife inspect
17
+
18
+ knife cookbook inspect
19
+ knife cookbook inspect [COOKBOOK]
20
+
21
+ knife data bag inspect
22
+ knife data bag inspect [BAG]
23
+ knife data bag inspect [BAG] [ITEM]
24
+
25
+ knife environment inspect
26
+ knife environment inspect [ENVIRONMENT]
27
+
28
+ knife role inspect
29
+ knife role inspect [ROLE]
30
+
31
+ ## What it does
32
+
33
+ So far it checks if...
34
+
35
+ * your cookbooks are in sync
36
+ * you have uncommitted changes in a cookbook (assuming your cookbooks are in
37
+ their own git repos)
38
+ * you have commits in a cookbook that haven't been pushed to your remote
39
+ (assuming your cookbooks are in their own git repos)
40
+ * your data bags are in sync
41
+ * your data bag items are in sync
42
+ * your environments are in sync
43
+ * your roles are in sync
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new do |t|
5
+ t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
6
+ t.pattern = 'spec/**/*_spec.rb'
7
+ end
8
+
9
+ task :default => :spec
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "health_inspector/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "knife-inspect"
7
+ s.version = HealthInspector::VERSION
8
+ s.authors = ["Ben Marini"]
9
+ s.email = ["bmarini@gmail.com"]
10
+ s.homepage = "https://github.com/bmarini/knife-inspect"
11
+ s.summary = %q{Inspect your chef repo as is compares to what is on your chef server}
12
+ s.description = %q{Inspect your chef repo as is compares to what is on your chef server}
13
+
14
+ s.rubyforge_project = "knife-inspect"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "rake"
22
+ s.add_development_dependency "rspec"
23
+
24
+ s.add_runtime_dependency "thor"
25
+ s.add_runtime_dependency "chef", "~> 10.14"
26
+ s.add_runtime_dependency "yajl-ruby"
27
+ end
@@ -0,0 +1,36 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ class CookbookInspect < Knife
6
+
7
+ deps do
8
+ require 'health_inspector'
9
+ require 'chef/json_compat'
10
+ require 'uri'
11
+ require 'chef/cookbook_version'
12
+ end
13
+
14
+ banner "knife cookbook inspect [COOKBOOK] (options)"
15
+
16
+ def run
17
+ case @name_args.length
18
+ when 1 # We are inspecting a cookbook
19
+ cookbook_name = @name_args[0]
20
+ # TODO: Support environments
21
+ # env = config[:environment]
22
+ # api_endpoint = env ? "environments/#{env}/cookbooks/#{cookbook_name}" : "cookbooks/#{cookbook_name}"
23
+
24
+ validator = HealthInspector::Checklists::Cookbooks.new(self)
25
+ validator.validate_item( validator.load_item(cookbook_name) )
26
+ when 0 # We are inspecting all the cookbooks
27
+ HealthInspector::Checklists::Cookbooks.run(self)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+
36
+
@@ -0,0 +1,35 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ class DataBagInspect < Knife
6
+
7
+ deps do
8
+ require 'health_inspector'
9
+ end
10
+
11
+ banner "knife data bag inspect [BAG] [ITEM] (options)"
12
+
13
+ def run
14
+ case @name_args.length
15
+ when 2 # We are inspecting a data bag item
16
+ bag_name = @name_args[0]
17
+ item_name = @name_args[1]
18
+
19
+ validator = HealthInspector::Checklists::DataBagItems.new(self)
20
+ validator.validate_item( validator.load_item("#{bag_name}/#{item_name}") )
21
+
22
+ when 1 # We are inspecting a data bag
23
+ bag_name = @name_args[0]
24
+
25
+ validator = HealthInspector::Checklists::DataBags.new(self)
26
+ validator.validate_item( validator.load_item(bag_name) )
27
+
28
+ when 0 # We are inspecting all the data bags
29
+ HealthInspector::Checklists::DataBags.run(self)
30
+ HealthInspector::Checklists::DataBagItems.run(self)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ class EnvironmentInspect < Knife
6
+
7
+ deps do
8
+ require 'health_inspector'
9
+ end
10
+
11
+ banner "knife environment inspect [ENVIRONMENT] (options)"
12
+
13
+ def run
14
+ case @name_args.length
15
+ when 1 # We are inspecting a environment
16
+ environment_name = @name_args[0]
17
+ validator = HealthInspector::Checklists::Environments.new(self)
18
+ validator.validate_item( validator.load_item(environment_name) )
19
+ when 0 # We are inspecting all the environments
20
+ HealthInspector::Checklists::Environments.run(self)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ class Inspect < Knife
6
+
7
+ deps do
8
+ require "health_inspector"
9
+ end
10
+
11
+ banner "knife inspect"
12
+
13
+ def run
14
+ %w[ Cookbooks DataBags DataBagItems Environments Roles ].each do |checklist|
15
+ HealthInspector::Checklists.const_get(checklist).run(self)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ class RoleInspect < Knife
6
+
7
+ deps do
8
+ require 'health_inspector'
9
+ end
10
+
11
+ banner "knife role inspect [ROLE] (options)"
12
+
13
+ def run
14
+ case @name_args.length
15
+ when 1 # We are inspecting a role
16
+ role_name = @name_args[0]
17
+ validator = HealthInspector::Checklists::Roles.new(self)
18
+ validator.validate_item( validator.load_item(role_name) )
19
+ when 0 # We are inspecting all the roles
20
+ HealthInspector::Checklists::Roles.run(self)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: UTF-8
2
+ require "health_inspector/version"
3
+ require "health_inspector/color"
4
+ require "health_inspector/context"
5
+ require "health_inspector/pairing"
6
+ require "health_inspector/inspector"
7
+ require "health_inspector/checklists/base"
8
+ require "health_inspector/checklists/cookbooks"
9
+ require "health_inspector/checklists/data_bags"
10
+ require "health_inspector/checklists/data_bag_items"
11
+ require "health_inspector/checklists/environments"
12
+ require "health_inspector/checklists/roles"
13
+ require 'chef/rest'
14
+ require 'chef/checksum_cache'
15
+ require 'chef/version'
16
+
17
+ module HealthInspector
18
+ end
@@ -0,0 +1,136 @@
1
+ # encoding: UTF-8
2
+ require "pathname"
3
+
4
+ module HealthInspector
5
+ module Checklists
6
+ class Base
7
+ include Color
8
+
9
+ class << self
10
+ attr_reader :title
11
+
12
+ def title(val=nil)
13
+ val.nil? ? @title : @title = val
14
+ end
15
+ end
16
+
17
+ def self.run(knife)
18
+ new(knife).run
19
+ end
20
+
21
+ def initialize(knife)
22
+ @context = Context.new(knife)
23
+ end
24
+
25
+ def ui
26
+ @context.knife.ui
27
+ end
28
+
29
+ def all_item_names
30
+ ( server_items + local_items ).uniq.sort
31
+ end
32
+
33
+ # Subclasses should collect all items from the server and the local repo,
34
+ # and for each item pair, yield an object that contains a reference to
35
+ # the server item, and the local repo item. A reference can be nil if it does
36
+ # not exist in one of the locations.
37
+ def each_item
38
+ raise NotImplementedError, "You must implement this method in a subclass"
39
+ end
40
+
41
+ def run
42
+ banner "Inspecting #{self.class.title}"
43
+
44
+ each_item do |item|
45
+ validate_item(item)
46
+ end
47
+ end
48
+
49
+ def validate_item(item)
50
+ item.validate
51
+ failures = item.errors
52
+
53
+ if failures.empty?
54
+ print_success(item.name) # unless @context.quiet_success
55
+ else
56
+ print_failures(item.name, failures)
57
+ end
58
+ end
59
+
60
+ def banner(message)
61
+ ui.msg ""
62
+ ui.msg message
63
+ ui.msg "-" * 80
64
+ end
65
+
66
+ def print_success(subject)
67
+ ui.msg color('bright pass', "✓") + " #{subject}"
68
+ end
69
+
70
+ def print_failures(subject, failures)
71
+ ui.msg color('bright fail', "- #{subject}")
72
+
73
+ failures.each do |message|
74
+ if message.kind_of? Hash
75
+ puts color('bright yellow'," has the following values mismatched on the server and repo\n")
76
+ print_failures_from_hash(message)
77
+ else
78
+ puts color('bright yellow', " #{message}")
79
+ end
80
+ end
81
+ end
82
+
83
+ def print_failures_from_hash(message, depth=2)
84
+ message.keys.each do |key|
85
+ print_key(key,depth)
86
+
87
+ if message[key].include? "server"
88
+ print_value_diff(message[key],depth)
89
+ message[key].delete_if { |k,v| k == "server" || "local" }
90
+ print_failures_from_hash(message[key], depth + 1) unless message[key].empty?
91
+ else
92
+ print_failures_from_hash(message[key], depth + 1)
93
+ end
94
+ end
95
+ end
96
+
97
+ def print_key(key, depth)
98
+ ui.msg indent( color('bright yellow',"#{key} : "), depth )
99
+ end
100
+
101
+ def print_value_diff(value, depth)
102
+ print indent( color('bright fail',"server value = "), depth + 1 )
103
+ print value["server"]
104
+ print "\n"
105
+ print indent( color('bright fail',"local value = "), depth + 1 )
106
+ print value["local"]
107
+ print "\n\n"
108
+ end
109
+
110
+ def load_ruby_or_json_from_local(chef_class, folder, name)
111
+ path_template = "#{@context.repo_path}/#{folder}/#{name}.%s"
112
+ ruby_pathname = Pathname.new(path_template % "rb")
113
+ json_pathname = Pathname.new(path_template % "json")
114
+ js_pathname = Pathname.new(path_template % "js")
115
+
116
+ if ruby_pathname.exist?
117
+ instance = chef_class.new
118
+ instance.from_file(ruby_pathname.to_s)
119
+ elsif json_pathname.exist?
120
+ instance = chef_class.json_create( Yajl::Parser.parse( json_pathname.read ) )
121
+ elsif js_pathname.exist?
122
+ instance = chef_class.json_create( Yajl::Parser.parse( js_pathname.read ) )
123
+ end
124
+
125
+ instance ? instance.to_hash : nil
126
+ rescue IOError
127
+ nil
128
+ end
129
+
130
+ def indent(string, depth)
131
+ (' ' * 2 * depth) + string
132
+ end
133
+
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,134 @@
1
+ module HealthInspector
2
+ module Checklists
3
+
4
+ class Cookbook < Pairing
5
+ include ExistenceValidations
6
+
7
+ def validate_versions
8
+ if versions_exist? && !versions_match?
9
+ errors.add "chef server has #{server} but local version is #{local}"
10
+ end
11
+ end
12
+
13
+ def validate_uncommited_changes
14
+ if git_repo?
15
+ result = `cd #{cookbook_path} && git status -s`
16
+
17
+ unless result.empty?
18
+ errors.add "Uncommitted changes:\n#{result.chomp}"
19
+ end
20
+ end
21
+ end
22
+
23
+ def validate_commits_not_pushed_to_remote
24
+ if git_repo?
25
+ result = `cd #{cookbook_path} && git status`
26
+
27
+ if result =~ /Your branch is ahead of (.+)/
28
+ errors.add "ahead of #{$1}"
29
+ end
30
+ end
31
+ end
32
+
33
+ # TODO: Check files that exist locally but not in manifest on server
34
+ def validate_changes_on_the_server_not_in_the_repo
35
+ if versions_exist? && versions_match?
36
+
37
+ begin
38
+ cookbook = context.rest.get_rest("/cookbooks/#{name}/#{local}")
39
+ messages = []
40
+
41
+ Chef::CookbookVersion::COOKBOOK_SEGMENTS.each do |segment|
42
+ cookbook.manifest[segment].each do |manifest_record|
43
+ path = cookbook_path.join("#{manifest_record["path"]}")
44
+
45
+ if path.exist?
46
+ checksum = checksum_cookbook_file(path)
47
+ messages << "#{manifest_record['path']}" if checksum != manifest_record['checksum']
48
+ else
49
+ messages << "#{manifest_record['path']} does not exist in the repo"
50
+ end
51
+ end
52
+ end
53
+
54
+ unless messages.empty?
55
+ message = "has a checksum mismatch between server and repo in\n"
56
+ message << messages.map { |f| " #{f}" }.join("\n")
57
+ errors.add message
58
+ end
59
+
60
+ rescue Net::HTTPServerException => e
61
+ errors.add "Could not find cookbook #{name} on the server"
62
+ end
63
+
64
+ end
65
+ end
66
+
67
+ def versions_exist?
68
+ local && server
69
+ end
70
+
71
+ def versions_match?
72
+ local == server
73
+ end
74
+
75
+ def git_repo?
76
+ cookbook_path && File.exist?("#{cookbook_path}/.git")
77
+ end
78
+
79
+ def cookbook_path
80
+ path = context.cookbook_path.find { |f| File.exist?("#{f}/#{name}") }
81
+ path ? Pathname.new(path).join(name) : nil
82
+ end
83
+
84
+ def checksum_cookbook_file(filepath)
85
+ Chef::CookbookVersion.checksum_cookbook_file(filepath)
86
+ end
87
+
88
+ end
89
+
90
+ class Cookbooks < Base
91
+
92
+ title "cookbooks"
93
+
94
+ def each_item
95
+ all_cookbook_names = ( server_cookbooks.keys + local_cookbooks.keys ).uniq.sort
96
+
97
+ all_cookbook_names.each do |name|
98
+ yield load_item(name)
99
+ end
100
+ end
101
+
102
+ def load_item(name)
103
+ Cookbook.new(@context,
104
+ :name => name,
105
+ :server => server_cookbooks[name],
106
+ :local => local_cookbooks[name]
107
+ )
108
+ end
109
+
110
+ def server_cookbooks
111
+ @context.rest.get_rest("/cookbooks").inject({}) do |hsh, (name,version)|
112
+ hsh[name] = Chef::Version.new(version["versions"].first["version"])
113
+ hsh
114
+ end
115
+ end
116
+
117
+ def local_cookbooks
118
+ @context.cookbook_path.
119
+ map { |path| Dir["#{path}/*"] }.
120
+ flatten.
121
+ select { |path| File.exists?("#{path}/metadata.rb") }.
122
+ inject({}) do |hsh, path|
123
+
124
+ name = File.basename(path)
125
+ version = (`grep '^version' #{path}/metadata.rb`).split.last[1...-1]
126
+
127
+ hsh[name] = Chef::Version.new(version)
128
+ hsh
129
+ end
130
+ end
131
+
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,60 @@
1
+ require "chef/data_bag"
2
+
3
+ module HealthInspector
4
+ module Checklists
5
+ class DataBagItem < Pairing
6
+ include ExistenceValidations
7
+ include JsonValidations
8
+ end
9
+
10
+ class DataBagItems < Base
11
+ title "data bag items"
12
+
13
+ def each_item
14
+ all_item_names.each do |name|
15
+ yield load_item(name)
16
+ end
17
+ end
18
+
19
+ def load_item(name)
20
+ DataBagItem.new(@context,
21
+ :name => name,
22
+ :server => load_item_from_server(name),
23
+ :local => load_item_from_local(name)
24
+ )
25
+ end
26
+
27
+ def server_items
28
+ @server_items ||= Chef::DataBag.list.keys.map do |bag_name|
29
+ [ bag_name, Chef::DataBag.load(bag_name) ]
30
+ end.inject([]) do |arr, (bag_name, data_bag)|
31
+ arr += data_bag.keys.map { |item_name| "#{bag_name}/#{item_name}"}
32
+ end
33
+ end
34
+
35
+ def local_items
36
+ entries = nil
37
+
38
+ Dir.chdir("#{@context.repo_path}/data_bags") do
39
+ entries = Dir["**/*.json"].map { |entry| entry.gsub('.json', '') }
40
+ end
41
+
42
+ return entries
43
+ end
44
+
45
+ def load_item_from_server(name)
46
+ bag_name, item_name = name.split("/")
47
+ Chef::DataBagItem.load(bag_name, item_name).raw_data
48
+ rescue
49
+ nil
50
+ end
51
+
52
+ def load_item_from_local(name)
53
+ Yajl::Parser.parse( File.read("#{@context.repo_path}/data_bags/#{name}.json") )
54
+ rescue IOError, Errno::ENOENT
55
+ nil
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,38 @@
1
+ require "chef/data_bag"
2
+
3
+ module HealthInspector
4
+ module Checklists
5
+ class DataBag < Pairing
6
+ include ExistenceValidations
7
+ end
8
+
9
+ class DataBags < Base
10
+ title "data bags"
11
+
12
+ def each_item
13
+ all_item_names.each do |name|
14
+ yield load_item(name)
15
+ end
16
+ end
17
+
18
+ def load_item(name)
19
+ DataBag.new(@context,
20
+ :name => name,
21
+ :server => server_items.include?(name),
22
+ :local => local_items.include?(name)
23
+ )
24
+ end
25
+
26
+ def server_items
27
+ @server_items ||= Chef::DataBag.list.keys
28
+ end
29
+
30
+ def local_items
31
+ @local_items ||= Dir["#{@context.repo_path}/data_bags/*"].entries.
32
+ select { |e| File.directory?(e) }.
33
+ map { |e| File.basename(e) }
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ require "chef/environment"
2
+
3
+ module HealthInspector
4
+ module Checklists
5
+ class Environment < Pairing
6
+ include ExistenceValidations
7
+ include JsonValidations
8
+
9
+ # Override to ignore _default environment if it is missing locally
10
+ def validate_local_copy_exists
11
+ super unless name == '_default'
12
+ end
13
+ end
14
+
15
+ class Environments < Base
16
+ title "environments"
17
+
18
+ def each_item
19
+ all_item_names.each do |name|
20
+ yield load_item(name)
21
+ end
22
+ end
23
+
24
+ def load_item(name)
25
+ Environment.new(@context,
26
+ :name => name,
27
+ :server => load_item_from_server(name),
28
+ :local => load_item_from_local(name)
29
+ )
30
+ end
31
+
32
+ def server_items
33
+ @server_items ||= Chef::Environment.list.keys
34
+ end
35
+
36
+ def local_items
37
+ Dir.chdir("#{@context.repo_path}/environments") do
38
+ Dir["*.{rb,json,js}"].map { |e| e.gsub(/\.(rb|json|js)/,"") }
39
+ end
40
+ end
41
+
42
+ def load_item_from_server(name)
43
+ env = Chef::Environment.load(name)
44
+ env.to_hash
45
+ rescue
46
+ nil
47
+ end
48
+
49
+ def load_item_from_local(name)
50
+ load_ruby_or_json_from_local(Chef::Environment, "environments", name)
51
+ end
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,51 @@
1
+ require "chef/role"
2
+ require 'yajl'
3
+
4
+ module HealthInspector
5
+ module Checklists
6
+ class Role < Pairing
7
+ include ExistenceValidations
8
+ include JsonValidations
9
+ end
10
+
11
+ class Roles < Base
12
+ title "roles"
13
+
14
+ def each_item
15
+ all_item_names.each do |name|
16
+ yield load_item(name)
17
+ end
18
+ end
19
+
20
+ def load_item(name)
21
+ Role.new(@context,
22
+ :name => name,
23
+ :server => load_item_from_server(name),
24
+ :local => load_item_from_local(name)
25
+ )
26
+ end
27
+
28
+ def server_items
29
+ @server_items ||= Chef::Role.list.keys
30
+ end
31
+
32
+ def local_items
33
+ Dir.chdir("#{@context.repo_path}/roles") do
34
+ Dir["*.{rb,json,js}"].map { |e| e.gsub(/\.(rb|json|js)/, '') }
35
+ end
36
+ end
37
+
38
+ def load_item_from_server(name)
39
+ role = Chef::Role.load(name)
40
+ role.to_hash
41
+ rescue
42
+ nil
43
+ end
44
+
45
+ def load_item_from_local(name)
46
+ load_ruby_or_json_from_local(Chef::Role, "roles", name)
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ module HealthInspector
2
+ module Color
3
+
4
+ # TODO: Use a highline color scheme here instead
5
+ def color(type, str)
6
+ colors = {
7
+ 'pass' => [:green],# 90,
8
+ 'fail' => [:red],# 31,
9
+ 'bright pass' => [:bold, :green],# 92,
10
+ 'bright fail' => [:bold, :red],# 91,
11
+ 'bright yellow' => [:bold, :yellow],# 93,
12
+ 'pending' => [:yellow],# 36,
13
+ 'suite' => [],# 0,
14
+ 'error title' => [],# 0,
15
+ 'error message' => [:red],# 31,
16
+ 'error stack' => [:green],# 90,
17
+ 'checkmark' => [:green],# 32,
18
+ 'fast' => [:green],# 90,
19
+ 'medium' => [:green],# 33,
20
+ 'slow' => [:red],# 31,
21
+ 'green' => [:green],# 32,
22
+ 'light' => [:green],# 90,
23
+ 'diff gutter' => [:green],# 90,
24
+ 'diff added' => [:green],# 42,
25
+ 'diff removed' => [:red]# 41
26
+ }
27
+
28
+ @context.knife.ui.color( str, *colors[type] )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ require "chef/config"
2
+
3
+ module HealthInspector
4
+ class Context
5
+ attr_accessor :knife
6
+
7
+ def initialize(knife)
8
+ @knife = knife
9
+ end
10
+
11
+ def cookbook_path
12
+ Array( config.cookbook_path )
13
+ end
14
+
15
+ def config
16
+ Chef::Config
17
+ end
18
+
19
+ def rest
20
+ @knife.rest
21
+ end
22
+
23
+ def repo_path
24
+ ENV['PWD']
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ module HealthInspector
2
+ class Inspector
3
+ def self.inspect(checklists, options)
4
+ new(options).inspect( checklists )
5
+ end
6
+
7
+ def initialize(options)
8
+ @context = Context.new( options[:repopath], options[:configpath] )
9
+ @context.quiet_success = options[:'quiet-success']
10
+ @context.no_color = options[:'no-color']
11
+ @context.configure
12
+ end
13
+
14
+ def inspect(checklists)
15
+ checklists.each do |checklist|
16
+ Checklists.const_get(checklist).run(@context) if Checklists.const_defined?(checklist)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,73 @@
1
+ module HealthInspector
2
+ class Errors
3
+ include Enumerable
4
+
5
+ def initialize
6
+ @errors = []
7
+ end
8
+
9
+ def add(message)
10
+ @errors << message
11
+ end
12
+
13
+ def each
14
+ @errors.each { |e| yield(e) }
15
+ end
16
+
17
+ def empty?
18
+ @errors.empty?
19
+ end
20
+ end
21
+
22
+ class Pairing
23
+ attr_accessor :name, :local, :server
24
+ attr_reader :context, :errors
25
+
26
+ def initialize(context, opts={})
27
+ @context = context
28
+ @name = opts[:name]
29
+ @local = opts[:local]
30
+ @server = opts[:server]
31
+
32
+ @validations = []
33
+ @errors = Errors.new
34
+ end
35
+
36
+ def validate
37
+ self.methods.grep(/^validate_/).each { |meth| send(meth) }
38
+ end
39
+
40
+ def hash_diff(original, other)
41
+ (original.keys + other.keys).uniq.inject({}) do |memo, key|
42
+ unless original[key] == other[key]
43
+ if original[key].kind_of?(Hash) && other[key].kind_of?(Hash)
44
+ memo[key] = hash_diff(original[key], other[key])
45
+ else
46
+ memo[key] = {"server" => original[key],"local" => other[key]}
47
+ end
48
+ end
49
+ memo
50
+ end
51
+ end
52
+ end
53
+
54
+ # Mixins for common validations across pairings
55
+ module ExistenceValidations
56
+ def validate_local_copy_exists
57
+ errors.add "exists on server but not locally" if local.nil?
58
+ end
59
+
60
+ def validate_server_copy_exists
61
+ errors.add "exists locally but not on server" if server.nil?
62
+ end
63
+ end
64
+
65
+ module JsonValidations
66
+ def validate_items_are_the_same
67
+ if server && local
68
+ differences = hash_diff(server, local)
69
+ errors.add differences unless differences.empty?
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,3 @@
1
+ module HealthInspector
2
+ VERSION = "0.6.0"
3
+ end
File without changes
@@ -0,0 +1,7 @@
1
+ chef_repo_dir = File.expand_path( "../", File.dirname(__FILE__) )
2
+
3
+ client_key "#{chef_repo_dir}/.chef/client.pem"
4
+ log_level :info
5
+ log_location STDOUT
6
+ cache_type "BasicFile"
7
+ cookbook_path [ "#{chef_repo_dir}/cookbooks" ]
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe HealthInspector::Checklists::Cookbook do
4
+ let(:pairing) { described_class.new(health_inspector_context, :name => "dummy") }
5
+
6
+ it "should detect if an item does not exist locally" do
7
+ pairing.server = "0.0.1"
8
+ pairing.local = nil
9
+ pairing.validate
10
+
11
+ pairing.errors.should_not be_empty
12
+ pairing.errors.first.should == "exists on server but not locally"
13
+ end
14
+
15
+ it "should detect if an item does not exist on server" do
16
+ pairing.server = nil
17
+ pairing.local = "0.0.1"
18
+ pairing.validate
19
+
20
+ pairing.errors.should_not be_empty
21
+ pairing.errors.first.should == "exists locally but not on server"
22
+ end
23
+
24
+ it "should detect if an item is different" do
25
+ pairing.server = "0.0.1"
26
+ pairing.local = "0.0.2"
27
+ pairing.validate
28
+
29
+ pairing.errors.should_not be_empty
30
+ pairing.errors.first.should == "chef server has 0.0.1 but local version is 0.0.2"
31
+ end
32
+
33
+ it "should detect if an item is the same" do
34
+ pairing.should_receive(:validate_changes_on_the_server_not_in_the_repo)
35
+ pairing.server = "0.0.1"
36
+ pairing.local = "0.0.1"
37
+ pairing.validate
38
+
39
+ pairing.errors.should be_empty
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+
3
+ describe HealthInspector::Checklists::DataBagItem do
4
+ it_behaves_like "a chef model"
5
+ it_behaves_like "a chef model that can be respresented in json"
6
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe HealthInspector::Checklists::DataBag do
4
+ it_behaves_like "a chef model"
5
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe HealthInspector::Checklists::Environment do
4
+ let(:pairing) { described_class.new(health_inspector_context) }
5
+
6
+ it_behaves_like "a chef model"
7
+ it_behaves_like "a chef model that can be respresented in json"
8
+
9
+ it "should ignore _default environment if it only exists on server" do
10
+ pairing.name = "_default"
11
+ pairing.server = {}
12
+ pairing.local = nil
13
+ pairing.validate
14
+
15
+ pairing.errors.should be_empty
16
+ end
17
+
18
+ end
data/spec/role_spec.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+
3
+ describe HealthInspector::Checklists::Role do
4
+ it_behaves_like "a chef model"
5
+ it_behaves_like "a chef model that can be respresented in json"
6
+ end
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'health_inspector'
4
+
5
+ module HealthInspector::SpecHelpers
6
+ def health_inspector_context
7
+ @health_inspector_context ||= HealthInspector::Context.new(nil)
8
+ end
9
+ end
10
+
11
+ RSpec.configure do |c|
12
+ c.include HealthInspector::SpecHelpers
13
+ end
14
+
15
+ shared_examples "a chef model" do
16
+ let(:pairing) { described_class.new(health_inspector_context, :name => "dummy") }
17
+
18
+ it "should detect if an item does not exist locally" do
19
+ pairing.server = {}
20
+ pairing.local = nil
21
+ pairing.validate
22
+
23
+ pairing.errors.should_not be_empty
24
+ pairing.errors.first.should == "exists on server but not locally"
25
+ end
26
+
27
+ it "should detect if an item does not exist on server" do
28
+ pairing.server = nil
29
+ pairing.local = {}
30
+ pairing.validate
31
+
32
+ pairing.errors.should_not be_empty
33
+ pairing.errors.first.should == "exists locally but not on server"
34
+ end
35
+ end
36
+
37
+ shared_examples "a chef model that can be respresented in json" do
38
+ let(:pairing) { described_class.new(health_inspector_context, :name => "dummy") }
39
+
40
+ it "should detect if an item is different" do
41
+ pairing.server = {"foo" => "bar"}
42
+ pairing.local = {"foo" => "baz"}
43
+ pairing.validate
44
+
45
+ pairing.errors.should_not be_empty
46
+ pairing.errors.first.should == {"foo"=>{"server"=>"bar", "local"=>"baz"}}
47
+ end
48
+
49
+ it "should detect if an item is the same" do
50
+ pairing.server = {"foo" => "bar"}
51
+ pairing.local = {"foo" => "bar"}
52
+ pairing.validate
53
+
54
+ pairing.errors.should be_empty
55
+ end
56
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: knife-inspect
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ben Marini
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70274490577920 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70274490577920
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70274490573900 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70274490573900
36
+ - !ruby/object:Gem::Dependency
37
+ name: thor
38
+ requirement: &70274490581800 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70274490581800
47
+ - !ruby/object:Gem::Dependency
48
+ name: chef
49
+ requirement: &70274490608360 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '10.14'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70274490608360
58
+ - !ruby/object:Gem::Dependency
59
+ name: yajl-ruby
60
+ requirement: &70274490625560 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70274490625560
69
+ description: Inspect your chef repo as is compares to what is on your chef server
70
+ email:
71
+ - bmarini@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - Gemfile
78
+ - HISTORY.md
79
+ - MIT-LICENSE
80
+ - README.md
81
+ - Rakefile
82
+ - knife-inspect.gemspec
83
+ - lib/chef/knife/cookbook_inspect.rb
84
+ - lib/chef/knife/data_bag_inspect.rb
85
+ - lib/chef/knife/environment_inspect.rb
86
+ - lib/chef/knife/inspect.rb
87
+ - lib/chef/knife/role_inspect.rb
88
+ - lib/health_inspector.rb
89
+ - lib/health_inspector/checklists/base.rb
90
+ - lib/health_inspector/checklists/cookbooks.rb
91
+ - lib/health_inspector/checklists/data_bag_items.rb
92
+ - lib/health_inspector/checklists/data_bags.rb
93
+ - lib/health_inspector/checklists/environments.rb
94
+ - lib/health_inspector/checklists/roles.rb
95
+ - lib/health_inspector/color.rb
96
+ - lib/health_inspector/context.rb
97
+ - lib/health_inspector/inspector.rb
98
+ - lib/health_inspector/pairing.rb
99
+ - lib/health_inspector/version.rb
100
+ - spec/chef-repo/.chef/client.pem
101
+ - spec/chef-repo/.chef/knife.rb
102
+ - spec/cookbook_spec.rb
103
+ - spec/data_bag_item_spec.rb
104
+ - spec/data_bag_spec.rb
105
+ - spec/environment_spec.rb
106
+ - spec/role_spec.rb
107
+ - spec/spec_helper.rb
108
+ homepage: https://github.com/bmarini/knife-inspect
109
+ licenses: []
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project: knife-inspect
128
+ rubygems_version: 1.8.11
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: Inspect your chef repo as is compares to what is on your chef server
132
+ test_files:
133
+ - spec/chef-repo/.chef/client.pem
134
+ - spec/chef-repo/.chef/knife.rb
135
+ - spec/cookbook_spec.rb
136
+ - spec/data_bag_item_spec.rb
137
+ - spec/data_bag_spec.rb
138
+ - spec/environment_spec.rb
139
+ - spec/role_spec.rb
140
+ - spec/spec_helper.rb