knife-spork 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # knife-spork
2
+
3
+ A workflow plugin for Chef::Knife which helps multiple devs work on the same chef server and repo without treading on eachothers toes. This plugin was designed around the workflow we have here at Etsy, where several people are working on the chef repo and chef server at the same time. It contains several functions, documented below:
4
+
5
+ # Spork Check
6
+
7
+ 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.
8
+
9
+ ## Usage
10
+
11
+ ````
12
+ knife spork check COOKBOOK
13
+ ````
14
+
15
+ ## Example (Checking an Unfrozen Cookbook with version clash)
16
+
17
+ ````
18
+ $ knife spork check apache
19
+ Checking versions for cookbook apache...
20
+
21
+ Current local version: 0.1.0
22
+
23
+ Remote versions:
24
+ *0.1.0, unfrozen
25
+ 0.0.0, unfrozen
26
+
27
+ DANGER: Your local cookbook version number clashes with an unfrozen remote version.
28
+
29
+ If you upload now, you'll overwrite it.
30
+ ````
31
+
32
+ ## Example (Checking a Frozen Cookbook with version clash)
33
+
34
+ ````
35
+ $ knife spork check apache2
36
+ Checking versions for cookbook apache2...
37
+
38
+ Current local version: 1.0.6
39
+
40
+ Remote versions:
41
+ *1.0.6, frozen
42
+ 1.0.5, frozen
43
+ 1.0.4, frozen
44
+ 1.0.3, frozen
45
+ 1.0.2, frozen
46
+ 1.0.1, frozen
47
+ 1.0.0, frozen
48
+
49
+ DANGER: Your local cookbook has same version number as the starred version above!
50
+
51
+ Please bump your local version or you won't be able to upload.
52
+ ````
53
+
54
+ ## Example (No version clashes)
55
+
56
+ ````
57
+ $ knife spork check apache2
58
+ Checking versions for cookbook apache2...
59
+
60
+ Current local version: 1.0.7
61
+
62
+ Remote versions:
63
+ 1.0.6, frozen
64
+ 1.0.5, frozen
65
+ 1.0.4, frozen
66
+ 1.0.3, frozen
67
+ 1.0.2, frozen
68
+ 1.0.1, frozen
69
+ 1.0.0, frozen
70
+
71
+ Everything looks fine, no version clashes. You can upload!
72
+ ````
73
+
74
+ # Spork Bump
75
+
76
+ 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.
77
+
78
+ ## Usage
79
+
80
+ ````
81
+ knife bump COOKBOOK <MAJOR | MINOR | PATCH | MANUAL x.x.x>
82
+
83
+ ````
84
+
85
+ ## Example (Bumping patch level)
86
+
87
+ ````
88
+ $ knife spork bump apache2 patch
89
+ Bumping patch level of the apache2 cookbook from 1.0.6 to 1.0.7
90
+ ````
91
+
92
+ ## Example (Manually setting version)
93
+
94
+ ````
95
+ $ knife spork bump apache2 manual 1.0.13
96
+ Manually bumped version of the apache2 cookbook from 1.0.7 to 1.0.13
97
+ ````
98
+
99
+ #Spork Upload
100
+
101
+ This function works mostly the same as normal "knife cookbook upload" except that this version automatically freezes cookbooks when you upload them. If you don't want to have to remember to add "--freeze" to your "knife cookbook upload" commands, then use this version.
102
+
103
+ ## Usage
104
+
105
+ ````
106
+ knife spork upload COOKBOOK
107
+ ````
108
+
109
+ ## Example
110
+
111
+ ````
112
+ $ knife spork upload apache
113
+
114
+ Uploading and freezing apache [1.0.6]
115
+ upload complete
116
+ ````
117
+
118
+ # Spork Promote
119
+
120
+ 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.
121
+
122
+ ## Usage
123
+
124
+ ````
125
+ knife spork promote ENVIRONMENT COOKBOOK (OPTIONS: --version, --remote)
126
+ ````
127
+
128
+ ## Example (Using local Cookbook version number, into environment "foo", uploading to chef server)
129
+
130
+ ````
131
+ $ knife spork promote foo php --remote
132
+ Adding version constraint php = 0.1.0
133
+
134
+ Saving changes into foo.json
135
+
136
+ Uploading foo to server
137
+
138
+ Promotion complete! Please remember to upload your changed Environment file to the Chef Server.
139
+ ````
140
+
141
+ ## Example (Using manual version, into environment "foo", saving to local environment file only)
142
+
143
+ ````
144
+ $ knife spork promote foo php -v 1.0.6
145
+ Adding version constraint php = 1.0.6
146
+
147
+ Saving changes into foo.json
148
+
149
+ Promotion complete! Please remember to upload your changed Environment file to the Chef Server.
150
+ ````
151
+
data/Rakefile ADDED
@@ -0,0 +1,122 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'date'
4
+
5
+ #############################################################################
6
+ #
7
+ # Helper functions
8
+ #
9
+ #############################################################################
10
+
11
+ def name
12
+ @name ||= Dir['*.gemspec'].first.split('.').first
13
+ end
14
+
15
+ def version
16
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
17
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
18
+ end
19
+
20
+ def date
21
+ Date.today.to_s
22
+ end
23
+
24
+ def rubyforge_project
25
+ name
26
+ end
27
+
28
+ def gemspec_file
29
+ "#{name}.gemspec"
30
+ end
31
+
32
+ def gem_file
33
+ "#{name}-#{version}.gem"
34
+ end
35
+
36
+ def replace_header(head, header_name)
37
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
38
+ end
39
+
40
+ #############################################################################
41
+ #
42
+ # Standard tasks
43
+ #
44
+ #############################################################################
45
+
46
+ task :default => :validate
47
+
48
+ desc "Open an irb session preloaded with this library"
49
+ task :console do
50
+ sh "irb -rubygems -r ./lib/#{name}.rb"
51
+ end
52
+
53
+ #############################################################################
54
+ #
55
+ # Custom tasks (add your own tasks here)
56
+ #
57
+ #############################################################################
58
+
59
+
60
+
61
+ #############################################################################
62
+ #
63
+ # Packaging tasks
64
+ #
65
+ #############################################################################
66
+
67
+ desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
68
+ task :release => :build do
69
+ unless `git branch` =~ /^\* master$/
70
+ puts "You must be on the master branch to release!"
71
+ exit!
72
+ end
73
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
74
+ sh "git tag v#{version}"
75
+ sh "git push origin master"
76
+ sh "git push origin v#{version}"
77
+ sh "gem push pkg/#{name}-#{version}.gem"
78
+ end
79
+
80
+ desc "Build #{gem_file} into the pkg directory"
81
+ task :build => :gemspec do
82
+ sh "mkdir -p pkg"
83
+ sh "gem build #{gemspec_file}"
84
+ sh "mv #{gem_file} pkg"
85
+ end
86
+
87
+ desc "Generate #{gemspec_file}"
88
+ task :gemspec => :validate do
89
+ # read spec file and split out manifest section
90
+ spec = File.read(gemspec_file)
91
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
92
+
93
+ # replace name version and date
94
+ replace_header(head, :name)
95
+ replace_header(head, :version)
96
+ replace_header(head, :date)
97
+ #comment this out if your rubyforge_project has a different name
98
+ replace_header(head, :rubyforge_project)
99
+
100
+ # determine file list from git ls-files
101
+ files = `git ls-files`.
102
+ split("\n").
103
+ sort.
104
+ reject { |file| file =~ /^\./ }.
105
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
106
+ map { |file| " #{file}" }.
107
+ join("\n")
108
+
109
+ # piece file back together and write
110
+ manifest = " s.files = %w[\n#{files}\n ]\n"
111
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
112
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
113
+ puts "Updated #{gemspec_file}"
114
+ end
115
+
116
+ desc "Validate #{gemspec_file}"
117
+ task :validate do
118
+ unless Dir['VERSION*'].empty?
119
+ puts "A `VERSION` file at root level violates Gem best practices."
120
+ exit!
121
+ end
122
+ end
@@ -0,0 +1,65 @@
1
+ ## This is the rakegem gemspec template. Make sure you read and understand
2
+ ## all of the comments. Some sections require modification, and others can
3
+ ## be deleted if you don't need them. Once you understand the contents of
4
+ ## this file, feel free to delete any comments that begin with two hash marks.
5
+ ## You can find comprehensive Gem::Specification documentation, at
6
+ ## http://docs.rubygems.org/read/chapter/20
7
+ Gem::Specification.new do |s|
8
+ s.specification_version = 2 if s.respond_to? :specification_version=
9
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.rubygems_version = '1.3.5'
11
+
12
+ ## Leave these as is they will be modified for you by the rake gemspec task.
13
+ ## If your rubyforge_project name is different, then edit it and comment out
14
+ ## the sub! line in the Rakefile
15
+ s.name = 'knife-spork'
16
+ s.version = '0.1.0'
17
+ s.date = '2012-01-28'
18
+ s.rubyforge_project = 'knife-spork'
19
+
20
+ ## Make sure your summary is short. The description may be as long
21
+ ## as you like.
22
+ s.summary = "A workflow plugin to help many devs work with the same chef repo/server"
23
+ s.description = "A workflow plugin to help many devs work with the same chef repo/server"
24
+
25
+ ## List the primary authors. If there are a bunch of authors, it's probably
26
+ ## better to set the email to an email list or something. If you don't have
27
+ ## a custom homepage, consider using your GitHub URL or the like.
28
+ s.authors = ["Jon Cowie"]
29
+ s.email = 'jonlives@gmail.com'
30
+ s.homepage = 'https://github.com/jonlives/knife-spork'
31
+
32
+ ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
33
+ ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
34
+ s.require_paths = %w[lib]
35
+
36
+ ## Specify any RDoc options here. You'll want to add your README and
37
+ ## LICENSE files to the extra_rdoc_files list.
38
+ s.rdoc_options = ["--charset=UTF-8"]
39
+ s.extra_rdoc_files = %w[README.md]
40
+
41
+ ## List your runtime dependencies here. Runtime dependencies are those
42
+ ## that are needed for an end user to actually USE your code.
43
+ s.add_dependency('chef', [">= 0.10.4"])
44
+ s.add_dependency('git', [">= 1.2.5"])
45
+
46
+ ## Leave this section as-is. It will be automatically generated from the
47
+ ## contents of your Git repository via the gemspec task. DO NOT REMOVE
48
+ ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
49
+ # = MANIFEST =
50
+ s.files = %w[
51
+ README.md
52
+ Rakefile
53
+ knife-spork.gemspec
54
+ lib/chef/knife/spork-bump.rb
55
+ lib/chef/knife/spork-check.rb
56
+ lib/chef/knife/spork-promote.rb
57
+ lib/chef/knife/spork-upload.rb
58
+ lib/knife-spork.rb
59
+ ]
60
+ # = MANIFEST =
61
+
62
+ ## Test files will be grabbed from the file list. Make sure the path glob
63
+ ## matches what you actually use.
64
+ s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
65
+ end
@@ -0,0 +1,163 @@
1
+ #
2
+ # Modifying Author:: Jon Cowie (<jonlives@gmail.com>)
3
+ # Copyright:: Copyright (c) 2011 Jon Cowie
4
+ # License:: GPL
5
+
6
+ # Based on the knife-cookbook-bump plugin by:
7
+ # Alalanta (no license specified)
8
+
9
+ require 'chef/knife'
10
+ require 'chef/cookbook_loader'
11
+ require 'chef/cookbook_uploader'
12
+
13
+ module KnifeSpork
14
+ class SporkBump < Chef::Knife
15
+
16
+ TYPE_INDEX = { "major" => 0, "minor" => 1, "patch" => 2, "manual" => 3 }
17
+
18
+ banner "knife spork bump COOKBOOK [MAJOR|MINOR|PATCH|MANUAL]"
19
+
20
+ @@gitavail = true
21
+ deps do
22
+ begin
23
+ require "git"
24
+ rescue LoadError
25
+ @@gitavail = false
26
+ end
27
+ end
28
+
29
+ def run
30
+
31
+ bump_type=""
32
+
33
+ self.config = Chef::Config.merge!(config)
34
+ if config.has_key?(:cookbook_path)
35
+ cookbook_path = config["cookbook_path"]
36
+ else
37
+ ui.fatal "No default cookbook_path; Specify with -o or fix your knife.rb."
38
+ show_usage
39
+ exit 1
40
+ end
41
+
42
+ if name_args.size == 0
43
+ show_usage
44
+ exit 0
45
+ end
46
+
47
+ if name_args.size == 3
48
+ bump_type = name_args[1]
49
+ elsif name_args.size == 2
50
+ bump_type = name_args.last
51
+ else
52
+ ui.fatal "Please specify the cookbook whose version you which to bump, and the type of bump you wish to apply."
53
+ show_usage
54
+ exit 1
55
+ end
56
+
57
+ unless TYPE_INDEX.has_key?(bump_type)
58
+ ui.fatal "Sorry, '#{name_args.last}' isn't a valid bump type. Specify one of 'major', 'minor', 'patch' or 'manual'"
59
+ show_usage
60
+ exit 1
61
+ end
62
+
63
+ if bump_type == "manual"
64
+ manual_version = name_args.last
65
+ cookbook = name_args.first
66
+ cookbook_path = Array(config[:cookbook_path]).first
67
+ patch_type = "manual"
68
+ patch_manual(cookbook_path, cookbook, manual_version)
69
+ else
70
+ cookbook = name_args.first
71
+ patch_type = name_args.last
72
+ cookbook_path = Array(config[:cookbook_path]).first
73
+ patch(cookbook_path, cookbook, patch_type)
74
+ end
75
+
76
+ if !@@gitavail
77
+ ui.msg "Git gem not available, skipping git add.\n\n"
78
+ else
79
+ git_add(cookbook)
80
+ end
81
+
82
+ end
83
+
84
+
85
+ def patch(cookbook_path, cookbook, type)
86
+ t = TYPE_INDEX[type]
87
+ current_version = get_version(cookbook_path, cookbook).split(".").map{|i| i.to_i}
88
+ bumped_version = current_version.clone
89
+ bumped_version[t] = bumped_version[t] + 1
90
+ while t < 2
91
+ t+=1
92
+ bumped_version[t] = 0
93
+ end
94
+ metadata_file = File.join(cookbook_path, cookbook, "metadata.rb")
95
+ old_version = current_version.join('.')
96
+ new_version = bumped_version.join('.')
97
+ update_metadata(old_version, new_version, metadata_file)
98
+ ui.msg("Bumping #{type} level of the #{cookbook} cookbook from #{old_version} to #{new_version}\n\n")
99
+ end
100
+
101
+ def patch_manual(cookbook_path, cookbook, version)
102
+ current_version = get_version(cookbook_path, cookbook)
103
+ v = version.split(".")
104
+ if v.size < 3 or v.size > 3
105
+ ui.msg "That isn't a valid version number to bump to."
106
+ exit 1
107
+ end
108
+
109
+ v.each do |v_comp|
110
+ if !v_comp.is_i?
111
+ ui.msg "That isn't a valid version number to bump to."
112
+ exit 1
113
+ end
114
+ end
115
+
116
+ metadata_file = File.join(cookbook_path, cookbook, "metadata.rb")
117
+ update_metadata(current_version, version, metadata_file)
118
+ ui.msg("Manually bumped version of the #{cookbook} cookbook from #{current_version} to #{version}")
119
+ end
120
+
121
+ def update_metadata(old_version, new_version, metadata_file)
122
+ open_file = File.open(metadata_file, "r")
123
+ body_of_file = open_file.read
124
+ open_file.close
125
+ body_of_file.gsub!(old_version, new_version)
126
+ File.open(metadata_file, "w") { |file| file << body_of_file }
127
+ end
128
+
129
+ def get_version(cookbook_path, cookbook)
130
+ loader = ::Chef::CookbookLoader.new(cookbook_path)
131
+ return loader[cookbook].version
132
+ end
133
+
134
+ def git_add(cookbook)
135
+ strio = StringIO.new
136
+ l = Logger.new strio
137
+ cookbook_path = config[:cookbook_path]
138
+ if cookbook_path.size > 1
139
+ ui.warn "It looks like you have multiple cookbook paths defined so I can't tell if you're running inside a git repo.\n\n"
140
+ else
141
+ begin
142
+ path = cookbook_path[0].gsub("cookbooks","")
143
+ ui.msg "Opening git repo #{path}\n\n"
144
+ g = Git.open(path, :log => Logger.new(strio))
145
+ ui.msg "Git add'ing #{path}cookbooks/#{cookbook}/metadata.rb\n\n"
146
+ g.add("#{path}cookbooks/#{cookbook}/metadata.rb")
147
+ rescue ArgumentError => e
148
+ puts "Git Error: The root of your chef repo doesn't look like it's a git repo. Skipping git add...\n\n"
149
+ rescue
150
+ puts "Git Error: Something went wrong, Dumping log info..."
151
+ puts "#{strio.string}"
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ end
158
+
159
+ class String
160
+ def is_i?
161
+ !!(self =~ /^[-+]?[0-9]+$/)
162
+ end
163
+ end
@@ -0,0 +1,123 @@
1
+ #
2
+ # Author:: Jon Cowie (<jonlives@gmail.com>)
3
+ # Copyright:: Copyright (c) 2011 Jon Cowie
4
+ # License:: GPL
5
+
6
+
7
+ require 'json'
8
+ require 'chef/knife'
9
+ require 'chef/cookbook_loader'
10
+
11
+ module KnifeSpork
12
+ class SporkCheck < Chef::Knife
13
+
14
+ deps do
15
+ require 'chef/json_compat'
16
+ require 'uri'
17
+ require 'chef/cookbook_version'
18
+ end
19
+ banner "knife spork check COOKBOOK"
20
+
21
+ def run
22
+
23
+ self.config = Chef::Config.merge!(config)
24
+
25
+ if config.has_key?(:cookbook_path)
26
+ cookbook_path = config["cookbook_path"]
27
+ else
28
+ ui.fatal "No default cookbook_path; Specify with -o or fix your knife.rb."
29
+ show_usage
30
+ exit 1
31
+ end
32
+
33
+ if name_args.size == 0
34
+ show_usage
35
+ exit 0
36
+ end
37
+
38
+ unless name_args.size == 1
39
+ ui.fatal "Please specify the cookbook whose version you which to check."
40
+ show_usage
41
+ exit 1
42
+ end
43
+
44
+ cookbook = name_args.first
45
+ cookbook_path = Array(config[:cookbook_path]).first
46
+
47
+ local_version = get_local_cookbook_version(cookbook_path, cookbook)
48
+ remote_versions = get_remote_cookbook_versions(cookbook)
49
+
50
+ check_versions(cookbook, local_version, remote_versions)
51
+ end
52
+
53
+
54
+ def get_local_cookbook_version(cookbook_path, cookbook)
55
+ current_version = get_version(cookbook_path, cookbook).split(".").map{|i| i.to_i}
56
+ metadata_file = File.join(cookbook_path, cookbook, "metadata.rb")
57
+ local_version = current_version.join('.')
58
+ return local_version
59
+ end
60
+
61
+ def get_remote_cookbook_versions(cookbook)
62
+ env = config[:environment]
63
+ api_endpoint = env ? "environments/#{env}/cookbooks/#{cookbook}" : "cookbooks/#{cookbook}"
64
+ cookbooks = rest.get_rest(api_endpoint)
65
+ versions = cookbooks[cookbook]["versions"]
66
+ return versions
67
+ end
68
+
69
+ def check_versions(cookbook, local_version, remote_versions)
70
+
71
+ conflict = false
72
+ frozen = false
73
+ ui.msg "Checking versions for cookbook #{cookbook}..."
74
+ ui.msg ""
75
+ ui.msg "Current local version: #{local_version}"
76
+ ui.msg ""
77
+ ui.msg "Remote versions:"
78
+ remote_versions.each do |v|
79
+
80
+ version_frozen = check_frozen(cookbook,v["version"])
81
+
82
+ if version_frozen then
83
+ pretty_frozen = "frozen"
84
+ else
85
+ pretty_frozen = "unfrozen"
86
+ end
87
+
88
+ if v["version"] == local_version then
89
+ ui.msg "*" + v["version"] + ", " + pretty_frozen
90
+ conflict = true
91
+ if version_frozen then
92
+ frozen = true
93
+ end
94
+ else
95
+ ui.msg v["version"] + ", " + pretty_frozen
96
+ end
97
+
98
+ end
99
+ ui.msg ""
100
+
101
+ if conflict && frozen
102
+ ui.msg "DANGER: Your local cookbook has same version number as the starred version above!\n\nPlease bump your local version or you won't be able to upload."
103
+ elsif conflict && !frozen
104
+ ui.msg "DANGER: Your local cookbook version number clashes with an unfrozen remote version.\n\nIf you upload now, you'll overwrite it."
105
+ else
106
+ ui.msg "Everything looks fine, no version clashes. You can upload!"
107
+ end
108
+ end
109
+
110
+ def get_version(cookbook_path, cookbook)
111
+ loader = ::Chef::CookbookLoader.new(cookbook_path)
112
+ return loader[cookbook].version
113
+ end
114
+
115
+ def check_frozen(cookbook,version)
116
+ env = config[:environment]
117
+ api_endpoint = env ? "environments/#{env}/cookbooks/#{cookbook}" : "cookbooks/#{cookbook}/#{version}"
118
+ cookbooks = rest.get_rest(api_endpoint)
119
+ cookbook_hash = cookbooks.to_hash
120
+ return cookbook_hash["frozen?"]
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,228 @@
1
+ #
2
+ # Author:: Jon Cowie (<jonlives@gmail.com>)
3
+ # Copyright:: Copyright (c) 2011 Jon Cowie
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ #
7
+ # Uses code from the knife cookbook upload plugin by:
8
+ #
9
+ # Author:: Adam Jacob (<adam@opscode.com>)
10
+ # Author:: Christopher Walters (<cw@opscode.com>)
11
+ # Author:: Nuo Yan (<yan.nuo@gmail.com>)
12
+ # Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
13
+ # License:: Apache License, Version 2.0
14
+ #
15
+ # Licensed under the Apache License, Version 2.0 (the "License");
16
+ # you may not use this file except in compliance with the License.
17
+ # You may obtain a copy of the License at
18
+ #
19
+ # http://www.apache.org/licenses/LICENSE-2.0
20
+ #
21
+ # Unless required by applicable law or agreed to in writing, software
22
+ # distributed under the License is distributed on an "AS IS" BASIS,
23
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
+ # See the License for the specific language governing permissions and
25
+ # limitations under the License.
26
+ #
27
+
28
+ require 'chef/knife'
29
+ require 'json'
30
+
31
+ module KnifeSpork
32
+ class SporkPromote < Chef::Knife
33
+
34
+ @@gitavail = true
35
+ deps do
36
+ require 'chef/exceptions'
37
+ require 'chef/cookbook_loader'
38
+ require 'chef/knife/core/object_loader'
39
+ begin
40
+ require "git"
41
+ rescue LoadError
42
+ @@gitavail = false
43
+ end
44
+ end
45
+
46
+ banner "knife spork promote ENVIRONMENT COOKBOOK (options)"
47
+
48
+ option :version,
49
+ :short => '-v',
50
+ :long => '--version VERSION',
51
+ :description => "Set the environment's version constraint to the specified version",
52
+ :default => nil
53
+
54
+ option :remote,
55
+ :long => '--remote',
56
+ :description => "Save the environment to the chef server in addition to the local JSON file",
57
+ :default => nil
58
+
59
+ def run
60
+ config[:cookbook_path] ||= Chef::Config[:cookbook_path]
61
+
62
+ if @name_args.empty?
63
+ show_usage
64
+ ui.error("You must specify a cookbook name and an environment")
65
+ exit 1
66
+ elsif @name_args.size != 2
67
+ show_usage
68
+ ui.error("You must specify a cookbook name and an environment")
69
+ exit 1
70
+ end
71
+
72
+ if !@@gitavail
73
+ ui.msg "Git gem not available, skipping git pull.\n\n"
74
+ else
75
+ git_pull_if_repo
76
+ end
77
+
78
+ @cookbook = @name_args[1]
79
+ @environment = loader.load_from("environments", @name_args[0] + ".json")
80
+
81
+ if @cookbook == "all"
82
+ ui.msg "Promoting ALL cookbooks to environment #{@environment}\n\n"
83
+ cookbook_names = get_all_cookbooks
84
+ cookbook_names.each do |c|
85
+ @environment = promote(@environment, c)
86
+ end
87
+ else
88
+ @environment = promote(@environment, @cookbook)
89
+ end
90
+
91
+ ui.msg "\nSaving changes into #{@name_args[0]}.json"
92
+ new_environment_json = pretty_print(@environment)
93
+ save_environment_changes(@name_args[0],new_environment_json)
94
+
95
+ if config[:remote]
96
+ ui.msg "\nUploading #{@name_args[0]} to server"
97
+ save_environment_changes_remote(@name_args[0] + ".json")
98
+ ui.info "\nPromotion complete, and environment uploaded."
99
+ else
100
+ ui.info "\nPromotion complete! Please remember to upload your changed Environment file to the Chef Server."
101
+ end
102
+
103
+ end
104
+
105
+ def update_version_constraints(environment,cookbook,version_constraint)
106
+ environment.cookbook_versions[cookbook] = "= #{version_constraint}"
107
+ return environment
108
+ end
109
+
110
+ def get_version(cookbook_path, cookbook)
111
+ loader = ::Chef::CookbookLoader.new(cookbook_path)
112
+ return loader[cookbook].version
113
+ end
114
+
115
+ def load_environment(env)
116
+ e = Chef::Environment.load(env)
117
+ ejson = JSON.parse(e.to_json)
118
+ puts JSON.pretty_generate(ejson)
119
+ return e
120
+ rescue Net::HTTPServerException => e
121
+ if e.response.code.to_s == "404"
122
+ ui.error "The environment #{env} does not exist on the server, aborting."
123
+ Chef::Log.debug(e)
124
+ exit 1
125
+ else
126
+ raise
127
+ end
128
+ end
129
+
130
+ def valid_version(version)
131
+ v = version.split(".")
132
+ if v.size < 3 or v.size > 3
133
+ return false
134
+ end
135
+ v.each do |v_comp|
136
+ if !v_comp.is_i?
137
+ return false
138
+ end
139
+ end
140
+ return true
141
+ end
142
+
143
+ def loader
144
+ @loader ||= Chef::Knife::Core::ObjectLoader.new(Chef::Environment, ui)
145
+ end
146
+
147
+ def save_environment_changes_remote(environment)
148
+ @loader ||= Knife::Core::ObjectLoader.new(Chef::Environment, ui)
149
+ updated = loader.load_from("environments", environment)
150
+ updated.save
151
+ end
152
+
153
+ def save_environment_changes(environment,envjson)
154
+ cookbook_path = config[:cookbook_path]
155
+
156
+ if cookbook_path.size > 1
157
+ ui.warn "It looks like you have multiple cookbook paths defined so I'm not sure where to save your changed environment file.\n\n"
158
+ ui.msg "Here's the JSON for you to paste into #{environment}.json in the environments directory you wish to use.\n\n"
159
+ ui.msg "#{envjson}\n\n"
160
+ else
161
+ path = cookbook_path[0].gsub("cookbooks","environments") + "/#{environment}.json"
162
+
163
+ File.open(path, 'w') do |f2|
164
+ # use "\n" for two lines of text
165
+ f2.puts envjson
166
+ end
167
+ end
168
+ end
169
+
170
+ def promote(environment,cookbook)
171
+
172
+ if config[:version]
173
+ if !valid_version(config[:version])
174
+ ui.error("#{config[:version]} isn't a valid version number.")
175
+ return 1
176
+ else
177
+ @version = config[:version]
178
+ end
179
+ else
180
+ @version = get_version(config[:cookbook_path], cookbook)
181
+ end
182
+
183
+ ui.msg "Adding version constraint #{cookbook} = #{@version}"
184
+ return update_version_constraints(environment,cookbook,@version)
185
+ end
186
+
187
+ def get_all_cookbooks
188
+ results = []
189
+ cookbooks = ::Chef::CookbookLoader.new(config[:cookbook_path])
190
+ cookbooks.each do |c|
191
+ results << c
192
+ end
193
+ return results
194
+ end
195
+
196
+ def pretty_print(environment)
197
+ return JSON.pretty_generate(JSON.parse(environment.to_json))
198
+ end
199
+
200
+ def git_pull_if_repo
201
+ strio = StringIO.new
202
+ l = Logger.new strio
203
+ cookbook_path = config[:cookbook_path]
204
+ if cookbook_path.size > 1
205
+ ui.warn "It looks like you have multiple cookbook paths defined so I can't tell if you're running inside a git repo.\n\n"
206
+ else
207
+ begin
208
+ path = cookbook_path[0].gsub("cookbooks","")
209
+ ui.msg "Opening git repo #{path}\n\n"
210
+ g = Git.open(path, :log => Logger.new(strio))
211
+ ui.msg "Pulling latest changes from git\n\n"
212
+ g.pull
213
+ rescue ArgumentError => e
214
+ puts "Git Error: The root of your chef repo doesn't look like it's a git repo. Skipping git pull...\n\n"
215
+ rescue
216
+ puts "Git Error: Something went wrong, Dumping log info..."
217
+ puts "#{strio.string}"
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+
224
+ class String
225
+ def is_i?
226
+ !!(self =~ /^[-+]?[0-9]+$/)
227
+ end
228
+ end
@@ -0,0 +1,227 @@
1
+ #
2
+ # Modifying Author:: Jon Cowie (<jonlives@gmail.com>)
3
+ # Copyright:: Copyright (c) 2011 Jon Cowie
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Modified cookbook upload to always freeze, and disable --force option
7
+ # Based on the knife cookbook upload plugin by:
8
+ #
9
+ # Author:: Adam Jacob (<adam@opscode.com>)
10
+ # Author:: Christopher Walters (<cw@opscode.com>)
11
+ # Author:: Nuo Yan (<yan.nuo@gmail.com>)
12
+ # Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
13
+ # License:: Apache License, Version 2.0
14
+ #
15
+ # Licensed under the Apache License, Version 2.0 (the "License");
16
+ # you may not use this file except in compliance with the License.
17
+ # You may obtain a copy of the License at
18
+ #
19
+ # http://www.apache.org/licenses/LICENSE-2.0
20
+ #
21
+ # Unless required by applicable law or agreed to in writing, software
22
+ # distributed under the License is distributed on an "AS IS" BASIS,
23
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
+ # See the License for the specific language governing permissions and
25
+ # limitations under the License.
26
+ #
27
+
28
+ require 'chef/knife'
29
+
30
+ module KnifeSpork
31
+ class SporkUpload < Chef::Knife
32
+
33
+ CHECKSUM = "checksum"
34
+ MATCH_CHECKSUM = /[0-9a-f]{32,}/
35
+
36
+ deps do
37
+ require 'chef/exceptions'
38
+ require 'chef/cookbook_loader'
39
+ require 'chef/cookbook_uploader'
40
+ end
41
+
42
+ banner "knife spork upload [COOKBOOKS...] (options)"
43
+
44
+ option :cookbook_path,
45
+ :short => "-o PATH:PATH",
46
+ :long => "--cookbook-path PATH:PATH",
47
+ :description => "A colon-separated path to look for cookbooks in",
48
+ :proc => lambda { |o| o.split(":") }
49
+
50
+ option :freeze,
51
+ :long => '--freeze',
52
+ :description => 'Freeze this version of the cookbook so that it cannot be overwritten',
53
+ :boolean => true
54
+
55
+ option :environment,
56
+ :short => '-E',
57
+ :long => '--environment ENVIRONMENT',
58
+ :description => "Set ENVIRONMENT's version dependency match the version you're uploading.",
59
+ :default => nil
60
+
61
+ option :depends,
62
+ :short => "-d",
63
+ :long => "--include-dependencies",
64
+ :description => "Also upload cookbook dependencies"
65
+
66
+ def run
67
+ config[:cookbook_path] ||= Chef::Config[:cookbook_path]
68
+
69
+ assert_environment_valid!
70
+ warn_about_cookbook_shadowing
71
+ version_constraints_to_update = {}
72
+ # Get a list of cookbooks and their versions from the server
73
+ # for checking existence of dependending cookbooks.
74
+ @server_side_cookbooks = Chef::CookbookVersion.list
75
+
76
+ if @name_args.empty?
77
+ show_usage
78
+ ui.error("You must specify the --all flag or at least one cookbook name")
79
+ exit 1
80
+ end
81
+ justify_width = @name_args.map {|name| name.size }.max.to_i + 2
82
+ @name_args.each do |cookbook_name|
83
+ begin
84
+ cookbook = cookbook_repo[cookbook_name]
85
+ if config[:depends]
86
+ cookbook.metadata.dependencies.each do |dep, versions|
87
+ @name_args.push dep
88
+ end
89
+ end
90
+ ui.info("Uploading and freezing #{cookbook.name.to_s.ljust(justify_width + 10)} [#{cookbook.version}]")
91
+ upload(cookbook, justify_width)
92
+ cookbook.freeze_version
93
+ upload(cookbook, justify_width)
94
+ version_constraints_to_update[cookbook_name] = cookbook.version
95
+ rescue Chef::Exceptions::CookbookNotFoundInRepo => e
96
+ ui.error("Could not find cookbook #{cookbook_name} in your cookbook path, skipping it")
97
+ Chef::Log.debug(e)
98
+ end
99
+ end
100
+
101
+ ui.info "upload complete"
102
+ update_version_constraints(version_constraints_to_update) if config[:environment]
103
+ end
104
+
105
+ def cookbook_repo
106
+ @cookbook_loader ||= begin
107
+ Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, config[:cookbook_path]) }
108
+ Chef::CookbookLoader.new(config[:cookbook_path])
109
+ end
110
+ end
111
+
112
+ def update_version_constraints(new_version_constraints)
113
+ new_version_constraints.each do |cookbook_name, version|
114
+ environment.cookbook_versions[cookbook_name] = "= #{version}"
115
+ end
116
+ environment.save
117
+ end
118
+
119
+
120
+ def environment
121
+ @environment ||= config[:environment] ? Chef::Environment.load(config[:environment]) : nil
122
+ end
123
+
124
+ def warn_about_cookbook_shadowing
125
+ unless cookbook_repo.merged_cookbooks.empty?
126
+ ui.warn "* " * 40
127
+ ui.warn(<<-WARNING)
128
+ The cookbooks: #{cookbook_repo.merged_cookbooks.join(', ')} exist in multiple places in your cookbook_path.
129
+ A composite version of these cookbooks has been compiled for uploading.
130
+
131
+ #{ui.color('IMPORTANT:', :red, :bold)} In a future version of Chef, this behavior will be removed and you will no longer
132
+ be able to have the same version of a cookbook in multiple places in your cookbook_path.
133
+ WARNING
134
+ ui.warn "The affected cookbooks are located:"
135
+ ui.output ui.format_for_display(cookbook_repo.merged_cookbook_paths)
136
+ ui.warn "* " * 40
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def assert_environment_valid!
143
+ environment
144
+ rescue Net::HTTPServerException => e
145
+ if e.response.code.to_s == "404"
146
+ ui.error "The environment #{config[:environment]} does not exist on the server, aborting."
147
+ Chef::Log.debug(e)
148
+ exit 1
149
+ else
150
+ raise
151
+ end
152
+ end
153
+
154
+ def upload(cookbook, justify_width)
155
+
156
+ check_for_broken_links(cookbook)
157
+ check_dependencies(cookbook)
158
+ Chef::CookbookUploader.new(cookbook, config[:cookbook_path]).upload_cookbook
159
+ rescue Net::HTTPServerException => e
160
+ case e.response.code
161
+ when "409"
162
+ ui.error "Version #{cookbook.version} of cookbook #{cookbook.name} is frozen. Please bump your version number."
163
+ Chef::Log.debug(e)
164
+ else
165
+ raise
166
+ end
167
+ end
168
+
169
+ # if only you people wouldn't put broken symlinks in your cookbooks in
170
+ # the first place. ;)
171
+ def check_for_broken_links(cookbook)
172
+ # MUST!! dup the cookbook version object--it memoizes its
173
+ # manifest object, but the manifest becomes invalid when you
174
+ # regenerate the metadata
175
+ broken_files = cookbook.dup.manifest_records_by_path.select do |path, info|
176
+ info[CHECKSUM].nil? || info[CHECKSUM] !~ MATCH_CHECKSUM
177
+ end
178
+ unless broken_files.empty?
179
+ broken_filenames = Array(broken_files).map {|path, info| path}
180
+ ui.error "The cookbook #{cookbook.name} has one or more broken files"
181
+ ui.info "This is probably caused by broken symlinks in the cookbook directory"
182
+ ui.info "The broken file(s) are: #{broken_filenames.join(' ')}"
183
+ exit 1
184
+ end
185
+ end
186
+
187
+ def check_dependencies(cookbook)
188
+ # for each dependency, check if the version is on the server, or
189
+ # the version is in the cookbooks being uploaded. If not, exit and warn the user.
190
+ cookbook.metadata.dependencies.each do |cookbook_name, version|
191
+ unless check_server_side_cookbooks(cookbook_name, version) || check_uploading_cookbooks(cookbook_name, version)
192
+ # warn the user and exit
193
+ ui.error "Cookbook #{cookbook.name} depends on cookbook #{cookbook_name} version #{version},"
194
+ ui.error "which is not currently being uploaded and cannot be found on the server."
195
+ exit 1
196
+ end
197
+ end
198
+ end
199
+
200
+ def check_server_side_cookbooks(cookbook_name, version)
201
+ if @server_side_cookbooks[cookbook_name].nil?
202
+ false
203
+ else
204
+ @server_side_cookbooks[cookbook_name]["versions"].each do |versions_hash|
205
+ return true if Chef::VersionConstraint.new(version).include?(versions_hash["version"])
206
+ end
207
+ false
208
+ end
209
+ end
210
+
211
+ def check_uploading_cookbooks(cookbook_name, version)
212
+ if config[:all]
213
+ # check from all local cookbooks in the path
214
+ unless cookbook_repo[cookbook_name].nil?
215
+ return Chef::VersionConstraint.new(version).include?(cookbook_repo[cookbook_name].version)
216
+ end
217
+ else
218
+ # check from only those in the command argument
219
+ if @name_args.include?(cookbook_name)
220
+ return Chef::VersionConstraint.new(version).include?(cookbook_repo[cookbook_name].version)
221
+ end
222
+ end
223
+ false
224
+ end
225
+
226
+ end
227
+ end
@@ -0,0 +1,3 @@
1
+ module KnifeSpork
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: knife-spork
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jon Cowie
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-28 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: chef
16
+ requirement: &70176180960680 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.10.4
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70176180960680
25
+ - !ruby/object:Gem::Dependency
26
+ name: git
27
+ requirement: &70176180960200 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 1.2.5
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70176180960200
36
+ description: A workflow plugin to help many devs work with the same chef repo/server
37
+ email: jonlives@gmail.com
38
+ executables: []
39
+ extensions: []
40
+ extra_rdoc_files:
41
+ - README.md
42
+ files:
43
+ - README.md
44
+ - Rakefile
45
+ - knife-spork.gemspec
46
+ - lib/chef/knife/spork-bump.rb
47
+ - lib/chef/knife/spork-check.rb
48
+ - lib/chef/knife/spork-promote.rb
49
+ - lib/chef/knife/spork-upload.rb
50
+ - lib/knife-spork.rb
51
+ homepage: https://github.com/jonlives/knife-spork
52
+ licenses: []
53
+ post_install_message:
54
+ rdoc_options:
55
+ - --charset=UTF-8
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project: knife-spork
72
+ rubygems_version: 1.8.15
73
+ signing_key:
74
+ specification_version: 2
75
+ summary: A workflow plugin to help many devs work with the same chef repo/server
76
+ test_files: []