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.
@@ -1,327 +1,100 @@
1
- #
2
- # Modifying Author:: Jon Cowie (<jonlives@gmail.com>)
3
- # Copyright:: Copyright (c) 2011 Jon Cowie
4
- # License:: Apache License, Version 2.0
5
- #
6
- # Modified cookbook upload to always freeze, and disable --force option, some other options disabled such as
7
- # updating environment constraints, as this is done later in the spork workflow.
8
-
9
- # Based on the knife cookbook upload plugin by:
10
- #
11
- # Author:: Adam Jacob (<adam@opscode.com>)
12
- # Author:: Christopher Walters (<cw@opscode.com>)
13
- # Author:: Nuo Yan (<yan.nuo@gmail.com>)
14
- # Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
15
- # License:: Apache License, Version 2.0
16
- #
17
- # Licensed under the Apache License, Version 2.0 (the "License");
18
- # you may not use this file except in compliance with the License.
19
- # You may obtain a copy of the License at
20
- #
21
- # http://www.apache.org/licenses/LICENSE-2.0
22
- #
23
- # Unless required by applicable law or agreed to in writing, software
24
- # distributed under the License is distributed on an "AS IS" BASIS,
25
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26
- # See the License for the specific language governing permissions and
27
- # limitations under the License.
28
- #
29
-
30
- require 'app_conf'
31
1
  require 'chef/knife'
2
+ require 'chef/exceptions'
3
+ require 'chef/cookbook_loader'
4
+ require 'chef/cookbook_uploader'
32
5
  require 'socket'
33
- require 'hipchat'
34
6
 
35
7
  module KnifeSpork
36
8
  class SporkUpload < Chef::Knife
9
+ include KnifeSpork::Runner
37
10
 
38
- CHECKSUM = "checksum"
39
- MATCH_CHECKSUM = /[0-9a-f]{32,}/
11
+ CHECKSUM = 'checksum'
12
+ MATCH_CHECKSUM = /[0-9a-f]{32,}/
40
13
 
41
- @@fcavail = true
42
- deps do
43
- require 'chef/exceptions'
44
- require 'chef/cookbook_loader'
45
- require 'chef/cookbook_uploader'
46
- begin
47
- require "foodcritic"
48
- rescue LoadError
49
- @@fcavail = false
50
- end
51
- end
52
-
53
- banner "knife spork upload [COOKBOOKS...] (options)"
14
+ banner 'knife spork upload [COOKBOOKS...] (options)'
54
15
 
55
- option :cookbook_path,
56
- :short => "-o PATH:PATH",
57
- :long => "--cookbook-path PATH:PATH",
58
- :description => "A colon-separated path to look for cookbooks in",
59
- :proc => lambda { |o| o.split(":") }
16
+ option :cookbook_path,
17
+ :short => '-o PATH:PATH',
18
+ :long => '--cookbook-path PATH:PATH',
19
+ :description => 'A colon-separated path to look for cookbooks in',
20
+ :proc => lambda { |o| o.split(':') }
60
21
 
61
- option :freeze,
62
- :long => '--freeze',
63
- :description => 'Freeze this version of the cookbook so that it cannot be overwritten',
64
- :boolean => true
22
+ option :freeze,
23
+ :long => '--freeze',
24
+ :description => 'Freeze this version of the cookbook so that it cannot be overwritten',
25
+ :boolean => true
65
26
 
66
- option :depends,
67
- :short => "-d",
68
- :long => "--include-dependencies",
69
- :description => "Also upload cookbook dependencies"
27
+ option :depends,
28
+ :short => '-D',
29
+ :long => '--include-dependencies',
30
+ :description => 'Also upload cookbook dependencies'
70
31
 
