├── .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 |
--------------------------------------------------------------------------------