├── .rvmrc ├── Gemfile ├── Rakefile ├── lib ├── knife-spork.rb ├── knife-spork │ ├── plugins │ │ ├── graphite.rb │ │ ├── statusnet.rb │ │ ├── foodcritic.rb │ │ ├── campfire.rb │ │ ├── hipchat.rb │ │ ├── jabber.rb │ │ ├── irccat.rb │ │ ├── eventinator.rb │ │ ├── plugin.rb │ │ └── git.rb │ ├── plugins.rb │ └── runner.rb └── chef │ └── knife │ ├── spork-info.rb │ ├── spork-bump.rb │ ├── spork-check.rb │ ├── spork-upload.rb │ └── spork-promote.rb ├── .gitignore ├── plugins ├── Eventinator.md ├── Graphite.md ├── Template.md ├── StatusNet.md ├── Irccat.md ├── Campfire.md ├── HipChat.md ├── Git.md ├── Foodcritic.md ├── Jabber.md └── README.md ├── knife-spork.gemspec ├── LICENSE ├── CHANGELOG.md └── README.md /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.3@knife-spork --create 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require 'bundler/gem_tasks' 3 | -------------------------------------------------------------------------------- /lib/knife-spork.rb: -------------------------------------------------------------------------------- 1 | module KnifeSpork 2 | require 'knife-spork/plugins' 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | .project 7 | Gemfile.lock 8 | InstalledFiles 9 | _yardoc 10 | coverage 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | .idea 20 | -------------------------------------------------------------------------------- /plugins/Eventinator.md: -------------------------------------------------------------------------------- 1 | Eventinator 2 | =========== 3 | 4 | Gem Requirements 5 | ---------------- 6 | This plugin has no gem requirements. 7 | 8 | Hooks 9 | ----- 10 | - `after_upload` 11 | 12 | Configuration 13 | ------------- 14 | ```yaml 15 | plugins: 16 | eventinator: 17 | url: www.example.com 18 | read_timeout: 5 19 | ``` 20 | 21 | #### url 22 | The server to post to. 23 | 24 | - Type: `String` 25 | 26 | #### read_timeout 27 | The timeout, in seconds, for the request to return. 28 | 29 | - Type: `Integer` 30 | - Default: `5` 31 | -------------------------------------------------------------------------------- /plugins/Graphite.md: -------------------------------------------------------------------------------- 1 | Graphite 2 | ======== 3 | Graphite will automatically send a request to your Graphite server under the deploys.chef.[environment] for graphical analysis. 4 | 5 | Gem Requirements 6 | ---------------- 7 | This plugin has no gem requirements. 8 | 9 | Hooks 10 | ----- 11 | - `after_promote` 12 | 13 | Configuration 14 | ------------- 15 | ```yaml 16 | plugins: 17 | graphite: 18 | server: graphite.example.com 19 | port: 12345 20 | ``` 21 | 22 | #### server 23 | The url to the graphite server 24 | 25 | - Type: `String` 26 | 27 | #### port 28 | The port of the graphite server 29 | 30 | - Type: `Integer` 31 | -------------------------------------------------------------------------------- /plugins/Template.md: -------------------------------------------------------------------------------- 1 | Plugin Name 2 | =========== 3 | Here is an optional, short description about your plugin. 4 | 5 | Gem Requirements 6 | ---------------- 7 | This plugin requires the following gems: 8 | 9 | ```ruby 10 | gem 'my_gem', '~> 1.4.5' 11 | gem 'other_gem', '>= 5.0.1' 12 | ``` 13 | 14 | Hooks 15 | ----- 16 | - `after_promote` 17 | 18 | Configuration 19 | ------------- 20 | ```yaml 21 | plugins: 22 | plugin_name: 23 | option_1: true 24 | option_2: 25 | - a 26 | - b 27 | - c 28 | ``` 29 | 30 | #### option_1 31 | This is a description of the option. 32 | 33 | - Type: `String` 34 | - Default: `ABC` 35 | -------------------------------------------------------------------------------- /lib/knife-spork/plugins/graphite.rb: -------------------------------------------------------------------------------- 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 26 | -------------------------------------------------------------------------------- /plugins/StatusNet.md: -------------------------------------------------------------------------------- 1 | StatusNet 2 | ======= 3 | StatusNet posts messages to a your StatusNet instance 4 | 5 | Gem Requirements 6 | ---------------- 7 | This plugin requires the following gems: 8 | 9 | ```ruby 10 | gem 'curb' 11 | ``` 12 | 13 | Hooks 14 | ----- 15 | - `after_upload` 16 | - `after_promote` 17 | 18 | Configuration 19 | ------------- 20 | ```yaml 21 | plugins: 22 | statusnet: 23 | url: YOUR INSTANCE API URL 24 | username: YOURUSER 25 | password: YOURPASSWORD 26 | ``` 27 | 28 | #### url 29 | Your StatusNet instance API url, usually server url + /api/statuses/update.xml 30 | 31 | - Type: `Srtring` 32 | 33 | #### username 34 | Your StatusNet username. 35 | 36 | - Type: `String` 37 | 38 | #### password 39 | Your StatusNet password. 40 | 41 | - Type: `String` 42 | -------------------------------------------------------------------------------- /lib/chef/knife/spork-info.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife' 2 | require 'knife-spork/runner' 3 | 4 | module KnifeSpork 5 | class SporkInfo < Chef::Knife 6 | include KnifeSpork::Runner 7 | 8 | banner 'knife spork info' 9 | 10 | def run 11 | self.config = Chef::Config.merge!(config) 12 | 13 | run_plugins(:before_info) 14 | info 15 | run_plugins(:after_info) 16 | end 17 | 18 | private 19 | def info 20 | ui.msg "Config Hash:" 21 | ui.msg "#{spork_config.to_hash}" 22 | ui.msg "" 23 | ui.msg "Plugins:" 24 | KnifeSpork::Plugins.klasses.each do |klass| 25 | plugin = klass.new(:config => spork_config) 26 | ui.msg "#{klass}: #{plugin.enabled? ? 'enabled' : 'disabled'}" 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/knife-spork/plugins.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /plugins/Irccat.md: -------------------------------------------------------------------------------- 1 | Plugin Name 2 | =========== 3 | This plugin interfaces with the irccat IRC bot (https://github.com/RJ/irccat) 4 | 5 | Gem Requirements 6 | ---------------- 7 | This plugin has no gem requirements.` 8 | 9 | Hooks 10 | ----- 11 | - `after_promote` 12 | 13 | Configuration 14 | ------------- 15 | ```yaml 16 | plugins: 17 | irccat: 18 | server: irc.example.com 19 | port: 54 20 | channels: 21 | - #chef 22 | - #knife 23 | gist: "/usr/bin/gist" 24 | ``` 25 | 26 | #### server 27 | The url of the IRC server. 28 | 29 | - Type: `String` 30 | 31 | #### port 32 | The port of the IRC server. 33 | 34 | - Type: `String` 35 | 36 | #### channels 37 | The channels to post to. 38 | 39 | - Type: `Array` 40 | 41 | #### gist 42 | Optional path to gist binary installed by https://rubygems.org/gems/gist 43 | 44 | - Type: `String` -------------------------------------------------------------------------------- /plugins/Campfire.md: -------------------------------------------------------------------------------- 1 | Campfire 2 | ======== 3 | Automatically posts informational messages to Campfire 4 | 5 | Gem Requirements 6 | ---------------- 7 | This plugin requires the following gems: 8 | 9 | ```ruby 10 | gem 'campy' 11 | ``` 12 | 13 | Hooks 14 | ----- 15 | - `after_promote` 16 | - `after_upload` 17 | 18 | Configuration 19 | ------------- 20 | ```yaml 21 | plugins: 22 | campfire: 23 | account: my_company 24 | token: ABC123 25 | rooms: 26 | - General 27 | - Web Operations 28 | ``` 29 | 30 | #### account 31 | This is your campfire account name. It is the subdomain part of your account. 32 | 33 | - Type: `String` 34 | 35 | #### token 36 | This is the secure token you get from the Campfire configuration. 37 | 38 | - Type: `String` 39 | 40 | #### Rooms 41 | This is an array of room names to post messages to. 42 | 43 | - Type: `String` 44 | -------------------------------------------------------------------------------- /plugins/HipChat.md: -------------------------------------------------------------------------------- 1 | HipChat 2 | ======= 3 | HipChat posts messages to your HipChat client. 4 | 5 | Gem Requirements 6 | ---------------- 7 | This plugin requires the following gems: 8 | 9 | ```ruby 10 | gem 'hipchat' 11 | ``` 12 | 13 | Hooks 14 | ----- 15 | - `after_upload` 16 | - `after_promote` 17 | 18 | Configuration 19 | ------------- 20 | ```yaml 21 | plugins: 22 | hipchat: 23 | api_token: ABC123 24 | rooms: 25 | - General 26 | - Web Operations 27 | notify: true 28 | color: yellow 29 | ``` 30 | 31 | #### api_token 32 | Your HipChat API token. 33 | 34 | - Type: `String` 35 | 36 | #### rooms 37 | The list of rooms to post to. 38 | 39 | - Type: `Array` 40 | 41 | #### notify 42 | Boolean value indicating whether the room should be notified. 43 | 44 | - Type: `Boolean` 45 | 46 | #### color 47 | THe color of the message. 48 | 49 | - Type: `String` 50 | - Acceptable Values: `[yellow, red, green, purple, random]` 51 | -------------------------------------------------------------------------------- /knife-spork.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path('../lib', __FILE__) 2 | 3 | Gem::Specification.new do |gem| 4 | gem.name = 'knife-spork' 5 | gem.version = '1.0.17' 6 | gem.authors = ["Jon Cowie"] 7 | gem.email = 'jonlives@gmail.com' 8 | gem.homepage = 'https://github.com/jonlives/knife-spork' 9 | gem.summary = "A workflow plugin to help many devs work with the same chef repo/server" 10 | gem.description = "A workflow plugin to help many devs work with the same chef repo/server" 11 | 12 | gem.files = `git ls-files`.split($\) 13 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 14 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 15 | gem.name = "knife-spork" 16 | gem.require_paths = ["lib"] 17 | 18 | gem.add_runtime_dependency 'chef', '>= 0.10.4' 19 | gem.add_runtime_dependency 'git', '>= 1.2.5' 20 | gem.add_runtime_dependency 'app_conf', '>= 0.4.0' 21 | end 22 | -------------------------------------------------------------------------------- /plugins/Git.md: -------------------------------------------------------------------------------- 1 | Git 2 | === 3 | This plugin attempts to help manage your workflow by automatically pulling changes from your repo. **Do not use this plugin if you are not using Git.** 4 | 5 | Gem Requirements 6 | ---------------- 7 | This plugin requires the following gems: 8 | 9 | ```ruby 10 | gem 'git' 11 | ``` 12 | 13 | Hooks 14 | ----- 15 | - `before_bump` 16 | - `after_bump` 17 | - `after_promote` 18 | 19 | Configuration 20 | ------------- 21 | ```yaml 22 | plugins: 23 | git: 24 | remote: origin 25 | branch: master 26 | ``` 27 | 28 | **Note** Due to the nature of the git plugin, it's possible that you accept all the defaults. In that case, you should make your configuration like this: 29 | 30 | ```yaml 31 | plugins: 32 | git: 33 | enabled: true 34 | ``` 35 | 36 | #### remote 37 | The git remote to push/pull to/from. 38 | 39 | - Type: `String` 40 | - Default: `origin` 41 | 42 | #### branch 43 | The git branch to push/pull to/from. 44 | 45 | - Type: `String` 46 | - Default: `master` 47 | -------------------------------------------------------------------------------- /plugins/Foodcritic.md: -------------------------------------------------------------------------------- 1 | Foodcritic 2 | ========== 3 | Automatically runs foodcritic against your cookbooks on check and upload. 4 | 5 | Gem Requirements 6 | ---------------- 7 | This plugin requires the following gems: 8 | 9 | ```ruby 10 | gem 'foodcritic' 11 | ``` 12 | 13 | Hooks 14 | ----- 15 | - `after_check` 16 | - `before_upload` 17 | 18 | Configuration 19 | ------------- 20 | ```yaml 21 | plugins: 22 | foodcritic: 23 | tags: 24 | - FC0023 25 | fail_tags: 26 | - any 27 | include_rules: 28 | - foodcritic/etsy 29 | epic_fail: true 30 | ``` 31 | 32 | #### tags 33 | The tags to check against. 34 | 35 | - Type: `Array` 36 | - Default: '[any]' 37 | 38 | #### fail_tags 39 | The list of tags to fail on. 40 | 41 | - Type: 'Array' 42 | - Default: '[any]' 43 | 44 | #### include_rules 45 | An optional list of additional rules to run. 46 | 47 | - Type: `Array` 48 | 49 | #### epic_fail: 50 | If set to true, `epic_fail` will prevent you from uploading a cookbook until all foodcritic rules pass. 51 | 52 | - Type: `Boolean` 53 | - Default: `true` 54 | -------------------------------------------------------------------------------- /plugins/Jabber.md: -------------------------------------------------------------------------------- 1 | Jabber 2 | ======= 3 | Jabber posts messages to a designated Jabber group chat room. 4 | 5 | Gem Requirements 6 | ---------------- 7 | This plugin requires the following gems: 8 | 9 | ```ruby 10 | gem 'xmpp4r' 11 | ``` 12 | 13 | Hooks 14 | ----- 15 | - `after_upload` 16 | - `after_promote` 17 | 18 | Configuration 19 | ------------- 20 | ```yaml 21 | plugins: 22 | jabber: 23 | username: YOURUSER 24 | password: YOURPASSWORD 25 | nickname: Chef Bot 26 | server_name: your.jabberserver.com 27 | server_port: 5222 28 | rooms: 29 | - engineering@your.conference.com/spork 30 | - systems@your.conference.com/spork 31 | ``` 32 | 33 | #### username 34 | Your Jabber username. 35 | 36 | - Type: `String` 37 | 38 | #### password 39 | Your Jabber password. 40 | 41 | - Type: `String` 42 | 43 | #### nickname 44 | A nickname to use in the conference room when making announcements. 45 | 46 | - Type: `String` 47 | 48 | #### server_name 49 | Your Jabber server name. 50 | 51 | - Type: `String` 52 | 53 | #### server_port 54 | Your Jabber server port number. Default: 5222 55 | 56 | - Type: `String` 57 | 58 | #### rooms 59 | The list of rooms to post to. 60 | 61 | - Type: `Array` 62 | -------------------------------------------------------------------------------- /lib/knife-spork/plugins/statusnet.rb: -------------------------------------------------------------------------------- 1 | require 'knife-spork/plugins/plugin' 2 | 3 | module KnifeSpork 4 | module Plugins 5 | class StatusNet < Plugin 6 | name :statusnet 7 | 8 | def perform; end 9 | 10 | def after_upload 11 | statusnet "#{organization}#{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 | statusnet "#{organization}#{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 | 20 | def statusnet(message) 21 | safe_require 'curb' 22 | 23 | begin 24 | c = Curl::Easy.new(config.url) 25 | c.http_auth_types = :basic 26 | c.username = config.username 27 | c.password = config.password 28 | c.post_body = message 29 | c.perform 30 | rescue Exception => e 31 | ui.error 'Something went wrong sending to StatusNet.' 32 | ui.error e.to_s 33 | end 34 | end 35 | 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | KnifeSpork 2 | ---------- 3 | Author:: Jon Cowie () 4 | Copyright:: Copyright (c) 2011 Jon Cowie 5 | License:: GPL 6 | 7 | Plugin API 8 | ---------- 9 | Author:: Seth Vargo () 10 | Copyright:: Copyright (c) 2012 Seth Vargo 11 | License:: GPL 12 | 13 | Other 14 | ----- 15 | Based on the knife-cookbook-bump plugin by: 16 | Alalanta (no license specified) 17 | 18 | With snippets from: 19 | Author:: Adam Jacob () 20 | Author:: Christopher Walters () 21 | Author:: Nuo Yan () 22 | Copyright:: Copyright (c) 2009, 2010 Opscode, Inc. 23 | License:: Apache License, Version 2.0 24 | 25 | Licensed under the Apache License, Version 2.0 (the "License"); 26 | you may not use this file except in compliance with the License. 27 | You may obtain a copy of the License at 28 | 29 | http://www.apache.org/licenses/LICENSE-2.0 30 | 31 | Unless required by applicable law or agreed to in writing, software 32 | distributed under the License is distributed on an "AS IS" BASIS, 33 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 34 | See the License for the specific language governing permissions and 35 | limitations under the License. 36 | -------------------------------------------------------------------------------- /lib/knife-spork/plugins/foodcritic.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/knife-spork/plugins/campfire.rb: -------------------------------------------------------------------------------- 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 | #{organization}#{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 | #{organization}#{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 'campy' 36 | 37 | rooms = [config.rooms || config.room].flatten.compact 38 | campfire = Campy::Room.new(:account => config.account, :token => config.token) 39 | 40 | rooms.each do |room_name| 41 | room = Campy::Room.new( 42 | :account => config.account, 43 | :token => config.token, 44 | :room => room_name 45 | ) 46 | yield(room) unless room.nil? 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/knife-spork/plugins/hipchat.rb: -------------------------------------------------------------------------------- 1 | require 'knife-spork/plugins/plugin' 2 | 3 | module KnifeSpork 4 | module Plugins 5 | class HipChat < Plugin 6 | name :hipchat 7 | 8 | def perform; end 9 | 10 | def after_upload 11 | hipchat "#{organization}#{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 "#{organization}#{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 51 | -------------------------------------------------------------------------------- /lib/knife-spork/plugins/jabber.rb: -------------------------------------------------------------------------------- 1 | require 'knife-spork/plugins/plugin' 2 | 3 | module KnifeSpork 4 | module Plugins 5 | class Jabber < Plugin 6 | name :jabber 7 | 8 | def perform; end 9 | 10 | def after_upload 11 | jabber "#{organization}#{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 | jabber "#{organization}#{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 | 20 | def jabber(message) 21 | safe_require 'xmpp4r' 22 | safe_require 'xmpp4r/muc/helper/simplemucclient' 23 | 24 | client = ::Jabber::Client.new(config.username) 25 | client.connect(host = config.server_name, port = config.server_port ||= '5222') 26 | client.auth(config.password) 27 | 28 | rooms.each do |room_name| 29 | begin 30 | conference = ::Jabber::MUC::SimpleMUCClient.new(client) 31 | conference.join("#{room_name}/#{nickname}") 32 | conference.say(message) 33 | rescue Exception => e 34 | ui.error 'Something went wrong sending to Jabber.' 35 | ui.error e.to_s 36 | end 37 | end 38 | end 39 | 40 | def rooms 41 | [ config.room || config.rooms ].flatten 42 | end 43 | 44 | def nickname 45 | config.nickname || 'KnifeSpork' 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/knife-spork/plugins/irccat.rb: -------------------------------------------------------------------------------- 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 #{organization}#{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 #{organization}#{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 49 | -------------------------------------------------------------------------------- /lib/chef/knife/spork-bump.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife' 2 | require 'knife-spork/runner' 3 | 4 | module KnifeSpork 5 | class SporkBump < Chef::Knife 6 | include KnifeSpork::Runner 7 | 8 | TYPE_INDEX = { :major => 0, :minor => 1, :patch => 2, :manual => 3 }.freeze 9 | 10 | banner 'knife spork bump COOKBOOK [major|minor|patch|manual]' 11 | 12 | def run 13 | self.config = Chef::Config.merge!(config) 14 | 15 | if @name_args.empty? 16 | show_usage 17 | ui.error("You must specify at least a cookbook name") 18 | exit 1 19 | end 20 | 21 | #First load so plugins etc know what to work with 22 | @cookbook = load_cookbook(name_args.first) 23 | 24 | run_plugins(:before_bump) 25 | 26 | #Reload cookbook in case a VCS plugin found updates 27 | @cookbook = load_cookbook(name_args.first) 28 | bump 29 | run_plugins(:after_bump) 30 | end 31 | 32 | private 33 | def bump 34 | old_version = @cookbook.version 35 | 36 | if bump_type == 3 37 | # manual bump 38 | version_array = manual_bump_version.split('.') 39 | else 40 | # major, minor, or patch bump 41 | version_array = old_version.split('.').collect{ |i| i.to_i } 42 | version_array[bump_type] += 1 43 | ((bump_type+1)..2).each{ |i| version_array[i] = 0 } # reset all lower version numbers to 0 44 | end 45 | 46 | new_version = version_array.join('.') 47 | 48 | metadata_file = "#{@cookbook.root_dir}/metadata.rb" 49 | new_contents = File.read(metadata_file).gsub(/version\s+['"][0-9\.]+['"]/, "version \"#{new_version}\"") 50 | File.open(metadata_file, 'w'){ |f| f.write(new_contents) } 51 | 52 | ui.info "Successfully bumped #{@cookbook.name} to v#{new_version}!" 53 | end 54 | 55 | def bump_type 56 | TYPE_INDEX[(name_args[1] || 'patch').to_sym] 57 | end 58 | 59 | def manual_bump_version 60 | version = name_args.last 61 | validate_version!(version) 62 | version 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/knife-spork/plugins/eventinator.rb: -------------------------------------------------------------------------------- 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 => "#{organization}#{current_user} uploaded and froze #{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 => "#{organization}#{current_user} 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 72 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | Plugins Directory 2 | ================= 3 | This folder contains relevant documentation for each KnifeSpork plugin. For more information, usage, and options for an particular plugin, click on the assoicated markdown file in the tree above. 4 | 5 | Creating a Plugin 6 | ----------------- 7 | To create a plugin, start with the following basic boiler template: 8 | 9 | ```ruby 10 | require 'knife-spork/plugins/plugin' 11 | 12 | module KnifeSpork 13 | module Plugins 14 | class MyPlugin < Plugin 15 | name :my_plugin 16 | 17 | def perform 18 | # your plugin code here 19 | end 20 | end 21 | end 22 | end 23 | ``` 24 | 25 | **Don't forget to update the class name and the `name` at the very top of the class!** 26 | 27 | Helpers 28 | ------- 29 | The following "helpers" or "methods" are exposed: 30 | 31 | #### safe_require 32 | This method allows you to safely require a gem. This is helpful when your plugin requires an external plugin. It will output a nice error message if the gem cannot be loaded and stop executing. 33 | 34 | #### current_user 35 | This method tries to get the current user's name in the following manner: 36 | 37 | 1. From the git configuration 38 | 2. From the `ENV` 39 | 40 | #### config 41 | This method returns the config associated with the current plugin. For example, if a `spork-config.yml` file looked like this: 42 | 43 | ```yaml 44 | plugins: 45 | my_plugin: 46 | option_1: my_value 47 | option_2: other_value 48 | ``` 49 | 50 | then 51 | 52 | ```text 53 | config.option_1 #=> 'my_value' 54 | config.option_2 #=> 'other_value' 55 | ``` 56 | 57 | This uses `app_conf`, so you access the keys are methods, not `[]`. 58 | 59 | #### cookbooks 60 | This returns an array of `Chef::CookbookVersion` objects corresponding to the cookbooks that are being changed/altered in the hook. For more information on the methods avaliable, see the [file in the Chef repo](https://github.com/opscode/chef/blob/master/chef/lib/chef/cookbook_version.rb). 61 | 62 | #### environments 63 | This returns an array of `Chef::Environment` objects corresponding to the environments that are being changed/altered in the hook. For more information on the methods avaliable, see the [file in the Chef repo](https://github.com/opscode/chef/blob/master/chef/lib/chef/environment.rb). 64 | 65 | #### environment_diffs 66 | This returns a Hash of Hash objects containing a diff between local and remote environment for each environment changed/altered in the hook. Currently, this will only be populated during the promotea action. 67 | 68 | 69 | #### ui 70 | This returns a `Chef::Knife::UI` element for outputting to the console. For more information on the methods avaliable, see the [file in the Chef repo](https://github.com/opscode/chef/blob/master/chef/lib/chef/knife/core/ui.rb). 71 | -------------------------------------------------------------------------------- /lib/knife-spork/plugins/plugin.rb: -------------------------------------------------------------------------------- 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? || config.enabled == false) 36 | end 37 | 38 | private 39 | def config 40 | @options[:config].plugins.send(self.class.name.to_sym) unless @options[:config].nil? || @options[:config].plugins.nil? 41 | end 42 | 43 | def organization 44 | unless ::Chef::Config.chef_server_url.nil? 45 | split_server_url = Chef::Config.chef_server_url.gsub(/http(s)?:\/\//,"").split('/') 46 | if split_server_url.length > 1 47 | return "#{split_server_url.last}: " 48 | end 49 | end 50 | 51 | nil 52 | end 53 | 54 | def cookbooks 55 | @options[:cookbooks] 56 | end 57 | 58 | def environments 59 | @options[:environments] 60 | end 61 | 62 | def environment_diffs 63 | @options[:environment_diffs] 64 | end 65 | 66 | def environment_path 67 | @options[:environment_path] 68 | end 69 | 70 | def cookbook_path 71 | @options[:cookbook_path] 72 | end 73 | 74 | def ui 75 | @options[:ui] 76 | end 77 | 78 | def current_user 79 | (begin `git config user.name`.chomp; rescue nil; end || ENV['USERNAME'] || ENV['USER']).strip 80 | end 81 | 82 | # Wrapper method around require that attempts to include the associated file. If it does not exist 83 | # or cannot be loaded, an nice error is produced instead of blowing up. 84 | def safe_require(file) 85 | begin 86 | require file 87 | rescue LoadError 88 | 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." 89 | end 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/chef/knife/spork-check.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife' 2 | require 'knife-spork/runner' 3 | 4 | module KnifeSpork 5 | class SporkCheck < Chef::Knife 6 | include KnifeSpork::Runner 7 | 8 | banner 'knife spork check COOKBOOK (options)' 9 | 10 | option :all, 11 | :short => '--a', 12 | :long => '--all', 13 | :description => 'Show all uploaded versions of the cookbook' 14 | 15 | option :fail, 16 | :long => "--fail", 17 | :description => "If the check fails exit with non-zero exit code" 18 | 19 | def run 20 | self.config = Chef::Config.merge!(config) 21 | 22 | if name_args.empty? 23 | ui.fatal 'You must specify a cookbook name!' 24 | show_usage 25 | exit(1) 26 | end 27 | 28 | #First load so plugins etc know what to work with 29 | initial_load 30 | 31 | run_plugins(:before_check) 32 | 33 | #Reload cookbook in case a VCS plugin found updates 34 | initial_load 35 | 36 | check 37 | run_plugins(:after_check) 38 | end 39 | 40 | private 41 | def check 42 | ui.msg "Checking versions for cookbook #{@cookbook.name}..." 43 | ui.msg "" 44 | ui.msg "Local Version:" 45 | ui.msg " #{local_version}" 46 | ui.msg "" 47 | ui.msg "Remote Versions: (* indicates frozen)" 48 | remote_versions.each do |remote_version| 49 | if frozen?(remote_version) 50 | ui.msg " *#{remote_version}" 51 | else 52 | ui.msg " #{remote_version}" 53 | end 54 | end 55 | ui.msg "" 56 | 57 | remote_versions.each do |remote_version| 58 | if remote_version == local_version 59 | if frozen?(remote_version) 60 | message = "Your local version (#{local_version}) is frozen on the remote server. You'll need to bump before you can upload." 61 | config[:fail] ? fail_and_exit("#{message}") : ui.warn("#{message}") 62 | else 63 | message = "The version #{local_version} exists on the server and is not frozen. Uploading will overwrite!" 64 | config[:fail] ? fail_and_exit("#{message}") : ui.error("#{message}") 65 | end 66 | 67 | return 68 | end 69 | end 70 | 71 | ui.msg 'Everything looks good!' 72 | end 73 | 74 | def initial_load 75 | begin 76 | @cookbook = load_cookbook(name_args.first) 77 | rescue Chef::Exceptions::CookbookNotFoundInRepo => e 78 | ui.error "#{name_args.first} does not exist locally in your cookbook path(s), Exiting." 79 | exit(1) 80 | end 81 | end 82 | 83 | def local_version 84 | @cookbook.version 85 | end 86 | 87 | def remote_versions 88 | @remote_versions ||= begin 89 | environment = config[:environment] 90 | api_endpoint = environment ? "environments/#{environment}/cookbooks/#{@cookbook.name}" : "cookbooks/#{@cookbook.name}" 91 | cookbooks = rest.get_rest(api_endpoint) 92 | 93 | versions = cookbooks[@cookbook.name.to_s]['versions'] 94 | (config[:all] ? versions : versions[0..4]).collect{|v| v['version']} 95 | rescue Net::HTTPServerException => e 96 | ui.info "#{@cookbook.name} does not yet exist on the Chef Server!" 97 | return [] 98 | end 99 | end 100 | 101 | def frozen?(version) 102 | @versions_cache ||= {} 103 | 104 | @versions_cache[version.to_sym] ||= begin 105 | environment = config[:environment] 106 | api_endpoint = environment ? "environments/#{environment}/cookbooks/#{@cookbook.name}" : "cookbooks/#{@cookbook.name}/#{version}" 107 | rest.get_rest(api_endpoint).to_hash['frozen?'] 108 | end 109 | end 110 | 111 | def fail_and_exit(message, options={}) 112 | ui.fatal message 113 | show_usage if options[:show_usage] 114 | exit 1 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/chef/knife/spork-upload.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife' 2 | require 'chef/exceptions' 3 | require 'chef/cookbook_loader' 4 | require 'chef/cookbook_uploader' 5 | require 'knife-spork/runner' 6 | require 'socket' 7 | 8 | module KnifeSpork 9 | class SporkUpload < Chef::Knife 10 | include KnifeSpork::Runner 11 | 12 | CHECKSUM = 'checksum' 13 | MATCH_CHECKSUM = /[0-9a-f]{32,}/ 14 | 15 | banner 'knife spork upload [COOKBOOKS...] (options)' 16 | 17 | option :cookbook_path, 18 | :short => '-o PATH:PATH', 19 | :long => '--cookbook-path PATH:PATH', 20 | :description => 'A colon-separated path to look for cookbooks in', 21 | :proc => lambda { |o| o.split(':') } 22 | 23 | option :freeze, 24 | :long => '--freeze', 25 | :description => 'Freeze this version of the cookbook so that it cannot be overwritten', 26 | :boolean => true 27 | 28 | option :depends, 29 | :short => '-D', 30 | :long => '--include-dependencies', 31 | :description => 'Also upload cookbook dependencies' 32 | 33 | def run 34 | self.config = Chef::Config.merge!(config) 35 | config[:cookbook_path] ||= Chef::Config[:cookbook_path] 36 | 37 | if @name_args.empty? 38 | show_usage 39 | ui.error("You must specify the --all flag or at least one cookbook name") 40 | exit 1 41 | end 42 | 43 | #First load so plugins etc know what to work with 44 | @cookbooks = load_cookbooks(name_args) 45 | include_dependencies if config[:depends] 46 | 47 | run_plugins(:before_upload) 48 | 49 | #Reload cookbook in case a VCS plugin found updates 50 | @cookbooks = load_cookbooks(name_args) 51 | include_dependencies if config[:depends] 52 | 53 | upload 54 | run_plugins(:after_upload) 55 | end 56 | 57 | private 58 | def include_dependencies 59 | @cookbooks.each do |cookbook| 60 | @cookbooks.concat(load_cookbooks(cookbook.metadata.dependencies.keys)) 61 | end 62 | 63 | @cookbooks.uniq! 64 | end 65 | 66 | def upload 67 | # upload cookbooks in reverse so that dependencies are satisfied first 68 | @cookbooks.reverse.each do |cookbook| 69 | begin 70 | check_dependencies(cookbook) 71 | if name_args.include?(cookbook.name.to_s) 72 | uploader = Chef::CookbookUploader.new(cookbook, ::Chef::Config.cookbook_path) 73 | if uploader.respond_to?(:upload_cookbooks) 74 | # Chef >= 10.14.0 75 | uploader.upload_cookbooks 76 | ui.info "Freezing #{cookbook.name} at #{cookbook.version}..." 77 | cookbook.freeze_version 78 | uploader.upload_cookbooks 79 | else 80 | uploader.upload_cookbook 81 | ui.info "Freezing #{cookbook.name} at #{cookbook.version}..." 82 | cookbook.freeze_version 83 | uploader.upload_cookbook 84 | 85 | end 86 | end 87 | rescue Net::HTTPServerException => e 88 | if e.response.code == '409' 89 | ui.error "#{cookbook.name}@#{cookbook.version} is frozen. Please bump your version number before continuing!" 90 | exit(1) 91 | else 92 | raise 93 | end 94 | end 95 | end 96 | 97 | ui.msg "Successfully uploaded #{@cookbooks.collect{|c| "#{c.name}@#{c.version}"}.join(', ')}!" 98 | end 99 | 100 | # Ensures that all the cookbooks dependencies are either already on the server or being uploaded in this pass 101 | def check_dependencies(cookbook) 102 | cookbook.metadata.dependencies.each do |cookbook_name, version| 103 | unless server_side_cookbooks(cookbook_name, version) 104 | ui.error "#{cookbook.name} depends on #{cookbook_name} (#{version}), which is not currently being uploaded and cannot be found on the server!" 105 | exit(1) 106 | end 107 | end 108 | end 109 | 110 | def server_side_cookbooks(cookbook_name, version) 111 | if Chef::CookbookVersion.respond_to?(:list_all_versions) 112 | @server_side_cookbooks ||= Chef::CookbookVersion.list_all_versions 113 | else 114 | @server_side_cookbooks ||= Chef::CookbookVersion.list 115 | end 116 | 117 | hash = @server_side_cookbooks[cookbook_name] 118 | hash && hash['versions'] && hash['versions'].any?{ |v| Chef::VersionConstraint.new(version).include?(v['version']) } 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/knife-spork/plugins/git.rb: -------------------------------------------------------------------------------- 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(environment_path) unless cookbook_path.include?(environment_path.gsub"/environments","") 12 | git_pull_submodules(environment_path) unless cookbook_path.include?(environment_path.gsub"/environments","") 13 | cookbooks.each do |cookbook| 14 | git_pull(cookbook.root_dir) 15 | git_pull_submodules(cookbook.root_dir) 16 | end 17 | end 18 | 19 | def before_upload 20 | git_pull(environment_path) unless cookbook_path.include?(environment_path.gsub"/environments","") 21 | git_pull_submodules(environment_path) unless cookbook_path.include?(environment_path.gsub"/environments","") 22 | cookbooks.each do |cookbook| 23 | git_pull(cookbook.root_dir) 24 | git_pull_submodules(cookbook.root_dir) 25 | end 26 | end 27 | 28 | def before_promote 29 | cookbooks.each do |cookbook| 30 | git_pull(environment_path) unless cookbook.root_dir.include?(environment_path.gsub"/environments","") 31 | git_pull_submodules(environment_path) unless cookbook.root_dir.include?(environment_path.gsub"/environments","") 32 | git_pull(cookbook.root_dir) 33 | git_pull_submodules(cookbook.root_dir) 34 | end 35 | end 36 | 37 | def after_bump 38 | cookbooks.each do |cookbook| 39 | git_add(cookbook.root_dir,"metadata.rb") 40 | end 41 | end 42 | 43 | def after_promote_local 44 | environments.each do |environment| 45 | git_add(environment_path,"#{environment}.json") 46 | end 47 | end 48 | 49 | private 50 | def git 51 | safe_require 'git' 52 | log = Logger.new(STDOUT) 53 | log.level = Logger::WARN 54 | @git ||= begin 55 | ::Git.open('.', :log => log) 56 | rescue 57 | ui.error 'You are not currently in a git repository. Please ensure you are in a git repo, a repo subdirectory, or remove the git plugin from your KnifeSpork configuration!' 58 | exit(0) 59 | end 60 | end 61 | 62 | # In this case, a git pull will: 63 | # - Stash local changes 64 | # - Pull from the remote 65 | # - Pop the stash 66 | def git_pull(path) 67 | if is_repo?(path) 68 | ui.msg "Git: Pulling latest changes from #{path}" 69 | output = IO.popen("git pull 2>&1") 70 | Process.wait 71 | exit_code = $? 72 | if !exit_code.exitstatus == 0 73 | ui.error "#{output.read()}\n" 74 | exit 1 75 | end 76 | end 77 | end 78 | 79 | def git_pull_submodules(path) 80 | if is_repo?(path) 81 | ui.msg "Pulling latest changes from git submodules (if any)" 82 | top_level = `cd #{path} && git rev-parse --show-toplevel 2>&1`.chomp 83 | if is_submodule?(top_level) 84 | top_level = get_parent_dir(top_level) 85 | end 86 | output = IO.popen("cd #{top_level} && git submodule foreach git pull 2>&1") 87 | Process.wait 88 | exit_code = $? 89 | if !exit_code.exitstatus == 0 90 | ui.error "#{output.read()}\n" 91 | exit 1 92 | end 93 | end 94 | end 95 | 96 | def git_add(filepath,filename) 97 | if is_repo?(filepath) 98 | ui.msg "Git add'ing #{filepath}/#{filename}" 99 | output = IO.popen("cd #{filepath} && git add #{filename}") 100 | Process.wait 101 | exit_code = $? 102 | if !exit_code.exitstatus == 0 103 | ui.error "#{output.read()}\n" 104 | exit 1 105 | end 106 | end 107 | end 108 | 109 | # Commit changes, if any 110 | def git_commit 111 | begin 112 | git.add('.') 113 | `git ls-files --deleted`.chomp.split("\n").each{ |f| git.remove(f) } 114 | git.commit_all "[KnifeSpork] Bumping cookbooks:\n#{cookbooks.collect{|c| " #{c.name}@#{c.version}"}.join("\n")}" 115 | rescue ::Git::GitExecuteError; end 116 | end 117 | 118 | def git_push(tags = false) 119 | begin 120 | git.push remote, branch, tags 121 | rescue ::Git::GitExecuteError => e 122 | ui.error "Could not push to remote #{remote}/#{branch}. Does it exist?" 123 | end 124 | end 125 | 126 | def git_tag(tag) 127 | begin 128 | git.add_tag(tag) 129 | rescue ::Git::GitExecuteError => e 130 | ui.error "Could not tag #{tag_name}. Does it already exist?" 131 | ui.error 'You may need to delete the tag before running promote again.' 132 | end 133 | end 134 | 135 | def is_repo?(path) 136 | output = IO.popen("cd #{path} && git rev-parse --git-dir 2>&1") 137 | Process.wait 138 | if $? != 0 139 | ui.warn "#{path} is not a git repo, skipping..." 140 | return false 141 | else 142 | return true 143 | end 144 | end 145 | 146 | def is_submodule?(path) 147 | top_level = `cd #{path} && git rev-parse --show-toplevel 2>&1`.chomp 148 | output = IO.popen("cd #{top_level}/.. && git rev-parse --show-toplevel 2>&1") 149 | Process.wait 150 | if $? != 0 151 | return false 152 | else 153 | return true 154 | end 155 | end 156 | 157 | def get_parent_dir(path) 158 | top_level = path 159 | return_code = 0 160 | while return_code == 0 161 | output = IO.popen("cd #{top_level}/.. && git rev-parse --show-toplevel 2>&1") 162 | Process.wait 163 | return_code = $? 164 | cmd_output = output.read.chomp 165 | #cygwin, I hate you for making me do this 166 | if cmd_output.include?("fatal: Not a git repository") 167 | return_code = 1 168 | end 169 | if return_code == 0 170 | top_level = cmd_output 171 | end 172 | end 173 | top_level 174 | end 175 | 176 | def remote 177 | config.remote || 'origin' 178 | end 179 | 180 | def branch 181 | config.branch || 'master' 182 | end 183 | 184 | def tag_name 185 | cookbooks.collect{|c| "#{c.name}@#{c.version}"}.join('-') 186 | end 187 | end 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /lib/chef/knife/spork-promote.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife' 2 | require 'chef/exceptions' 3 | require 'knife-spork/runner' 4 | 5 | begin 6 | require 'berkshelf' 7 | rescue LoadError; end 8 | 9 | module KnifeSpork 10 | class SporkPromote < Chef::Knife 11 | include KnifeSpork::Runner 12 | 13 | banner 'knife spork promote ENVIRONMENT COOKBOOK (options)' 14 | 15 | option :version, 16 | :short => '-v', 17 | :long => '--version VERSION', 18 | :description => 'Set the environment\'s version constraint to the specified version', 19 | :default => nil 20 | 21 | option :remote, 22 | :long => '--remote', 23 | :description => 'Save the environment to the chef server in addition to the local JSON file', 24 | :default => nil 25 | 26 | if defined?(::Berkshelf) 27 | option :berksfile, 28 | :short => '-b', 29 | :long => 'berksfile', 30 | :description => 'Path to a Berksfile to operate off of', 31 | :default => File.join(Dir.pwd, ::Berkshelf::DEFAULT_FILENAME) 32 | end 33 | 34 | def run 35 | self.config = Chef::Config.merge!(config) 36 | 37 | if @name_args.empty? 38 | show_usage 39 | ui.error("You must specify the cookbook and environment to promote to") 40 | exit 1 41 | end 42 | 43 | #First load so plugins etc know what to work with 44 | @environments, @cookbook = load_environments_and_cookbook 45 | 46 | run_plugins(:before_promote) 47 | 48 | #Reload cookbook and env in case a VCS plugin found updates 49 | @environments, @cookbook = load_environments_and_cookbook 50 | 51 | check_cookbook_uploaded(@cookbook) 52 | 53 | @environments.each do |e| 54 | environment = load_environment(e) 55 | 56 | if @cookbook == 'all' 57 | ui.msg "Promoting ALL cookbooks to environment #{environment}" 58 | promote(environment, all_cookbooks) 59 | else 60 | promote(environment, @cookbook) 61 | end 62 | 63 | ui.msg "Saving changes to #{e}.json" 64 | 65 | new_environment_json = pretty_print_json(environment) 66 | save_environment_changes(e, new_environment_json) 67 | 68 | if config[:remote] 69 | ui.msg "Uploading #{environment.name}.json to Chef Server" 70 | save_environment_changes_remote(e) 71 | ui.info "Promotion complete at #{Time.now}!" 72 | else 73 | ui.info "Promotion complete. Don't forget to upload your changed #{environment.name}.json to Chef Server" 74 | end 75 | end 76 | run_plugins(:after_promote_local) 77 | if config[:remote] 78 | run_plugins(:after_promote_remote) 79 | end 80 | end 81 | 82 | def update_version_constraints(environment, cookbook, new_version) 83 | validate_version!(new_version) 84 | environment.cookbook_versions[cookbook] = "= #{new_version}" 85 | end 86 | 87 | def save_environment_changes_remote(environment) 88 | local_environment = load_environment(environment) 89 | remote_environment = load_remote_environment(environment) 90 | @environment_diffs ||= Hash.new 91 | @environment_diffs["#{environment}"] = environment_diff(local_environment, remote_environment) 92 | 93 | version_change_threshold = spork_config.version_change_threshold || 2 94 | env_constraints_diff = constraints_diff(@environment_diffs["#{environment}"]).select{|k,v| v > version_change_threshold} 95 | 96 | if env_constraints_diff.size !=0 then 97 | ui.warn 'You\'re about to promote a significant version number change to 1 or more cookbooks:' 98 | ui.warn @environment_diffs["#{environment}"].select{|k,v|env_constraints_diff.has_key?(k)}.collect{|k,v| "\t#{k}: #{v}"}.join("\n") 99 | 100 | begin 101 | ui.confirm('Are you sure you want to continue?') 102 | rescue SystemExit => e 103 | if e.status == 3 104 | ui.confirm("Would you like to reset your local #{environment}.json to match the remote server?") 105 | tmp = Chef::Environment.load(environment) 106 | save_environment_changes(environment, pretty_print_json(tmp)) 107 | ui.info "#{environment}.json was reset" 108 | end 109 | 110 | raise 111 | end 112 | end 113 | 114 | if @environment_diffs["#{environment}"].size > 1 115 | ui.msg "" 116 | ui.warn "You're about to promote changes to several cookbooks at once:" 117 | ui.warn @environment_diffs["#{environment}"].collect{|k,v| "\t#{k}: #{v}"}.join("\n") 118 | 119 | begin 120 | ui.confirm('Are you sure you want to continue?') 121 | rescue SystemExit => e 122 | if e.status == 3 123 | ui.confirm("Would you like to reset your local #{environment}.json to match the remote server?") 124 | tmp = Chef::Environment.load(environment) 125 | save_environment_changes(environment, pretty_print_json(tmp)) 126 | ui.info "#{environment}.json was reset" 127 | end 128 | 129 | raise 130 | end 131 | end 132 | 133 | local_environment.save 134 | end 135 | 136 | def save_environment_changes(environment, json) 137 | environments_path = spork_config[:environment_path] || cookbook_path.gsub('cookbooks', 'environments') 138 | environment_path = File.expand_path( File.join(environments_path, "#{environment}.json") ) 139 | 140 | File.open(environment_path, 'w'){ |f| f.puts(json) } 141 | end 142 | 143 | def promote(environment, cookbook_names) 144 | cookbook_names = [cookbook_names].flatten 145 | 146 | cookbook_names.each do |cookbook_name| 147 | validate_version!(config[:version]) 148 | version = config[:version] || load_cookbook(cookbook_name).version 149 | 150 | ui.msg "Adding version constraint #{cookbook_name} = #{version}" 151 | update_version_constraints(environment, cookbook_name, version) 152 | end 153 | end 154 | 155 | def check_cookbook_uploaded(cookbook_name) 156 | validate_version!(config[:version]) 157 | version = config[:version] || load_cookbook(cookbook_name).version 158 | 159 | environment = config[:environment] 160 | api_endpoint = environment ? "environments/#{environment}/cookbooks/#{cookbook_name}/#{version}" : "cookbooks/#{cookbook_name}/#{version}" 161 | 162 | begin 163 | cookbooks = rest.get_rest(api_endpoint) 164 | rescue Net::HTTPServerException => e 165 | ui.error "#{cookbook_name}@#{version} does not exist on Chef Server! Upload the cookbook first by running:\n\n\tknife spork upload #{cookbook_name}\n\n" 166 | exit(1) 167 | end 168 | end 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /lib/knife-spork/runner.rb: -------------------------------------------------------------------------------- 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("#{cookbook_path.gsub('cookbooks','')}/config/spork-config.yml"), 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 | :environment_path => environment_path, 39 | :cookbook_path => cookbook_path, 40 | :ui => ui 41 | ) 42 | end 43 | 44 | def load_environments_and_cookbook 45 | ensure_environment_provided! 46 | 47 | if @name_args.size == 2 48 | [ [@name_args[0]].flatten, @name_args[1] ] 49 | elsif @name_args.size == 1 50 | [ [default_environments].flatten, @name_args[0] ] 51 | end 52 | end 53 | 54 | def ensure_environment_provided! 55 | if default_environments.empty? && @name_args.size < 2 56 | ui.error('You must specify a cookbook name and an environment') 57 | exit(1) 58 | end 59 | end 60 | 61 | def default_environments 62 | [ spork_config.default_environment || spork_config.default_environments ].flatten.compact 63 | end 64 | 65 | def pretty_print_json(json) 66 | JSON.pretty_generate(json) 67 | end 68 | 69 | def valid_version?(version) 70 | version_keys = version.split('.') 71 | return false unless version_keys.size == 3 && version_keys.any?{ |k| begin Float(k); rescue false; else true; end } 72 | true 73 | end 74 | 75 | def validate_version!(version) 76 | if version && !valid_version?(version) 77 | ui.error("#{version} is not a valid version!") 78 | exit(1) 79 | end 80 | end 81 | 82 | def loader 83 | @loader ||= Chef::Knife::Core::ObjectLoader.new(::Chef::Environment, ui) 84 | end 85 | 86 | # It's not feasible to try and "guess" which cookbook path to use, so we will 87 | # always just use the first one in the path. 88 | def cookbook_path 89 | ensure_cookbook_path! 90 | [config[:cookbook_path] ||= ::Chef::Config.cookbook_path].flatten[0] 91 | end 92 | 93 | def environment_path 94 | spork_config[:environment_path] || cookbook_path.gsub("/cookbooks","/environments") 95 | end 96 | 97 | def all_cookbooks 98 | ::Chef::CookbookLoader.new(::Chef::Config.cookbook_path) 99 | end 100 | 101 | def load_cookbook(cookbook_name) 102 | return cookbook_name if cookbook_name.is_a?(::Chef::CookbookVersion) 103 | 104 | # Search the local chef repo first 105 | loader = ::Chef::CookbookLoader.new(Chef::Config.cookbook_path) 106 | if loader.has_key?(cookbook_name) 107 | return loader[cookbook_name] 108 | end 109 | 110 | # We didn't find the cookbook in our local repo, so check Berkshelf 111 | if defined?(::Berkshelf) 112 | berksfile = ::Berkshelf::Berksfile.from_file(self.config[:berksfile]) 113 | if cookbook = berksfile.sources.find{ |source| source.name == cookbook_name } 114 | return cookbook 115 | end 116 | end 117 | 118 | # TODO: add librarian support here 119 | 120 | raise ::Chef::Exceptions::CookbookNotFound, "Could not find cookbook '#{cookbook_name}' in any of the sources!" 121 | end 122 | 123 | def load_cookbooks(cookbook_names) 124 | cookbook_names = [cookbook_names].flatten 125 | cookbook_names.collect{ |cookbook_name| load_cookbook(cookbook_name) } 126 | end 127 | 128 | def load_environment(environment_name) 129 | loader.object_from_file("#{environment_path}/#{environment_name}.json") 130 | end 131 | 132 | def load_remote_environment(environment_name) 133 | begin 134 | Chef::Environment.load(environment_name) 135 | rescue Net::HTTPServerException => e 136 | ui.error "Could not load #{environment_name} from Chef Server. You must upload the environment manually the first time." 137 | exit(1) 138 | end 139 | end 140 | 141 | def environment_diff(local_environment, remote_environment) 142 | local_environment_versions = local_environment.to_hash['cookbook_versions'] 143 | remote_environment_versions = remote_environment.to_hash['cookbook_versions'] 144 | remote_environment_versions.diff(local_environment_versions) 145 | end 146 | 147 | def constraints_diff (environment_diff) 148 | Hash[Hash[environment_diff.map{|k,v| [k, v.split(" changed to ").map{|x|x.gsub("= ","")}]}].map{|k,v|[k,calc_diff(k,v)]}] 149 | end 150 | 151 | def calc_diff(cookbook, version) 152 | components = version.map{|v|v.split(".")} 153 | 154 | if components.length < 2 155 | ui.warn "#{cookbook} has no remote version to diff against!" 156 | return 0 157 | end 158 | 159 | if components[1][0].to_i != components[0][0].to_i 160 | return (components[1][0].to_i - components[0][0].to_i)*100 161 | elsif components[1][1].to_i != components[0][1].to_i 162 | return (components[1][1].to_i - components[0][1].to_i)*10 163 | else 164 | return (components[1][2].to_i - components[0][2].to_i) 165 | end 166 | end 167 | 168 | def ensure_cookbook_path! 169 | if !config.has_key?(:cookbook_path) 170 | ui.fatal "No default cookbook_path; Specify with -o or fix your knife.rb." 171 | show_usage 172 | exit(1) 173 | end 174 | end 175 | end 176 | 177 | def self.included(receiver) 178 | receiver.extend(ClassMethods) 179 | receiver.send(:include, InstanceMethods) 180 | end 181 | end 182 | end 183 | 184 | 185 | class Hash 186 | def diff(other) 187 | self.keys.inject({}) do |memo, key| 188 | unless self[key] == other[key] 189 | memo[key] = "#{self[key]} changed to #{other[key]}" 190 | end 191 | memo 192 | end 193 | end 194 | end 195 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.17 (15th February, 2013) 2 | 3 | Bugfixes: 4 | 5 | - Fix git plugin to work nicely with Cygwin and its unpredictable exit codes 6 | 7 | ## 1.0.16 (13th February, 2013) 8 | 9 | Bugfixes: 10 | 11 | - Reverted broken foodcritic plugin to that in 1.0.14 12 | 13 | ## 1.0.15 (12th February, 2013) 14 | 15 | Bugfixes: 16 | 17 | - Fixed git plugin so that when working on a submodule, submodules will be git pulled from the parent repo instead 18 | - Fixed foodcritic plugin bug where certain tag formats weren't being passed through 19 | 20 | ## 1.0.14 (15th January, 2013) 21 | 22 | Features: 23 | 24 | - Campfire plugin changed to use campy gem (thanks to Seth Vargo) 25 | - Organization name now added to messages when present (thanks to Seth Vargo) 26 | - Berkshelf support now added (thanks to Seth Vargo) 27 | 28 | Bugfixes: 29 | 30 | - Promote won't try to create a version diff if there is no existing remote version (thanks to Seth Vargo) 31 | 32 | ## 1.0.13 (9th January, 2013) 33 | 34 | Features: 35 | 36 | - Made spork promote Use environment_path from spork config if it's set( thanks to Greg Karékinian - https://github.com/gkarekinian) 37 | 38 | ## 1.0.12 (22nd November, 2012) 39 | Bugfixes: 40 | 41 | - Fix bug where cookbook dependancy loading broke in older chef client versions as a result of a fix in 1.0.10 42 | 43 | ## 1.0.11 (22nd November, 2012) 44 | 45 | Yanked 46 | 47 | ## 1.0.10 (22nd November, 2012) 48 | Bugfixes: 49 | 50 | - Load all cookbook versions from remote server when checking dependencies (thanks to gmcmillan) 51 | - Fix case where git plugin would update a previously loaded cookbook, resulting in out of data metadata being used. (thanks to zsol) 52 | 53 | ## 1.0.9 (28th October, 2012) 54 | Features: 55 | 56 | - Jabber Plugin (thanks to Graham McMillan - https://github.com/gmcmillan) 57 | 58 | Bugfixes: 59 | 60 | - Fix exception when spork promote called with no arguments (thanks to Julian Dunn - https://github.com/juliandunn) 61 | 62 | ## 1.0.8 (25th September, 2012) 63 | Bugfixes: 64 | 65 | - Fix whitespace warnings which occur in the git plugin under Ruby 1.8 66 | 67 | ## 1.0.7 (25th September, 2012) 68 | Bugfixes: 69 | 70 | - Fix invalid syntax in Hipchat plugin 71 | 72 | ## 1.0.6 (25th September, 2012) 73 | Bugfixes: 74 | 75 | - Fix for disabling plugins when override config files are present 76 | 77 | ## 1.0.5 (24th September, 2012) 78 | Bugfixes: 79 | 80 | - Fixes for hipchat plugin 81 | 82 | ## 1.0.4 (14th September, 2012) 83 | Features: 84 | 85 | - Spork can now run command from any directory, not just the root of your chef repository. 86 | 87 | Bugfixes: 88 | 89 | - Fixed spork uploader to work more cleanly with 10.14.0 and greater 90 | - Spork bump will no longer throw errors when no cookbook name is specified 91 | 92 | ## 1.0.3 (10th September, 2012) 93 | Bugfixes: 94 | 95 | - Fix spork upload when using Chef 10.14.0 96 | - Optional config override for chef environment location (not documented in README until 1.0.4) 97 | 98 | ## 1.0.2 (28th August, 2012) 99 | Bugfixes: 100 | 101 | - Fix bug which caused plugin errors when no spork config file could be found 102 | 103 | ## 1.0.1 (27th August, 2012) 104 | Bugfixes: 105 | 106 | - Fix require error which broke spork on CentOS 5.6 107 | 108 | ## 1.0.0 (27th August, 2012) 109 | Features: 110 | 111 | - Major refactor (initial refactor: Seth Vargo) 112 | - Plugin API (Seth Vargo) 113 | - Added "spork info" command to show config hash and plugins status 114 | - Missing local / remote cookbook now handled nicely in spork check 115 | - Add "--fail" option to spork check to "exit 1" if check fails 116 | - Git plugin now uses git gem instead of shelling out 117 | - Confirmation check on promote if version jumps more than version_change_threshold 118 | - Thanks also to jperry, bethanybenzur and nickmarden for contributions submitted pre-refactor which have been included in one form or another. 119 | 120 | 121 | 122 | ## 0.1.11 (5th June, 2012) 123 | Features: 124 | 125 | - Hipchat Support (courtesy of Chris Ferry: @cdferry) 126 | 127 | Bugfixes: 128 | 129 | - Tweaks to spork bump to play nicely with x.x versions as well as x.x.x (courtesy of Russ Garrett: @russss) 130 | 131 | ## 0.1.10 (12th April, 2012) 132 | Features: 133 | 134 | - All spork plugins now support multiple cookbook paths 135 | 136 | Bugfixes: 137 | 138 | - Fixes to work with app_conf 0.4.0 139 | 140 | ## 0.1.9 (3rd April, 2012) 141 | 142 | Features: 143 | 144 | - Spork Promote will now git add updated environment files if git is enabled 145 | - Spork Promote will optionally revert local changes to environment files if multiple changes were detected. 146 | - Spork Bump will now perform a git pull and pull from submodules if git is enabled 147 | - Optional Foodcritic integration added for Spork Upload 148 | - ickymettle's Eventinator service now optionally supported 149 | 150 | Bugfixes: 151 | 152 | - Correct irccat alerts to not fire if cookbook upload fails 153 | - Code cleanup to remove unused Opscode code from Spork Upload 154 | 155 | ## 0.1.8 (21st February, 2012) 156 | 157 | Features: 158 | 159 | - Make promote --remote check if the correct version of the cookbook has been uploaded before updating the remote environment 160 | 161 | ## 0.1.7 (21st February, 2012) 162 | 163 | Bugfixes: 164 | 165 | - Make promote --remote work nicely when not run from chef repo root 166 | 167 | ## 0.1.6 (21st February, 2012) 168 | Features: 169 | 170 | - Spork Bump now defaults to "patch" if bump level not specified 171 | - Spork Promote will prompt for confirmation if you're about to promote --remote changes to any cookbooks *other* than the one you specified on the command line. This should help avoid accidentally over-writing someone elses changes. 172 | - Irccat messages now support multiple channels 173 | - During promote, if git pull fails, ie a merge conflict has arisen, the error will be shown and promote will exit. 174 | - Spork Promote will now also update git submodules before promoting. Specifically, it will run "git submodule foreach git pull" 175 | - Failures during "git add" on spork bumps have a more helpful error message 176 | - Irccat messages are now more nicely formatted and have pretty colours. 177 | 178 | Bugfixes: 179 | 180 | - Spork Promote will now work from anywhere in your chef repo, not just the repo root 181 | 182 | ## 0.1.5 (21st February, 2012) 183 | 184 | Yanked 185 | 186 | ## 0.1.4 (3rd February, 2012) 187 | 188 | Features: 189 | 190 | - Spork Check only show the last 5 remote versions, include the --all option if you want to see them all 191 | - Spork will no longer work with Ruby 1.8. If you're on that version, the plugin will bail immediately. 192 | - Spork now support updating a graphite metric when promote --remote is run 193 | - Spork now supports alerting using irccat when a cookbook upload or promote --remote happens 194 | - It will also optionally post a gist of version constraint changes in the above message when a promote --remote happens 195 | - Added support for default environments to promote to 196 | - knife-spork gemification thanks to Daniel Schauenberg 197 | 198 | Bugfixes: 199 | 200 | - Various bugfixes and tweaks to formatting and log messages 201 | 202 | ## 0.1.4 (3rd February, 2012) 203 | 204 | Yanked 205 | 206 | ## 0.1.3 (3rd February, 2012) 207 | 208 | Yanked 209 | 210 | ## 0.1.2 (3rd February, 2012) 211 | 212 | Yanked 213 | 214 | ## 0.1.1 (3rd February, 2012) 215 | 216 | Yanked 217 | 218 | ## 0.1.0 (January 28, 2012) 219 | 220 | Initial version. 221 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | KnifeSpork 2 | =========== 3 | KnifeSpork is a workflow plugin for `Chef::Knife` which helps multiple developers work on the same Chef Server and repository without treading on each other's toes. This plugin was designed around the workflow we have here at Etsy, where several people are working on the Chef repository and Chef Server simultaneously. It contains several functions, documented below: 4 | 5 | Installation 6 | ------------ 7 | ### Gem Install (recommended) 8 | `knife-spork` is available on rubygems. Add the following to your `Gemfile`: 9 | 10 | ```ruby 11 | gem 'knife-spork' 12 | ``` 13 | 14 | or install the gem manually: 15 | 16 | ```bash 17 | gem install knife-spork 18 | ``` 19 | 20 | ### Plugin Install 21 | Copy spork-* script from lib/chef/knife/spork-*.rb to your ~/.chef/plugins/knife directory. 22 | 23 | Spork Configuration 24 | ------------------- 25 | Out of the box, knife spork will work with no configuration. However, you can optionally enable several features to enhance its functionality. 26 | 27 | KnifeSpork will look for a configuration file in the following locations, in ascending order of precedence: 28 | 29 | - `config/spork-config.yml` 30 | - `/etc/spork-config.yml` 31 | - `~/.chef/spork-config.yml` 32 | 33 | Anything set in the configuration file in your home directory for example, will override options set in your Chef repository or `/etc`. 34 | 35 | Below is a sample config file with all supported options and all shipped plugins enabled below, followed by an explanation of each section. 36 | 37 | ```yaml 38 | default_environments: 39 | - development 40 | - production 41 | version_change_threshold: 2 42 | environment_path: "/home/me/environments" 43 | plugins: 44 | campfire: 45 | account: myaccount 46 | token: a1b2c3d4... 47 | hipchat: 48 | api_token: ABC123 49 | rooms: 50 | - General 51 | - Web Operations 52 | notify: true 53 | color: yellow 54 | jabber: 55 | username: YOURUSER 56 | password: YOURPASSWORD 57 | nickname: Chef Bot 58 | server_name: your.jabberserver.com 59 | server_port: 5222 60 | rooms: 61 | - engineering@your.conference.com/spork 62 | - systems@your.conference.com/spork 63 | git: 64 | enabled: true 65 | irccat: 66 | server: irccat.mydomain.com 67 | port: 12345 68 | gist: "/usr/bin/gist" 69 | channel: ["chef-annoucements"] 70 | graphite: 71 | server: graphite.mydomain.com 72 | port: 2003 73 | eventinator: 74 | url: http://eventinator.mydomain.com/events/oneshot 75 | ``` 76 | 77 | #### Default Environments 78 | The `default_environments` directive allows you to specify a default list of environments you want to promote changes to. If this option is configured and you *omit* the environment parameter when promoting KnifeSpork will promote to all environments in this list. 79 | 80 | #### Version Change Threshold 81 | The `version_change_threshold` directive allows you to customise the threshold used by a safety check in spork promote which will prompt for confirmation if you're promoting a cookbook by more than version_change_threshold versions. This defaults to 2 if not set, ie promoting a cookbook from v1.0.1 to v 1.0.2 will not trip this check, wheras promoting from v1.0.1 to v1.0.3 will. 82 | 83 | #### Environment Path 84 | The `environment_path` allows you to specify the path to where you store your chef environment json files. If this parameter is not specified, spork will default to using the first element of your cookbook_path, replacing the word "cookbooks" with "environments" 85 | 86 | #### Plugins 87 | Knife spork supports plugins to allow users to hook it into existing systems such as source control, monitoring and chat systems. Plugins are enabled / disabled by adding / removing their config block from the plugin section of the config file. Any of the default plugins shown above can be disabled by removing their section. 88 | 89 | For more information on how to develop plugins for spork, please read the [plugins/README.md](plugins/README.md) file. 90 | 91 | Spork Info 92 | ----------- 93 | This function is designed to help you see which plugins you currently have loaded, and the current config Hash which knife spork is using. 94 | 95 | #### Usage 96 | ```bash 97 | knife spork info 98 | ``` 99 | 100 | #### Example 101 | 102 | ```text 103 | $ knife spork info 104 | Config Hash: 105 | {"plugins"=>{"git"=>{"enabled"=>true}, "irccat"=>{"server"=>"irccat.mydomain.com", "port"=>12345, "gist"=>"usr/bin/gist", "channel"=>["#chef-announce"]}, "graphite"=>{"server"=>"graphite.mydomain.com", "port"=>2003}, "eventinator"=>{"url"=>"http://eventinator.mydomain.com/events/oneshot"}}, "default_environments"=>["development", "production"], "version_change_threshold"=>2, "pplugins"=>{"foodcritic"=>{"fail_tags"=>["style,correctness,test"], "tags"=>["~portability"], "include_rules"=>["config/rules.rb"]}}} 106 | 107 | Plugins: 108 | KnifeSpork::Plugins::Campfire: disabled 109 | KnifeSpork::Plugins::Eventinator: enabled 110 | KnifeSpork::Plugins::Foodcritic: disabled 111 | KnifeSpork::Plugins::Git: enabled 112 | KnifeSpork::Plugins::Graphite: enabled 113 | KnifeSpork::Plugins::HipChat: disabled 114 | KnifeSpork::Plugins::Irccat: enabled 115 | ``` 116 | 117 | Spork Check 118 | ----------- 119 | This function is designed to help you avoid trampling on other people's cookbook versions, and to make sure that when you come to version your own work it's easy to see what version numbers have already been used and if the one you're using will overwrite anything. 120 | 121 | #### Usage 122 | ```bash 123 | knife spork check COOKBOOK [--all] 124 | ``` 125 | 126 | By default, spork check only shows the 5 most recent remote cookbook versions. Add the --all option if you want to see everything. 127 | 128 | #### Example (Checking an Unfrozen Cookbook with version clash) 129 | 130 | ```text 131 | $ knife spork check apache2 132 | Checking versions for cookbook apache2... 133 | 134 | Local Version: 135 | 1.1.49 136 | 137 | Remote Versions: (* indicates frozen) 138 | *2.0.2 139 | *2.0.1 140 | 1.1.49 141 | *1.1.14 142 | *1.1.13 143 | 144 | ERROR: The version 1.1.49 exists on the server and is not frozen. Uploading will overwrite! 145 | ``` 146 | 147 | #### Example (Checking a Frozen Cookbook with version clash) 148 | 149 | ```text 150 | $ knife spork check apache2 151 | Checking versions for cookbook apache2... 152 | 153 | Local Version: 154 | 2.0.2 155 | 156 | Remote Versions: (* indicates frozen) 157 | *2.0.2 158 | *2.0.1 159 | 1.1.49 160 | *1.1.14 161 | *1.1.13 162 | 163 | WARNING: Your local version (2.0.2) is frozen on the remote server. You'll need to bump before you can upload. 164 | ```` 165 | 166 | #### Example (No version clashes) 167 | 168 | ```text 169 | $ knife spork check apache2 170 | Checking versions for cookbook apache2... 171 | 172 | Local Version: 173 | 2.0.3 174 | 175 | Remote Versions: (* indicates frozen) 176 | *2.0.2 177 | *2.0.1 178 | 1.1.49 179 | *1.1.14 180 | *1.1.13 181 | 182 | Everything looks good! 183 | ``` 184 | 185 | Spork Bump 186 | ---------- 187 | This function lets you easily version your cookbooks without having to manually edit the cookbook's `metadata.rb` file. You can either specify the version level you'd like to bump (`major`, `minor`, or `patch`), or you can manually specify a version number. This might be used if, for example, you want to jump several version numbers in one go and don't want to have to run knife bump once for each number. If no bump level is specified, a patch level bump will be performed. 188 | 189 | #### Usage 190 | ```bash 191 | knife spork bump COOKBOOK [MAJOR | MINOR | PATCH | MANUAL x.x.x] 192 | ```` 193 | 194 | #### Example (No patch level specified - defaulting to patch) 195 | ```text 196 | $ knife spork bump apache2 197 | Successfully bumped apache2 to v2.0.4! 198 | 199 | #### Example (Bumping patch level) 200 | ```text 201 | $ knife spork bump apache2 patch 202 | Successfully bumped apache2 to v2.0.4! 203 | ```` 204 | 205 | #### Example (Manually setting version) 206 | ```text 207 | $ knife spork bump apache2 manual 1.0.13 208 | Successfully bumped apache2 to v1.0.13! 209 | ``` 210 | 211 | Spork Upload 212 | ------------ 213 | This function works mostly the same as normal `knife cookbook upload COOKBOOK` except that this automatically freezes cookbooks when you upload them. 214 | 215 | #### Usage 216 | ```bash 217 | knife spork upload COOKBOOK 218 | ``` 219 | #### Example 220 | ```text 221 | $ knife spork upload apache2 222 | Freezing apache2 at 1.0.13... 223 | Successfully uploaded apache2@1.0.13! 224 | ``` 225 | 226 | Spork Promote 227 | ------------- 228 | This function lets you easily set a version constraint in an environment for a particular cookbook. By default it will set the version constraint to whatever the local version of the specified cookbook is. Optionally, you can include a `--version` option which will set the version constraint for the specified cookbook to whatever version number you provide. You might want to use this if, for example, you pushed a version constraint for a cookbook version you don't want your nodes to use anymore, so you want to "roll back" the environment to a previous version. You can also specify the `--remote` option if you'd like to automatically upload your changed local environment file to the server. 229 | 230 | If you don't specify an environment, the default_environments config directive will be used if set. 231 | 232 | #### Usage 233 | 234 | ```bash 235 | knife spork promote [ENVIRONMENT] COOKBOOK [--version, --remote] 236 | ``` 237 | 238 | #### Example (Using local cookbook version number) 239 | 240 | ```text 241 | $ knife spork promote my_environment apache2 --remote 242 | Adding version constraint apache2 = 1.0.13 243 | Saving changes to my_environment.json 244 | Uploading my_environment to Chef Server 245 | Promotion complete! 246 | ``` 247 | 248 | #### Example (Using manual version) 249 | ```text 250 | $ knife spork promote my_environment apache2 -v 2.0.2 251 | Adding version constraint apache2 = 2.0.2 252 | Saving changes to my_environment.json 253 | Promotion complete. Don't forget to upload your changed my_environment to Chef Server 254 | ``` 255 | --------------------------------------------------------------------------------