71
- def run
72
-
73
- if RUBY_VERSION.to_f < 1.9
74
- ui.fatal "Sorry, knife-spork requires ruby 1.9 or newer."
75
- exit 1
76
- end
77
-
78
- self.config = Chef::Config.merge!(config)
79
- @conf = AppConf.new
80
-
81
- if File.exists?("#{config[:cookbook_path].first.gsub("cookbooks","")}config/spork-config.yml")
82
- @conf.load("#{config[:cookbook_path].first.gsub("cookbooks","")}config/spork-config.yml")
83
- ui.msg "Loaded config file #{config[:cookbook_path].first.gsub("cookbooks","")}config/spork-config.yml...\n\n"
84
- end
85
-
86
- if File.exists?("/etc/spork-config.yml")
87
- @conf.load("/etc/spork-config.yml")
88
- ui.msg "Loaded config file /etc/spork-config.yml...\n\n"
89
- end
90
-
91
- if File.exists?(File.expand_path("~/.chef/spork-config.yml"))
92
- @conf.load(File.expand_path("~/.chef/spork-config.yml"))
93
- ui.msg "Loaded config file #{File.expand_path("~/.chef/spork-config.yml")}...\n\n"
94
- end
95
-
96
- config[:cookbook_path] ||= Chef::Config[:cookbook_path]
97
-
98
- warn_about_cookbook_shadowing
99
- # Get a list of cookbooks and their versions from the server
100
- # for checking existence of dependending cookbooks.
101
- @server_side_cookbooks = Chef::CookbookVersion.list
102
-
103
- if @name_args.empty?
104
- show_usage
105
- ui.error("You must specify the --all flag or at least one cookbook name")
106
- exit 1
107
- end
108
- justify_width = @name_args.map {|name| name.size }.max.to_i + 2
109
- @name_args.each do |cookbook_name|
110
- begin
111
- cookbook = cookbook_repo[cookbook_name]
112
- if config[:depends]
113
- cookbook.metadata.dependencies.each do |dep, versions|
114
- @name_args.push dep
115
- end
116
- end
117
-
118
- if !@conf.foodcritic.nil? && @conf.foodcritic.enabled
119
- if !@@fcavail
120
- ui.msg "Foodcritic gem not available, skipping cookbook lint check.\n\n"
121
- else
122
- foodcritic_lint_check(cookbook_name)
123
- end
124
- end
125
-
126
- ui.info("Uploading and freezing #{cookbook.name.to_s.ljust(justify_width + 10)} [#{cookbook.version}]")
127
-
128
- upload(cookbook, justify_width)
129
- cookbook.freeze_version
130
- upload(cookbook, justify_width)
131
-
132
- if !@conf.irccat.nil? && @conf.irccat.enabled
133
- begin
134
-
135
- if !@conf.irccat.channel?(String)
136
- channels = @conf.irccat.channel
137
- else
138
- channels = ["#{@conf.irccat.channel}"]
139
- end
140
-
141
- channels.each do |c|
142
- message = "#{c} #BOLD#PURPLECHEF:#NORMAL #{ENV['USER']} uploaded and froze cookbook #TEAL#{cookbook_name}#NORMAL version #TEAL#{cookbook.version}#NORMAL"
143
- s = TCPSocket.open(@conf.irccat.server,@conf.irccat.port)
144
- s.write(message)
145
- s.close
146
- end
147
- rescue Exception => msg
148
- puts "Something went wrong with sending to irccat: (#{msg})"
149
- end
150
- end
32
+ def run
33
+ self.config = Chef::Config.merge!(config)
34
+ config[:cookbook_path] ||= Chef::Config[:cookbook_path]
151
35
 
