├── .gitignore ├── Gemfile ├── README.md ├── Rakefile ├── knife-flow.gemspec └── lib ├── chef └── knife │ ├── increment.rb │ ├── promote.rb │ └── release.rb └── knife-flow └── version.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in knife-flow.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | knife-flow 2 | ======== 3 | A collection of Chef plugins for managing the migration of cookbooks to environments in different Opscode organizations. 4 | The main reason for having a workflow around the development and promotion of cookbooks is to ensure quality, reliability and administrative security of the process. 5 | 6 | Requirements 7 | --------------- 8 | Right now knife-flow is build with many assumptions: 9 | 10 | * The knife-flow assumes you have at least 2 orgs; one for "development" and one for "production". 11 | * The "development" org has one environment called "candidate". 12 | * The "production" org has an "innovate" and a "production" environment. 13 | * You are using git flow [http://jeffkreeftmeijer.com/2010/why-arent-you-using-git-flow/](http://jeffkreeftmeijer.com/2010/why-arent-you-using-git-flow/) for your chef-repo project. 14 | 15 | Installing knife-flow 16 | ------------------- 17 | Be sure you are running the latest version Chef. 18 | 19 | gem install knife-flow 20 | 21 | If you are a production level administrator: map the "development" org to the knife.rb file, and the "production" org to a knife-production.rb file. 22 | All other developers just need the regurlar "development" org to the knife.rb file mapping. 23 | 24 | 25 | Plugins 26 | --------------- 27 | 28 | ### increment 29 | Increments the cookbooks version by 1 digit at the patch level (i.e. 2.3.1 -> 2.3.2 )
30 | Uploads the cookbook by running knife cookbook upload COOKBOOK COOKBOOK
31 | Commits the changes to the "develop" branch
32 | 33 | 34 | knife increment COOKBOOK COOKBOOK ... 35 | 36 | 37 | This plugin is useful when working on the projects in the "sandbox" stage. The "_default" environment will always load the latest versions of the cookbooks. 38 | 39 | 40 | ### promote 41 | Increments the cookbooks version by 1 digit at the patch level ( i.e. 2.3.1 -> 2.3.2 )
42 | Uploads the cookbook by running knife cookbook upload COOKBOOK COOKBOOK
43 | Updates the environments/ENVIRONMENT.json file with the list of COOKBOOK COOKBOOK and relative new versions.
44 | Uploads the ENVIRONMENT.json file to the "development" org.
45 | Commits the changes to the "develop" branch.
46 | 47 | 48 | knife promote ENVIRONMENT(i.e. candidate) COOKBOOK COOKBOOK ... 49 | 50 | 51 | This plugin is useful when working on the projects in the "validation" and "performance" stage. The "candidate" environment will be used to validate the cookbooks versions. 52 | 53 | 54 | ### release 55 | Copies the "candidate" environment cookbook list and transfer them to the ENVIRONMENT in the "production" org.
56 | Commits all changes and creates a release tag TAG using the git flow release start/finish TAG .
57 | Uploads all cookbooks to the "production" org.
58 | 59 | knife release ENVIRONMENT(i.e. innovate or production) TAG(i.e. 2011.2.3) 60 | 61 | This plugin is useful when we are ready to migrate the cookbooks to the environments in the "production" org. 62 | 63 | License terms 64 | ------------- 65 | Authors:: Johnlouis Petitbon, Jacob Zimmerman, Aaron Suggs 66 | 67 | Copyright:: Copyright (c) 2009-2011 Medidata Solutions Worldwide, Inc. 68 | 69 | License:: Apache License, Version 2.0 70 | 71 | 72 | Licensed under the Apache License, Version 2.0 (the "License"); 73 | you may not use this file except in compliance with the License. 74 | You may obtain a copy of the License at 75 | 76 | http://www.apache.org/licenses/LICENSE-2.0 77 | 78 | Unless required by applicable law or agreed to in writing, software 79 | distributed under the License is distributed on an "AS IS" BASIS, 80 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 81 | See the License for the specific language governing permissions and 82 | limitations under the License. 83 | 84 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | -------------------------------------------------------------------------------- /knife-flow.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "knife-flow/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "knife-flow" 7 | s.version = Knife::Flow::VERSION 8 | s.authors = ["Johnlouis Petitbon", "Jacob Zimmerman", "Aaron Suggs"] 9 | s.email = ["jpetitbon@mdsol.com"] 10 | s.homepage = "https://github.com/mdsol/knife-flow" 11 | s.summary = %q{A collection of Chef plugins for managing the migration of cookbooks to environments in different Opscode organizations.} 12 | s.description = %q{The main reason for having a workflow around the development and promotion of cookbooks is to ensure quality, reliability and administrative security of the process.} 13 | 14 | s.rubyforge_project = "knife-flow" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | end 21 | -------------------------------------------------------------------------------- /lib/chef/knife/increment.rb: -------------------------------------------------------------------------------- 1 | # 2 | ## Author:: Johnlouis Petitbon () 3 | ## 4 | ## Licensed under the Apache License, Version 2.0 (the "License"); 5 | ## you may not use this file except in compliance with the License. 6 | ## You may obtain a copy of the License at 7 | ## 8 | ## http://www.apache.org/licenses/LICENSE-2.0 9 | ## 10 | ## Unless required by applicable law or agreed to in writing, software 11 | ## distributed under the License is distributed on an "AS IS" BASIS, 12 | ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ## See the License for the specific language governing permissions and 14 | ## limitations under the License. 15 | ## 16 | # 17 | 18 | require 'chef/knife' 19 | 20 | module KnifeFlow 21 | class Increment < Chef::Knife 22 | 23 | deps do 24 | require 'chef/cookbook_loader' 25 | require 'chef/cookbook_uploader' 26 | end 27 | 28 | banner "knife increment COOKBOOK" 29 | 30 | WORKING_BRANCH = "develop" 31 | 32 | def run 33 | 34 | @cookbooks = parse_name_args! 35 | 36 | self.config = Chef::Config.merge!(config) 37 | 38 | if !config[:cookbook_path] 39 | raise ArgumentError, "Default cookbook_path is not specified in the knife.rb config file, and a value to -o is not provided. Nowhere to write the new cookbook to." 40 | end 41 | 42 | if check_branch(WORKING_BRANCH) 43 | 44 | pull_branch(WORKING_BRANCH) 45 | 46 | @cookbook_path = Array(config[:cookbook_path]).first 47 | 48 | @cookbooks.each do | book | 49 | metadata_file = File.join(@cookbook_path, book, "metadata.rb") 50 | 51 | # 1) increase version on the metadata file 52 | replace_version(find_version(book), increment_version(find_version(book)), metadata_file ) 53 | 54 | end 55 | 56 | # 2) upload cookbooks to chef server 57 | cookbook_up = Chef::Knife::CookbookUpload.new 58 | cookbook_up.name_args = @cookbooks 59 | cookbook_up.config[:freeze] = true 60 | cookbook_up.run 61 | 62 | # 3) commit and push WORKING_BRANCH 63 | commit_and_push_branch(WORKING_BRANCH, "#{@cookbooks} have been incremented") 64 | 65 | end 66 | 67 | end 68 | 69 | def parse_name_args! 70 | if name_args.empty? 71 | ui.error("USAGE: knife increment COOKBOOK COOKBOOK COOKBOOK") 72 | exit 1 73 | else 74 | return name_args 75 | end 76 | end 77 | 78 | def commit_and_push_branch(branch, comment) 79 | print "--------------------------------- \n" 80 | system("git pull origin #{branch}") 81 | system("git add .") 82 | system("git commit -am '#{comment}'") 83 | system("git push origin #{branch}") 84 | print "--------------------------------- \n" 85 | end 86 | 87 | def pull_branch(name) 88 | print "--------------------------------- \n" 89 | system("git pull origin #{name}") 90 | print "--------------------------------- \n" 91 | end 92 | 93 | def check_branch(name) 94 | if (`git status` =~ /#{name}/) != nil 95 | return true 96 | else 97 | ui.error("USAGE: you must be in the #{name} branch") 98 | exit 1 99 | end 100 | end 101 | 102 | def find_version(name) 103 | loader = Chef::CookbookLoader.new(@cookbook_path) 104 | return loader[name].version 105 | end 106 | 107 | def increment_version(version) 108 | current_version = version.split(".").map{|i| i.to_i} 109 | current_version[2] = current_version[2] + 1 110 | return current_version.join('.') 111 | end 112 | 113 | def replace_version(search_string, replace_string, file) 114 | open_file = File.open(file, "r") 115 | body_of_file = open_file.read 116 | open_file.close 117 | body_of_file.gsub!(search_string, replace_string) 118 | File.open(file, "w") { |file| file << body_of_file } 119 | end 120 | 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/chef/knife/promote.rb: -------------------------------------------------------------------------------- 1 | # 2 | ## Author:: Johnlouis Petitbon () 3 | ## 4 | ## Licensed under the Apache License, Version 2.0 (the "License"); 5 | ## you may not use this file except in compliance with the License. 6 | ## You may obtain a copy of the License at 7 | ## 8 | ## http://www.apache.org/licenses/LICENSE-2.0 9 | ## 10 | ## Unless required by applicable law or agreed to in writing, software 11 | ## distributed under the License is distributed on an "AS IS" BASIS, 12 | ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ## See the License for the specific language governing permissions and 14 | ## limitations under the License. 15 | ## 16 | # 17 | 18 | require 'chef/knife' 19 | 20 | module KnifeFlow 21 | class Promote < Chef::Knife 22 | 23 | deps do 24 | require 'chef/cookbook_loader' 25 | require 'chef/cookbook_uploader' 26 | require 'chef/environment' 27 | require 'chef/knife/core/object_loader' 28 | end 29 | 30 | banner "knife promote ENVIRONMENT COOKBOOK" 31 | 32 | WORKING_BRANCH = "develop" 33 | 34 | def run 35 | 36 | 37 | all_args = parse_name_args! 38 | env_name = all_args[0] 39 | all_args.shift 40 | cookbooks = all_args 41 | 42 | self.config = Chef::Config.merge!(config) 43 | 44 | if !config[:cookbook_path] 45 | raise ArgumentError, "Default cookbook_path is not specified in the knife.rb config file, and a value to -o is not provided. Nowhere to write the new cookbook to." 46 | end 47 | @cookbook_path = Array(config[:cookbook_path]).first 48 | 49 | 50 | if check_branch(WORKING_BRANCH) 51 | 52 | pull_branch(WORKING_BRANCH) 53 | 54 | env_json = load_env_file(env_name) 55 | 56 | env_data = JSON.parse(env_json) 57 | 58 | cookbooks.each do | book | 59 | metadata_file = File.join(@cookbook_path, book, "metadata.rb") 60 | 61 | # 1) increase version on the metadata file 62 | replace_version(find_version(book), increment_version(find_version(book)), metadata_file ) 63 | 64 | # 2) add or update the cookbook in the environment cookbook_versions list 65 | env_data.cookbook_versions.merge!(book => find_version(book)) 66 | 67 | end 68 | 69 | # 3) write the environment to file 70 | File.open("environments/#{env_name}.json","w") do |f| 71 | f.write(JSON.pretty_generate(env_data)) 72 | end 73 | 74 | # 4) upload cookbooks to chef server 75 | cookbook_up = Chef::Knife::CookbookUpload.new 76 | cookbook_up.name_args = cookbooks 77 | cookbook_up.config[:freeze] = true 78 | cookbook_up.run 79 | 80 | 81 | # 5) upload environment to chef server 82 | knife_environment_from_file = Chef::Knife::EnvironmentFromFile.new 83 | knife_environment_from_file.name_args = ["#{env_name}.json"] 84 | output = knife_environment_from_file.run 85 | 86 | # 6) commit and push all changes to develop 87 | commit_and_push_branch(WORKING_BRANCH, "#{cookbooks.join(" and ").to_s} have been promoted to the #{env_name} environment") 88 | 89 | end 90 | 91 | end 92 | 93 | def load_env_file(env_name) 94 | if File.exist?("environments/#{env_name}.json") 95 | File.read("environments/#{env_name}.json") 96 | else 97 | # TODO - we should handle the creation of the environment.json file if it doesn't exist. 98 | raise ArgumentError, "environments/#{env_name}.json was not found; please create the environment file manually.#{env_name}" 99 | end 100 | end 101 | 102 | def commit_and_push_branch(branch, comment) 103 | print "--------------------------------- \n" 104 | system("git pull origin #{branch}") 105 | system("git add .") 106 | system("git commit -am '#{comment}'") 107 | system("git push origin #{branch}") 108 | print "--------------------------------- \n" 109 | end 110 | 111 | def pull_branch(name) 112 | print "--------------------------------- \n" 113 | system("git pull origin #{name}") 114 | print "--------------------------------- \n" 115 | end 116 | 117 | def check_branch(name) 118 | if (`git status` =~ /#{name}/) != nil 119 | return true 120 | else 121 | ui.error("USAGE: you must be in the #{name} branch.") 122 | exit 1 123 | end 124 | end 125 | 126 | def parse_name_args! 127 | if name_args.empty? 128 | ui.error("USAGE: knife promote ENVIRONMENT COOKBOOK COOKBOOK ...") 129 | exit 1 130 | else 131 | return name_args 132 | end 133 | end 134 | 135 | def find_version(name) 136 | loader = Chef::CookbookLoader.new(@cookbook_path) 137 | return loader[name].version 138 | end 139 | 140 | def increment_version(version) 141 | current_version = version.split(".").map{|i| i.to_i} 142 | current_version[2] = current_version[2] + 1 143 | return current_version.join('.') 144 | end 145 | 146 | def replace_version(search_string, replace_string, file) 147 | open_file = File.open(file, "r") 148 | body_of_file = open_file.read 149 | open_file.close 150 | body_of_file.gsub!(search_string, replace_string) 151 | File.open(file, "w") { |file| file << body_of_file } 152 | end 153 | 154 | 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /lib/chef/knife/release.rb: -------------------------------------------------------------------------------- 1 | # 2 | ## Author:: Johnlouis Petitbon () 3 | ## 4 | ## Licensed under the Apache License, Version 2.0 (the "License"); 5 | ## you may not use this file except in compliance with the License. 6 | ## You may obtain a copy of the License at 7 | ## 8 | ## http://www.apache.org/licenses/LICENSE-2.0 9 | ## 10 | ## Unless required by applicable law or agreed to in writing, software 11 | ## distributed under the License is distributed on an "AS IS" BASIS, 12 | ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ## See the License for the specific language governing permissions and 14 | ## limitations under the License. 15 | ## 16 | # 17 | 18 | require 'chef/knife' 19 | 20 | module KnifeFlow 21 | class Release < Chef::Knife 22 | 23 | deps do 24 | require 'chef/cookbook_loader' 25 | require 'chef/cookbook_uploader' 26 | require 'chef/environment' 27 | require 'chef/knife/core/object_loader' 28 | end 29 | 30 | banner "knife release ENVIRONMENT TAG" 31 | 32 | WORKING_BRANCH = "develop" 33 | CANDIDATE_ENVIRONMENT = "candidate" 34 | 35 | def run 36 | 37 | all_args = parse_name_args! 38 | env_name = all_args[0] 39 | tag_name = all_args[1] 40 | 41 | self.config = Chef::Config.merge!(config) 42 | 43 | switch_org(env_name) 44 | 45 | self.config = Chef::Config.merge!(config) 46 | 47 | if !config[:cookbook_path] 48 | raise ArgumentError, "Default cookbook_path is not specified in the knife.rb config file, and a value to -o is not provided. Nowhere to write the new cookbook to." 49 | end 50 | @cookbook_path = Array(config[:cookbook_path]).first 51 | 52 | if check_branch(WORKING_BRANCH) 53 | 54 | pull_branch(WORKING_BRANCH) 55 | 56 | system("git fetch --tags") 57 | 58 | # 1) start a new git-flow release 59 | system("git flow release start #{tag_name}") 60 | 61 | candidate_json = load_env_file(CANDIDATE_ENVIRONMENT) 62 | candidate_data = JSON.parse(candidate_json) 63 | 64 | env_json = load_env_file(env_name) 65 | env_data = JSON.parse(env_json) 66 | 67 | cb_a = [] 68 | candidate_data.cookbook_versions.each do | book_data | 69 | cb_a << book_data[0] 70 | 71 | # 2) add or update the cookbook in the environment cookbook_versions list 72 | env_data.cookbook_versions.merge!(book_data[0] => book_data[1]) 73 | 74 | end 75 | 76 | # 3) write the environment to file 77 | File.open("environments/#{env_name}.json","w") do |f| 78 | f.write(JSON.pretty_generate(env_data)) 79 | end 80 | 81 | # 4) upload cookbooks to chef server 82 | cookbook_up = Chef::Knife::CookbookUpload.new 83 | cookbook_up.name_args = cb_a 84 | cookbook_up.config[:freeze] = true 85 | cookbook_up.run 86 | 87 | # 5) upload environment to chef server 88 | knife_environment_from_file = Chef::Knife::EnvironmentFromFile.new 89 | knife_environment_from_file.name_args = ["#{env_name}.json"] 90 | output = knife_environment_from_file.run 91 | 92 | # 6) commit all changes and finish the git-flow release 93 | system("git commit -am 'the candidate environemnt is now in production and tagged #{tag_name}'") 94 | system("git flow release finish -m 'testing' #{tag_name}") 95 | 96 | system("git push origin #{WORKING_BRANCH} --tags") 97 | 98 | end 99 | 100 | end 101 | 102 | def switch_org(env_name) 103 | # TODO - someone smarter than me can switch the organization without requiring 2 different knife.rb files 104 | current_dir = File.dirname(__FILE__) 105 | case env_name 106 | when "innovate", "production" 107 | Chef::Config[:config_file] = File.join File.expand_path('.chef'), "knife-production.rb" 108 | when "candidate" 109 | Chef::Config[:config_file] = File.join File.expand_path('.chef'), "knife.rb" 110 | end 111 | ::File::open(config[:config_file]) { |f| apply_config(f.path) } 112 | end 113 | 114 | def load_env_file(env_name) 115 | if File.exist?("environments/#{env_name}.json") 116 | File.read("environments/#{env_name}.json") 117 | else 118 | # TODO - we should handle the creation of the environment.json file if it doesn't exist. 119 | raise ArgumentError, "environments/#{env_name}.json was not found; please create the environment file manually.#{env_name}" 120 | end 121 | end 122 | 123 | def apply_config(config_file_path) 124 | Chef::Config.from_file(config_file_path) 125 | Chef::Config.merge!(config) 126 | end 127 | 128 | def commit_and_push_branch(branch, comment) 129 | print "--------------------------------- \n" 130 | system("git pull origin #{branch}") 131 | system("git add .") 132 | system("git commit -am '#{comment}'") 133 | system("git push origin #{branch}") 134 | print "--------------------------------- \n" 135 | end 136 | 137 | def checkout_branch(name) 138 | print "--------------------------------- \n" 139 | system("git checkout #{name}") 140 | print "--------------------------------- \n" 141 | end 142 | 143 | def checkout_tag(name) 144 | print "--------------------------------- \n" 145 | system("git checkout #{name}") 146 | print "--------------------------------- \n" 147 | end 148 | 149 | def pull_branch(name) 150 | print "--------------------------------- \n" 151 | system("git pull origin #{name}") 152 | print "--------------------------------- \n" 153 | end 154 | 155 | def check_branch(name) 156 | if (`git status` =~ /#{name}/) != nil 157 | return true 158 | else 159 | ui.error("USAGE: you must be in the #{name} branch.") 160 | exit 1 161 | end 162 | end 163 | 164 | def parse_name_args! 165 | if name_args.empty? 166 | ui.error("USAGE: knife release ENVIRONMENT TAG") 167 | exit 1 168 | else 169 | return name_args 170 | end 171 | end 172 | 173 | def find_version(name) 174 | loader = Chef::CookbookLoader.new(@cookbook_path) 175 | return loader[name].version 176 | end 177 | 178 | def increment_version(version) 179 | current_version = version.split(".").map{|i| i.to_i} 180 | current_version[2] = current_version[2] + 1 181 | return current_version.join('.') 182 | end 183 | 184 | def replace_version(search_string, replace_string, file) 185 | open_file = File.open(file, "r") 186 | body_of_file = open_file.read 187 | open_file.close 188 | body_of_file.gsub!(search_string, replace_string) 189 | File.open(file, "w") { |file| file << body_of_file } 190 | end 191 | 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /lib/knife-flow/version.rb: -------------------------------------------------------------------------------- 1 | module Knife 2 | module Flow 3 | VERSION = "0.0.4" 4 | end 5 | end 6 | --------------------------------------------------------------------------------