knife-spork 0.1.11 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,71 @@
1
+ require 'knife-spork/plugins/plugin'
2
+ require 'json'
3
+ require 'uri'
4
+
5
+ module KnifeSpork
6
+ module Plugins
7
+ class Eventinator < Plugin
8
+ name :eventinator
9
+
10
+ def perform; end
11
+
12
+ def after_upload
13
+ cookbooks.each do |cookbook|
14
+ event_data = {
15
+ :tag => 'knife',
16
+ :username => current_user,
17
+ :status => "#{current_user} has uploaded and frozen #{cookbook.name}@#{cookbook.version}",
18
+ :metadata => {
19
+ :cookbook_name => cookbook.name,
20
+ :cookbook_version => cookbook.version
21
+ }.to_json
22
+ }
23
+ eventinate(event_data)
24
+ end
25
+ end
26
+
27
+ def after_promote_remote
28
+ environments.each do |environment|
29
+ cookbooks.each do |cookbook|
30
+ event_data = {
31
+ :tag => 'knife',
32
+ :username => current_user,
33
+ :status => "#{current_user} has promoted #{cookbook.name}(#{cookbook.version}) to #{environment.name}",
34
+ :metadata => {
35
+ :cookbook_name => cookbook.name,
36
+ :cookbook_version => cookbook.version
37
+ }.to_json
38
+ }
39
+ eventinate(event_data)
40
+ end
41
+ end
42
+ end
43
+
44
+ def eventinate(event_data)
45
+ begin
46
+ uri = URI.parse(config.url)
47
+ rescue Exception => e
48
+ ui.error 'Could not parse URI for Eventinator.'
49
+ ui.error e.to_s
50
+ return
51
+ end
52
+
53
+ http = Net::HTTP.new(uri.host, uri.port)
54
+ http.read_timeout = config.read_timeout || 5
55
+
56
+ request = Net::HTTP::Post.new(uri.request_uri)
57
+ request.set_form_data(event_data)
58
+
59
+ begin
60
+ response = http.request(request)
61
+ ui.error "Eventinator at #{config.url} did not receive a good response from the server" if response.code != '200'
62
+ rescue Timeout::Error
63
+ ui.error "Eventinator timed out connecting to #{config.url}. Is that URL accessible?"
64
+ rescue Exception => e
65
+ ui.error 'Eventinator error.'
66
+ ui.error e.to_s
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,41 @@
1
+ require 'knife-spork/plugins/plugin'
2
+
3
+ module KnifeSpork
4
+ module Plugins
5
+ class Foodcritic < Plugin
6
+ name :foodcritic
7
+ hooks :after_check, :before_upload
8
+
9
+ def perform
10
+ safe_require 'foodcritic'
11
+
12
+ tags = config.tags || []
13
+ fail_tags = config.fail_tags || ['any']
14
+ include_rules = config.include_rules || []
15
+
16
+ cookbooks.each do |cookbook|
17
+ ui.info "Running foodcritic against #{cookbook.name}@#{cookbook.version}..."
18
+
19
+ cookbook_path = cookbook.root_dir
20
+
21
+ ui.info cookbook_path
22
+
23
+ options = {:tags => tags, :fail_tags => fail_tags, :include_rules => include_rules}
24
+ review = ::FoodCritic::Linter.new.check([cookbook_path], options)
25
+
26
+ if review.failed?
27
+ ui.error "Foodcritic failed!"
28
+ review.to_s.split("\n").each{ |r| ui.error r.to_s }
29
+ exit(1) if config.epic_fail
30
+ else
31
+ ui.info "Passed!"
32
+ end
33
+ end
34
+ end
35
+
36
+ def epic_fail?
37
+ config.epic_fail.nil? ? 'true' : config.epic_fail
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,123 @@
1
+ require 'knife-spork/plugins/plugin'
2
+
3
+ module KnifeSpork
4
+ module Plugins
5
+ class Git < Plugin
6
+ name :git
7
+
8
+ def perform; end
9
+
10
+ def before_bump
11
+ git_pull
12
+ git_pull_submodules
13
+ end
14
+
15
+ def before_upload
16
+ git_pull
17
+ git_pull_submodules
18
+ end
19
+
20
+ def before_promote
21
+ git_pull
22
+ git_pull_submodules
23
+ end
24
+
25
+ def after_bump
26
+ cookbooks.each do |cookbook|
27
+ git_add("#{cookbook.root_dir}/metadata.rb")
28
+ end
29
+ end
30
+
31
+ def after_promote_local
32
+ environments.each do |environment|
33
+ git_add("./environments/#{environment}.json")
34
+ end
35
+ end
36
+
37
+ private
38
+ def git
39
+ safe_require 'git'
40
+ log = Logger.new(STDOUT)
41
+ log.level = Logger::WARN
42
+ @git ||= begin
43
+ ::Git.open('.', :log => log)
44
+ rescue
45
+ ui.error 'You are not currently in a git repository. Ensure you are in the proper working directory or remove the git plugin from your KnifeSpork configuration!'
46
+ exit(0)
47
+ end
48
+ end
49
+
50
+ # In this case, a git pull will:
51
+ # - Stash local changes
52
+ # - Pull from the remote
53
+ # - Pop the stash
54
+ def git_pull
55
+ ui.msg "Pulling latest changes from remote Git repo."
56
+ begin
57
+ git.fetch(remote)
58
+ git.merge("#{remote}/#{branch}")
59
+ rescue ::Git::GitExecuteError => e
60
+ ui.error "Could not pull from remote #{remote}/#{branch}. Does it exist?"
61
+ end
62
+ end
63
+
64
+ def git_pull_submodules
65
+ ui.msg "Pulling latest changes from git submodules (if any)"
66
+ output = IO.popen ("git submodule foreach git pull 2>&1")
67
+ Process.wait
68
+ exit_code = $?
69
+ if !exit_code.exitstatus == 0
70
+ ui.error "#{output.read()}\n"
71
+ exit 1
72
+ end
73
+ end
74
+
75
+ def git_add(filepath)
76
+ begin
77
+ ui.msg "Git add'ing #{filepath}"
78
+ git.add("#{filepath}")
79
+ rescue ::Git::GitExecuteError => e
80
+ ui.error "Git: Something went wrong with git add #{filepath}. Please try running git add manually."
81
+ end
82
+ end
83
+
84
+ # Commit changes, if any
85
+ def git_commit
86
+ begin
87
+ git.add('.')
88
+ `git ls-files --deleted`.chomp.split("\n").each{ |f| git.remove(f) }
89
+ git.commit_all "[KnifeSpork] Bumping cookbooks:\n#{cookbooks.collect{|c| " #{c.name}@#{c.version}"}.join("\n")}"
90
+ rescue ::Git::GitExecuteError; end
91
+ end
92
+
93
+ def git_push(tags = false)
94
+ begin
95
+ git.push remote, branch, tags
96
+ rescue ::Git::GitExecuteError => e
97
+ ui.error "Could not push to remote #{remote}/#{branch}. Does it exist?"
98
+ end
99
+ end
100
+
101
+ def git_tag(tag)
102
+ begin
103
+ git.add_tag(tag)
104
+ rescue ::Git::GitExecuteError => e
105
+ ui.error "Could not tag #{tag_name}. Does it already exist?"
106
+ ui.error 'You may need to delete the tag before running promote again.'
107
+ end
108
+ end
109
+
110
+ def remote
111
+ config.remote || 'origin'
112
+ end
113
+
114
+ def branch
115
+ config.branch || 'master'
116
+ end
117
+
118
+ def tag_name
119
+ cookbooks.collect{|c| "#{c.name}@#{c.version}"}.join('-')
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,25 @@
1
+ require 'knife-spork/plugins/plugin'
2
+
3
+ module KnifeSpork
4
+ module Plugins
5
+ class Graphite < Plugin
6
+ name :graphite
7
+ hooks :after_promote_remote
8
+
9
+ def perform
10
+ environments.each do |environment|
11
+ begin
12
+ message = "deploys.chef.#{environment} 1 #{Time.now.to_i}\n"
13
+ socket = TCPSocket.open(config.server, config.port)
14
+ socket.write(message)
15
+ rescue Exception => e
16
+ ui.error 'Graphite was unable to process the request.'
17
+ ui.error e.to_s
18
+ ensure
19
+ socket.close unless socket.nil?
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,50 @@
1
+ require 'knife-spork/plugins/plugin'
2
+
3
+ module KnifeSpork
4
+ module Plugins
5
+ class HipChat < Plugin
6
+ name :hip_chat
7
+
8
+ def perform; end
9
+
10
+ def after_upload
11
+ hipchat "#{current_user} uploaded the following cookbooks:\n#{cookbooks.collect{ |c| " #{c.name}@#{c.version}" }.join("\n")}"
12
+ end
13
+
14
+ def after_promote_remote
15
+ hipchat "#{current_user} promoted the following cookbooks:\n#{cookbooks.collect{ |c| " #{c.name}@#{c.version}" }.join("\n")} to #{environments.collect{ |e| "#{e.name}" }.join(", ")}"
16
+ end
17
+
18
+ private
19
+ def hipchat(message)
20
+ safe_require 'hipchat'
21
+
22
+ rooms.each do |room_name|
23
+ begin
24
+ client = ::HipChat::Client.new(config.api_token)
25
+ client[room_name].send(nickname, message, notify:notify, color:color)
26
+ rescue Exception => e
27
+ ui.error 'Something went wrong sending to HipChat.'
28
+ ui.error e.to_s
29
+ end
30
+ end
31
+ end
32
+
33
+ def rooms
34
+ [ config.room || config.rooms ].flatten
35
+ end
36
+
37
+ def nickname
38
+ config.nickname || 'KnifeSpork'
39
+ end
40
+
41
+ def notify
42
+ config.notify.nil? ? true : config.notify
43
+ end
44
+
45
+ def color
46
+ config.color || 'yellow'
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,48 @@
1
+ require 'knife-spork/plugins/plugin'
2
+
3
+ module KnifeSpork
4
+ module Plugins
5
+ class Irccat < Plugin
6
+ name :irccat
7
+
8
+ def perform; end
9
+
10
+ def after_upload
11
+ irccat("#BOLD#PURPLECHEF:#NORMAL #{current_user} uploaded #TEAL#{cookbooks.collect{ |c| "#{c.name}@#{c.version}" }.join(", ")}#NORMAL")
12
+ end
13
+
14
+ def after_promote_remote
15
+ environments.each do |environment|
16
+ diff = environment_diffs[environment.name]
17
+ env_gist = gist(environment, diff) if config.gist
18
+ irccat("#BOLD#PURPLECHEF:#NORMAL #{current_user} promoted #TEAL#{cookbooks.collect{ |c| "#{c.name}@#{c.version}" }.join(", ")}#NORMAL to #{environment.name} #{env_gist}")
19
+ end
20
+ end
21
+
22
+ private
23
+ def irccat(message)
24
+ channels.each do |channel|
25
+ begin
26
+ # Write the message using a TCP Socket
27
+ socket = TCPSocket.open(config.server, config.port)
28
+ socket.write("#{channel} #{message}")
29
+ rescue Exception => e
30
+ ui.error 'Failed to post message with irccat.'
31
+ ui.error e.to_s
32
+ ensure
33
+ socket.close unless socket.nil?
34
+ end
35
+ end
36
+ end
37
+
38
+ def gist(environment, diff)
39
+ msg = "Environment #{environment} uploaded at #{Time.now.getutc} by #{current_user}\n\nConstraints updated on server in this version:\n\n#{diff.collect { |k, v| "#{k}: #{v}\n" }.join}"
40
+ %x[ echo "#{msg}" | #{config.gist}]
41
+ end
42
+
43
+ def channels
44
+ [ config.channel || config.channels ].flatten
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,74 @@
1
+ module KnifeSpork
2
+ module Plugins
3
+ class Plugin
4
+ # This is the name of the plugin. It must correspond to the name in the yaml configuration
5
+ # file in order to load this plugin. If an attribute is passed in, the name is set to that
6
+ # given value. Otherwise, the name is returned.
7
+ def self.name(name = nil)
8
+ if name.nil?
9
+ class_variable_get(:@@name)
10
+ else
11
+ class_variable_set(:@@name, name)
12
+ end
13
+ end
14
+
15
+ # This is a convenience method for defining multiple hooks in a single call.
16
+ def self.hooks(*the_hooks)
17
+ [the_hooks].flatten.each{ |the_hook| hook(the_hook) }
18
+ end
19
+
20
+ # When defining a hook, we define a method on the instance that corresponds to that
21
+ # hook. That will be fired when the hook is fired.
22
+ def self.hook(the_hook)
23
+ self.send(:define_method, the_hook.to_sym) do
24
+ perform
25
+ end
26
+ end
27
+
28
+ def initialize(options = {})
29
+ @options = {
30
+ :payload => {}
31
+ }.merge(options)
32
+ end
33
+
34
+ def enabled?
35
+ !config.nil?
36
+ end
37
+
38
+ private
39
+ def config
40
+ @options[:config].plugins.send(self.class.name.to_sym)
41
+ end
42
+
43
+ def cookbooks
44
+ @options[:cookbooks]
45
+ end
46
+
47
+ def environments
48
+ @options[:environments]
49
+ end
50
+
51
+ def environment_diffs
52
+ @options[:environment_diffs]
53
+ end
54
+
55
+ def ui
56
+ @options[:ui]
57
+ end
58
+
59
+ def current_user
60
+ (begin `git config user.name`.chomp; rescue nil; end || ENV['USERNAME'] || ENV['USER']).strip
61
+ end
62
+
63
+ # Wrapper method around require that attempts to include the associated file. If it does not exist
64
+ # or cannot be loaded, an nice error is produced instead of blowing up.
65
+ def safe_require(file)
66
+ begin
67
+ require file
68
+ rescue LoadError
69
+ raise "You are using a plugin for knife-spork that requires #{file}, but you have not installed it. Please either run \"gem install #{file}\", add #{file} to your Gemfile or remove the plugin from your configuration."
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,166 @@
1
+ require 'app_conf'
2
+ require 'json'
3
+
4
+ require 'chef/cookbook_loader'
5
+ require 'chef/knife/core/object_loader'
6
+ require 'knife-spork/plugins'
7
+
8
+ module KnifeSpork
9
+ module Runner
10
+ module ClassMethods; end
11
+
12
+ module InstanceMethods
13
+ def spork_config
14
+ return @spork_config unless @spork_config.nil?
15
+
16
+ @spork_config = AppConf.new
17
+ load_paths = [ File.expand_path('config/spork-config.yml'), '/etc/spork-config.yml', File.expand_path('~/.chef/spork-config.yml') ]
18
+ load_paths.each do |load_path|
19
+ if File.exists?(load_path)
20
+ @spork_config.load(load_path)
21
+ end
22
+ end
23
+
24
+ @spork_config
25
+ end
26
+
27
+ def run_plugins(hook)
28
+ cookbooks = [ @cookbooks || @cookbook ].flatten.compact.collect{|cookbook| cookbook.is_a?(::Chef::CookbookVersion) ? cookbook : load_cookbook(cookbook)}.sort{|a,b| a.name.to_s <=> b.name.to_s}
29
+ environments = [ @environments || @environment ].flatten.compact.collect{|environment| environment.is_a?(::Chef::Environment) ? environment : load_environment(environment)}.sort{|a,b| a.name.to_s <=> b.name.to_s}
30
+ environment_diffs = @environment_diffs
31
+
32
+ KnifeSpork::Plugins.run(
33
+ :config => spork_config,
34
+ :hook => hook.to_sym,
35
+ :cookbooks => cookbooks,
36
+ :environments => environments,
37
+ :environment_diffs => environment_diffs,
38
+ :ui => ui
39
+ )
40
+ end
41
+
42
+ def load_environments_and_cookbook
43
+ ensure_environment_provided!
44
+
45
+ if @name_args.size == 2
46
+ [ [@name_args[0]].flatten, @name_args[1] ]
47
+ elsif @name_args.size == 1
48
+ [ [default_environments].flatten, @name_args[0] ]
49
+ end
50
+ end
51
+
52
+ def ensure_environment_provided!
53
+ if default_environments.empty? && @name_args.size < 2
54
+ ui.error('You must specify a cookbook name and an environment')
55
+ exit(1)
56
+ end
57
+ end
58
+
59
+ def default_environments
60
+ [ spork_config.default_environment || spork_config.default_environments ].flatten.compact
61
+ end
62
+
63
+ def pretty_print_json(json)
64
+ JSON.pretty_generate(json)
65
+ end
66
+
67
+ def valid_version?(version)
68
+ version_keys = version.split('.')
69
+ return false unless version_keys.size == 3 && version_keys.any?{ |k| begin Float(k); rescue false; else true; end }
70
+ true
71
+ end
72
+
73
+ def validate_version!(version)
74
+ if version && !valid_version?(version)
75
+ ui.error("#{version} is not a valid version!")
76
+ exit(1)
77
+ end
78
+ end
79
+
80
+ def loader
81
+ @loader ||= Chef::Knife::Core::ObjectLoader.new(::Chef::Environment, ui)
82
+ end
83
+
84
+ # It's not feasible to try and "guess" which cookbook path to use, so we will
85
+ # always just use the first one in the path.
86
+ def cookbook_path
87
+ ensure_cookbook_path!
88
+ [config[:cookbook_path] ||= ::Chef::Config.cookbook_path].flatten[0]
89
+ end
90
+
91
+ def all_cookbooks
92
+ ::Chef::CookbookLoader.new(::Chef::Config.cookbook_path)
93
+ end
94
+
95
+ def load_cookbook(cookbook_name)
96
+ return cookbook_name if cookbook_name.is_a?(::Chef::CookbookVersion)
97
+ loader = ::Chef::CookbookLoader.new(Chef::Config.cookbook_path)
98
+ loader[cookbook_name]
99
+ end
100
+
101
+ def load_cookbooks(cookbook_names)
102
+ cookbook_names = [cookbook_names].flatten
103
+ cookbook_names.collect{ |cookbook_name| load_cookbook(cookbook_name) }
104
+ end
105
+
106
+ def load_environment(environment_name)
107
+ loader.load_from('environments', "#{environment_name}.json")
108
+ end
109
+
110
+ def load_remote_environment(environment_name)
111
+ begin
112
+ Chef::Environment.load(environment_name)
113
+ rescue Net::HTTPServerException => e
114
+ ui.error "Could not load #{environment_name} from Chef Server. You must upload the environment manually the first time."
115
+ exit(1)
116
+ end
117
+ end
118
+
119
+ def environment_diff (local_environment, remote_environment)
120
+ local_environment_versions = local_environment.to_hash['cookbook_versions']
121
+ remote_environment_versions = remote_environment.to_hash['cookbook_versions']
122
+ remote_environment_versions.diff(local_environment_versions)
123
+ end
124
+
125
+ def constraints_diff (environment_diff)
126
+ Hash[Hash[environment_diff.map{|k,v| [k, v.split(" changed to ").map{|x|x.gsub("= ","")}]}].map{|k,v|[k,calc_diff(v)]}]
127
+ end
128
+
129
+ def calc_diff(version)
130
+ components = version.map{|v|v.split(".")}
131
+ if components[1][0].to_i != components[0][0].to_i
132
+ return (components[1][0].to_i - components[0][0].to_i)*100
133
+ elsif components[1][1].to_i != components[0][1].to_i
134
+ return (components[1][1].to_i - components[0][1].to_i)*10
135
+ else
136
+ return (components[1][2].to_i - components[0][2].to_i)
137
+ end
138
+ end
139
+
140
+ def ensure_cookbook_path!
141
+ if !config.has_key?(:cookbook_path)
142
+ ui.fatal "No default cookbook_path; Specify with -o or fix your knife.rb."
143
+ show_usage
144
+ exit(1)
145
+ end
146
+ end
147
+ end
148
+
149
+ def self.included(receiver)
150
+ receiver.extend(ClassMethods)
151
+ receiver.send(:include, InstanceMethods)
152
+ end
153
+ end
154
+ end
155
+
156
+
157
+ class Hash
158
+ def diff(other)
159
+ self.keys.inject({}) do |memo, key|
160
+ unless self[key] == other[key]
161
+ memo[key] = "#{self[key]} changed to #{other[key]}"
162
+ end
163
+ memo
164
+ end
165
+ end
166
+ end