152
- if !@conf.hipchat.nil? && @conf.hipchat.enabled
153
- begin
154
- message = "#{ENV['USER']} uploaded and froze cookbook #{cookbook_name} version #{cookbook.version}"
155
- client = HipChat::Client.new(@conf.hipchat.apikey)
156
- client["#{@conf.hipchat.room}"].send( @conf.hipchat.nickname, message, :notify => @conf.hipchat.notify, :color => @conf.hipchat.color )
157
- rescue Exception => msg
158
- puts "Something went wrong with sending to HipChat: (#{msg})"
159
- end
160
- end
161
-
162
- if !@conf.eventinator.nil? && @conf.eventinator.enabled
163
- metadata = {}
164
- metadata[:cookbook_name] = cookbook.name
165
- metadata[:cookbook_version] = cookbook.version
166
-
167
- event_data = {}
168
- event_data[:tag] = "knife"
169
- event_data[:username] = ENV['USER']
170
- event_data[:status] = "#{ENV['USER']} uploaded and froze version #{cookbook.version} of cookbook #{cookbook_name}"
171
- event_data[:metadata] = metadata.to_json
172
-
173
- uri = URI.parse(@conf.eventinator.url)
174
-
175
- http = Net::HTTP.new(uri.host, uri.port)
176
-
177
- ## TODO: should make this configurable, timeout after 5 sec
178
- http.read_timeout = 5;
179
-
180
- request = Net::HTTP::Post.new(uri.request_uri)
181
- request.set_form_data(event_data)
182
-
183
- begin
184
- response = http.request(request)
185
- if response.code != "200"
186
- ui.warn("Got a #{response.code} from #{@conf.eventinator.url} upload wasn't eventinated")
187
- end
188
- rescue Timeout::Error
189
- ui.warn("Timed out connecting to #{@conf.eventinator.url} upload wasn't eventinated")
190
- rescue Exception => msg
191
- ui.warn("An unhandled execption occured while eventinating: #{msg}")
192
- end
193
- end
194
-
195
- rescue Chef::Exceptions::CookbookNotFoundInRepo => e
196
- ui.error("Could not find cookbook #{cookbook_name} in your cookbook path, skipping it")
197
- Chef::Log.debug(e)
198
- end
199
- end
200
-
201
- ui.info "upload complete"
36
+ if @name_args.empty?
37
+ show_usage
38
+ ui.error("You must specify the --all flag or at least one cookbook name")
39
+ exit 1
202
40
  end
203
41
 
204
- def cookbook_repo
205
- @cookbook_loader ||= begin
206
- Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, config[:cookbook_path]) }
207
- Chef::CookbookLoader.new(config[:cookbook_path])
208
- end
209
- end
210
-
211
- def warn_about_cookbook_shadowing
212
- unless cookbook_repo.merged_cookbooks.empty?
213
- ui.warn "* " * 40
214
- ui.warn(<<-WARNING)
215
- The cookbooks: #{cookbook_repo.merged_cookbooks.join(', ')} exist in multiple places in your cookbook_path.
216
- A composite version of these cookbooks has been compiled for uploading.
217
-
218
- #{ui.color('IMPORTANT:', :red, :bold)} In a future version of Chef, this behavior will be removed and you will no longer
219
- be able to have the same version of a cookbook in multiple places in your cookbook_path.
220
- WARNING
221
- ui.warn "The affected cookbooks are located:"
222
- ui.output ui.format_for_display(cookbook_repo.merged_cookbook_paths)
223
- ui.warn "* " * 40
224
- end
225
- end
42
+ @cookbooks = load_cookbooks(name_args)
43
+ include_dependencies if config[:depends]
226
44
 
227
- private
45
+ run_plugins(:before_upload)
46
+ upload
47
+ run_plugins(:after_upload)
48
+ end
228
49
 
229
- def upload(cookbook, justify_width)
230
- check_for_broken_links(cookbook)
231
- check_dependencies(cookbook)
232
- Chef::CookbookUploader.new(cookbook, config[:cookbook_path]).upload_cookbook
233
- rescue Net::HTTPServerException => e
234
- case e.response.code
235
- when "409"
236
- ui.error "Version #{cookbook.version} of cookbook #{cookbook.name} is frozen. Please bump your version number."
237
- Chef::Log.debug(e)
238
- exit 1
239
- else
240
- raise
241
- end
50
+ private
51
+ def include_dependencies
52
+ @cookbooks.each do |cookbook|
53
+ @cookbooks.concat(load_cookbooks(cookbook.metadata.dependencies.keys))
242
54
  end
243
55
 
244
- # if only you people wouldn't put broken symlinks in your cookbooks in
245
- # the first place. ;)
246
- def check_for_broken_links(cookbook)
247
- # MUST!! dup the cookbook version object--it memoizes its
248
- # manifest object, but the manifest becomes invalid when you
249
- # regenerate the metadata
250
- broken_files = cookbook.dup.manifest_records_by_path.select do |path, info|
251
- info[CHECKSUM].nil? || info[CHECKSUM] !~ MATCH_CHECKSUM
252
- end
253
- unless broken_files.empty?
254
- broken_filenames = Array(broken_files).map {|path, info| path}
255
- ui.error "The cookbook #{cookbook.name} has one or more broken files"
256
- ui.info "This is probably caused by broken symlinks in the cookbook directory"
257
- ui.info "The broken file(s) are: #{broken_filenames.join(' ')}"
258
- exit 1
259
- end
260
- end
56
+ @cookbooks.uniq!
57
+ end
261
58
 
262
- def check_dependencies(cookbook)
263
- # for each dependency, check if the version is on the server, or
264
- # the version is in the cookbooks being uploaded. If not, exit and warn the user.
265
- cookbook.metadata.dependencies.each do |cookbook_name, version|
266
- unless check_server_side_cookbooks(cookbook_name, version) || check_uploading_cookbooks(cookbook_name, version)
267
- # warn the user and exit
268
- ui.error "Cookbook #{cookbook.name} depends on cookbook #{cookbook_name} version #{version},"
269
- ui.error "which is not currently being uploaded and cannot be found on the server."
270
- exit 1
59
+ def upload
60
+ # upload cookbooks in reverse so that dependencies are satisfied first
61
+ @cookbooks.reverse.each do |cookbook|
62
+ begin
63
+ check_dependencies(cookbook)
64
+ Chef::CookbookUploader.new(cookbook, ::Chef::Config.cookbook_path).upload_cookbook
65
+ if name_args.include?(cookbook.name.to_s)
66
+ ui.info "Freezing #{cookbook.name} at #{cookbook.version}..."
67
+ cookbook.freeze_version
68
+ Chef::CookbookUploader.new(cookbook, ::Chef::Config.cookbook_path).upload_cookbook
271
69
  end
272
- end
273
- end
274
-
275
- def check_server_side_cookbooks(cookbook_name, version)
276
- if @server_side_cookbooks[cookbook_name].nil?
277
- false
278
- else
279
- @server_side_cookbooks[cookbook_name]["versions"].each do |versions_hash|
280
- return true if Chef::VersionConstraint.new(version).include?(versions_hash["version"])
70
+ rescue Net::HTTPServerException => e
71
+ if e.response.code == '409'
72
+ ui.error "#{cookbook.name}@#{cookbook.version} is frozen. Please bump your version number before continuing!"
73
+ exit(1)
74
+ else
75
+ raise
281
76
  end
282
- false
283
77
  end
284
78
  end
285
79
 
286
- def check_uploading_cookbooks(cookbook_name, version)
287
- if config[:all]
288
- # check from all local cookbooks in the path
289
- unless cookbook_repo[cookbook_name].nil?
290
- return Chef::VersionConstraint.new(version).include?(cookbook_repo[cookbook_name].version)
291
- end
292
- else
293
- # check from only those in the command argument
294
- if @name_args.include?(cookbook_name)
295
- return Chef::VersionConstraint.new(version).include?(cookbook_repo[cookbook_name].version)
296
- end
297
- end
298
- false
299
- end
300
-
301
- def foodcritic_lint_check(cookbook_name)
302
-
303
- cookbook_path = cookbook_repo[cookbook_name].root_dir
80
+ ui.msg "Successfully uploaded #{@cookbooks.collect{|c| "#{c.name}@#{c.version}"}.join(', ')}!"
81
+ end
304
82
 
305
- fail_tags = []
306
- fail_tags = @conf.foodcritic.fail_tags unless @conf.foodcritic.fail_tags.nil?
307
-
308
- tags = []
309
- tags = @conf.foodcritic.tags unless @conf.foodcritic.tags.nil?
310
-
311
- include_rules = []
312
- include_rules = @conf.foodcritic.include_rules unless @conf.foodcritic.include_rules.nil?
313
-
314
- ui.msg "Lint checking #{cookbook_name}..."
315
- options = {:fail_tags => fail_tags, :tags =>tags, :include_rules => include_rules}
316
- review = FoodCritic::Linter.new.check("#{cookbook_path}",options)
317
-
318
- if review.failed?
319
- ui.error "Lint check failed. Halting upload."
320
- ui.error "Lint check output:"
321
- ui.error review
322
- exit 1
83
+ # Ensures that all the cookbooks dependencies are either already on the server or being uploaded in this pass
84
+ def check_dependencies(cookbook)
85
+ cookbook.metadata.dependencies.each do |cookbook_name, version|
86
+ unless server_side_cookbooks(cookbook_name, version)
87
+ ui.error "#{cookbook.name} depends on #{cookbook_name} (#{version}), which is not currently being uploaded and cannot be found on the server!"
88
+ exit(1)
323
89
  end
324
- ui.msg "Lint check passed"
325
90
  end
326
91
  end
92
+
93
+ def server_side_cookbooks(cookbook_name, version)
94
+ @server_side_cookbooks ||= Chef::CookbookVersion.list
95
+
96
+ hash = @server_side_cookbooks[cookbook_name]
97
+ hash && hash['versions'] && hash['versions'].any?{ |v| Chef::VersionConstraint.new(version).include?(v['version']) }
98
+ end
327
99
  end
100
+ end
data/lib/knife-spork.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module KnifeSpork
2
- VERSION = "0.1.11"
2
+ require 'knife-spork/plugins'
3
3
  end
@@ -0,0 +1,22 @@
1
+ module KnifeSpork
2
+ module Plugins
3
+ # Load each of the drop-in plugins
4
+ Dir[File.expand_path('../plugins/**/*.rb', __FILE__)].each { |f| require f }
5
+
6
+ def self.run(options = {})
7
+ hook = options[:hook].to_sym
8
+
9
+ klasses.each do |klass|
10
+ plugin = klass.new(options)
11
+ plugin.send(hook) if plugin.respond_to?(hook) && plugin.enabled?
12
+ end
13
+ end
14
+
15
+ # Get and return a list of all subclasses (plugins) that are not the base plugin
16
+ def self.klasses
17
+ @@klasses ||= self.constants.collect do |c|
18
+ self.const_get(c) if self.const_get(c).is_a?(Class) && self.const_get(c) != KnifeSpork::Plugins::Plugin
19
+ end.compact
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ require 'knife-spork/plugins/plugin'
2
+
3
+ module KnifeSpork
4
+ module Plugins
5
+ class Campfire < Plugin
6
+ name :campfire
7
+
8
+ def perform; end
9
+
10
+ def after_upload
11
+ campfire do |rooms|
12
+ rooms.paste <<-EOH
13
+ #{current_user} froze the following cookbooks on Chef Server:
14
+ #{cookbooks.collect{|c| " #{c.name}@#{c.version}"}.join("\n")}
15
+ EOH
16
+ end
17
+ end
18
+
19
+ def after_promote_remote
20
+ campfire do |rooms|
21
+ rooms.paste <<-EOH
22
+ #{current_user} promoted cookbooks on Chef Server:
23
+
24
+ cookbooks:
25
+ #{cookbooks.collect{|c| " #{c.name}@#{c.version}"}.join("\n")}
26
+
27
+ environments:
28
+ #{environments.collect{|e| " #{e.name}"}.join("\n")}
29
+ EOH
30
+ end
31
+ end
32
+
33
+ private
34
+ def campfire(&block)
35
+ safe_require 'tinder'
36
+
37
+ rooms = [config.rooms || config.room].flatten.compact
38
+ campfire = Tinder::Campfire.new(config.account, :token => config.token)
39
+
40
+ rooms.each do |room_name|
41
+ room = campfire.find_room_by_name(room_name)
42
+ yield(room) unless room.nil?
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end