knife-table 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ ## v0.0.1
2
+ * Initial commit
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # ChefTable
2
+
3
+ ChefTable is a knife plugin to aid in cookbook development
4
+ workflow. Its intention is to help automate versioning
5
+ within environments and cookbook freezing based on a stable
6
+ branch. Building off of the knife-spork plugin ChefTable
7
+ helps to provide consistency within the environment.
8
+
9
+
10
+ ## Usage
11
+
12
+ Currently, two helpers are available:
13
+
14
+ `knife table set`
15
+
16
+ and
17
+
18
+ `knife table serve`
19
+
20
+ ### Setting the table
21
+
22
+ First, we set the table either by adding new or modifying
23
+ existing features. To do this, we set the table with a basic description
24
+ of what is being added:
25
+
26
+ `knife table set new feature`
27
+
28
+ This will create a new working branch named 'WIP-new_feature'. The prefix
29
+ for the branch defaults to 'WIP-' but can be modified using the `-p` option.
30
+ If it is known what cookbooks will be modified, you can provide them while
31
+ setting:
32
+
33
+ `knife table set -c iptables,mysql new feature`
34
+
35
+ ### Service
36
+
37
+ Service works on the assumption that any new code into the stable branch (master
38
+ by default) will arrive via pull requests. By default, it will find the last
39
+ pull request in the log and update based on changes within that merge. The default
40
+ behavior of the `serve` command will upload and freeze any changed cookbooks. Optionally,
41
+ environments can be provided to have the cookbook versions automatically pegged. Also,
42
+ roles and data bags can be automatically uploaded as well.
43
+
44
+ Options for serve:
45
+
46
+ * `--environments ENV[,ENV...]` 'Update versions in given environments'
47
+ * `--git-autopush` 'Automatically commit and push any changes to master'
48
+ * `--git-tag` 'Automatically create tag for frozen cookbook'
49
+ * `--git-branch BRANCH` 'Set working branch'
50
+ * `--git-remote-name NAME` 'Remote repo name'
51
+ * `--git-autocommit` 'Automatically commit changes'
52
+ * `--autoproceed` 'Answer yes to any prompts'
53
+ * `--roles` 'Upload any changed roles'
54
+ * `--data-bags` 'Upload any changed data bags'
@@ -0,0 +1,12 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'knife-table'
3
+ s.version = '0.0.1'
4
+ s.summary = 'Help chef set and serve the table'
5
+ s.author = 'Chris Roberts'
6
+ s.email = 'chrisroberts.code@gmail.com'
7
+ s.homepage = 'http://github.com/heavywater/knife-table'
8
+ s.description = "Chef's table"
9
+ s.require_path = 'lib'
10
+ s.files = Dir.glob('**/*')
11
+ s.add_dependency 'knife-spork'
12
+ end
@@ -0,0 +1,54 @@
1
+ require 'pp'
2
+
3
+ class DataHolder
4
+
5
+ def initialize
6
+ @holder = {}
7
+ @path = nil
8
+ end
9
+
10
+ def method_missing(name, *args)
11
+ @holder[name] = args
12
+ end
13
+
14
+ def ==(holder)
15
+ raise TypeError.new('Comparisons only allowed between DataHolder instances')
16
+ self._holder == holder._holder
17
+ end
18
+
19
+ def _holder
20
+ @holder
21
+ end
22
+
23
+ def _load(path)
24
+ @path = path
25
+ self.instance_eval(
26
+ File.read(path)
27
+ )
28
+ self
29
+ end
30
+
31
+ def _output
32
+ output = ''
33
+ @holder.each_pair do |k,v|
34
+ output << "#{k}(\n"
35
+ inards = []
36
+ v.each do |item|
37
+ s = ''
38
+ PP.pp(item, s)
39
+ inards << s
40
+ end
41
+ output << inards.join(",\n")
42
+ output << ")\n"
43
+ end
44
+ output
45
+ File.open(@path, 'w') do |file|
46
+ file.write(output)
47
+ end
48
+ output
49
+ end
50
+
51
+ def _path
52
+ @path
53
+ end
54
+ end
@@ -0,0 +1,21 @@
1
+ require 'knife-table/helpers'
2
+
3
+ module KnifeTable
4
+ class TableClear < Chef::Knife
5
+
6
+ include KnifeTable::Helpers
7
+
8
+ deps do
9
+ require 'git'
10
+ require 'chef/knife/core/object_loader'
11
+ end
12
+
13
+ banner 'knife table clear'
14
+
15
+ def run
16
+
17
+ end
18
+
19
+ end
20
+ end
21
+
@@ -0,0 +1,332 @@
1
+ require 'knife-table/helpers'
2
+
3
+ module KnifeTable
4
+ class TableServe < Chef::Knife
5
+
6
+ include KnifeTable::Helpers
7
+
8
+ deps do
9
+ require 'git'
10
+ require 'chef/knife/core/object_loader'
11
+ end
12
+
13
+ banner "knife table serve [#PULLREQ|COMMITSHA[..COMMITSHA]]"
14
+
15
+ option :environments,
16
+ :short => '-e ENV[,ENV...]',
17
+ :long => '--environments ENV[,ENV...]',
18
+ :description => 'Update versions in given environments'
19
+
20
+ option :git_autopush,
21
+ :short => '-g',
22
+ :long => '--git-autopush',
23
+ :boolean => true,
24
+ :description => 'Automatically commit and push any changes to master'
25
+
26
+ option :git_tag,
27
+ :short => '-t',
28
+ :long => '--git-tag',
29
+ :boolean => true,
30
+ :description => 'Automatically create tag for frozen cookbook'
31
+
32
+ option :git_branch,
33
+ :short => '-b BRANCH',
34
+ :long => '--git-branch BRANCH',
35
+ :default => 'master',
36
+ :description => 'Set working branch'
37
+
38
+ option :git_remote_name,
39
+ :short => '-r NAME',
40
+ :long => '--git-remote-name NAME',
41
+ :default => 'origin',
42
+ :description => 'Remote repo name'
43
+
44
+ option :git_autocommit,
45
+ :short => '-c',
46
+ :long => '--git-autocommit',
47
+ :boolean => true,
48
+ :description => 'Automatically commit changes'
49
+
50
+ option :autoproceed,
51
+ :short => '-a',
52
+ :long => '--autoproceed',
53
+ :boolean => true,
54
+ :description => 'Answer yes to any prompts'
55
+
56
+ option :upload_roles,
57
+ :short => '-r',
58
+ :long => '--roles',
59
+ :boolean => true,
60
+ :description => 'Upload any changed roles'
61
+
62
+ option :upload_data_bags,
63
+ :short => '-d',
64
+ :long => '--data-bags',
65
+ :boolean => true,
66
+ :description => 'Upload any changed data bags'
67
+
68
+ def initialize(*args)
69
+ super
70
+ @environments = config[:environments].to_s.split(",").map(&:strip)
71
+ end
72
+
73
+ def run
74
+ sanity_checks
75
+ cookbooks = discover_changed(:cookbooks, *determine_commit_span).map{|c|c.split('/').first}
76
+ roles = discover_changed(:roles, *determine_commit_span) if config[:upload_roles]
77
+ data_bags = discover_changed(:data_bags, *determine_commit_span) if config[:upload_data_bags]
78
+
79
+ ui.msg ui.highline.color("#{' ' * 10}** Knife Table: Service started **", [HighLine::GREEN, HighLine::BOLD])
80
+ ui.highline.say ui.highline.color("Discovered cookbooks staged for freeze: #{cookbooks.join(', ')}", HighLine::CYAN)
81
+ ui.highline.say ui.highline.color("Environments staged to be updated: #{@environments.join(', ')}", HighLine::CYAN) unless @environments.empty?
82
+ ui.highline.say ui.highline.color("Roles staged to be uploaded: #{roles.sort.map{|r|r.sub(/\.(rb|json)/, '')}.join(', ')}", HighLine::CYAN) unless roles.nil? || roles.empty?
83
+ ui.highline.say ui.highline.color("Data Bags staged to be uploaded: #{data_bags.sort.join(', ')}", HighLine::CYAN) unless data_bags.nil? || data_bags.empty?
84
+
85
+ ui.highline.say "\n"
86
+
87
+ if(config[:autoproceed])
88
+ ui.warn "Autoproceeding based on config (ctrl+c to halt)"
89
+ sleep(3)
90
+ else
91
+ ui.confirm "Proceed"
92
+ end
93
+
94
+ ui.highline.say "\n"
95
+
96
+ ui.highline.say "#{ui.highline.color("Freezing cookbooks:", HighLine::GREEN)} "
97
+ cookbooks.each{|c| freeze_cookbook(c) }
98
+ ui.highline.say "\n"
99
+
100
+ unless(@environments.empty?)
101
+ ui.msg ui.highline.color("Updating environments:", HighLine::GREEN)
102
+ @environments.each do |env|
103
+ ui.highline.say " #{ui.highline.color(env, HighLine::BLUE)}: "
104
+ cookbooks.each{|c| update_environments(env, c) }
105
+ ui.highline.say "\n"
106
+ end
107
+
108
+ ui.highline.say "#{ui.highline.color("Uploading environments:", HighLine::GREEN)} "
109
+ upload_environments
110
+ ui.highline.say "\n"
111
+ end
112
+
113
+ upload_changes(:roles, roles) if roles && !roles.empty?
114
+ upload_changes(:data_bags, data_bags) if data_bags && !data_bags.empty?
115
+
116
+ if(config[:git_autocommit])
117
+ ui.highline.say "#{ui.highline.color("Committing environments:", HighLine::GREEN)} "
118
+ git_commit_environments(cookbooks)
119
+ ui.highline.say "\n"
120
+ end
121
+
122
+ if(config[:git_tag])
123
+ ui.highline.say "#{ui.highline.color("Tagging cookbooks:", HighLine::GREEN)} "
124
+ git_tag(cookbooks)
125
+ ui.highline.say "\n"
126
+ end
127
+
128
+ if(config[:git_autopush])
129
+ ui.highline.say "#{ui.highline.color("Pushing changes to remote repo:", HighLine::GREEN)} "
130
+ git_push
131
+ ui.highline.say "\n"
132
+ end
133
+
134
+ ui.msg ui.highline.color("#{' ' * 10}** Knife Table: Service complete **", [HighLine::GREEN, HighLine::BOLD])
135
+ end
136
+
137
+ def frozen_cookbook?(cookbook)
138
+ begin
139
+ spork_check = KnifeSpork::SporkCheck.new
140
+ spork_check.check_frozen(
141
+ cookbook,
142
+ spork_check.get_version(
143
+ cookbook_path,
144
+ cookbook
145
+ )
146
+ )
147
+ rescue Net::HTTPServerException => e
148
+ if(e.response.is_a?(Net::HTTPNotFound))
149
+ false
150
+ else
151
+ raise
152
+ end
153
+ end
154
+ end
155
+
156
+ def freeze_cookbook(cookbook)
157
+ unless(frozen_cookbook?(cookbook))
158
+ spork_upload = KnifeSpork::SporkUpload.new
159
+ spork_upload.config[:freeze] = true
160
+ spork_upload.config[:cookbook_path] = cookbook_path
161
+ repo_cookbook = spork_upload.cookbook_repo[cookbook]
162
+ repo_cookbook.freeze_version
163
+ Chef::CookbookUploader.new(repo_cookbook, cookbook_path).upload_cookbook
164
+ ui.highline.say "#{cookbook} "
165
+ else
166
+ ui.highline.say "#{ui.highline.color("#{cookbook} (already frozen)", HighLine::RED)} "
167
+ end
168
+ end
169
+
170
+ def update_environments(environment, cookbook)
171
+ promote_environment(environment, cookbook)
172
+ ui.highline.say "#{cookbook} "
173
+ end
174
+
175
+ def upload_environments
176
+ @environments.each do |environment|
177
+ spork_promote.config[:cookbook_path] = [cookbook_path]
178
+ spork_promote.save_environment_changes_remote(environment)
179
+ ui.highline.say "#{environment} "
180
+ end
181
+ end
182
+
183
+ def git_commit_environments(cookbooks)
184
+ commit = false
185
+ @environments.each do |environment|
186
+ status = git.status.changed[::File.join('environments', "#{environment}.json")]
187
+ if(status && !git.diff('HEAD', status.path).none?)
188
+ git.add(status.path)
189
+ commit_message = "Environment #{environment} cookbook version updates\n"
190
+ cookbooks.sort.each do |cookbook|
191
+ commit_message << "\n#{cookbook}: #{spork_promote.get_version(cookbook_path, cookbook)}"
192
+ end
193
+ git.commit(commit_message)
194
+ ui.highline.say "#{environment} "
195
+ @git_changed = true
196
+ commit = true
197
+ end
198
+ end
199
+ ui.highline.say "nothing to commit " unless commit
200
+ end
201
+
202
+ def git_tag(cookbooks)
203
+ cookbooks.each do |cookbook|
204
+ tag_string = "#{cookbook}-v#{spork_promote.get_version(cookbook_path, cookbook)}"
205
+ unless(git.tags.map(&:name).include?(tag_string))
206
+ git.add_tag(tag_string)
207
+ ui.highline.say "#{tag_string} "
208
+ @git_changed = true
209
+ else
210
+ ui.highline.say "#{ui.highline.color("#{tag_string} (exists)", HighLine::RED)} "
211
+ end
212
+ end
213
+ end
214
+
215
+ def git_push
216
+ if(@git_changed)
217
+ git.push(config[:git_remote_name], config[:git_branch], true)
218
+ ui.highline.say "pushed #{config[:git_branch]} to #{config[:git_remote_name]} "
219
+ else
220
+ ui.highline.say "nothing to push "
221
+ end
222
+ end
223
+
224
+ def upload_roles(role)
225
+ role_load = loader(:roles).load_from('roles', role)
226
+ role_load.save
227
+ end
228
+
229
+ def upload_data_bags(bag)
230
+ data_bag_load = loader(:data_bags).load_from('data_bags', bag.split.first, bag.split.last)
231
+ data_bag_load.save
232
+ end
233
+
234
+ private
235
+
236
+ def promote_environment(environment, cookbook)
237
+ version = spork_promote.get_version(cookbook_path, cookbook)
238
+ env = spork_promote.update_version_constraints(
239
+ Chef::Environment.load(environment),
240
+ cookbook,
241
+ version
242
+ )
243
+ env_json = spork_promote.pretty_print(env)
244
+ spork_promote.save_environment_changes(environment, env_json)
245
+ end
246
+
247
+ def spork_promote
248
+ unless(@spork_promote)
249
+ @spork_promote = KnifeSpork::SporkPromote.new
250
+ @spork_promote.config[:cookbook_path] = [cookbook_path]
251
+ @spork_promote.loader # work around for bad namespacing
252
+ @spork_promote.instance_variable_set(:@conf, AppConf.new) # configuration isn't isolated to single call so just stub
253
+ end
254
+ @spork_promote
255
+ end
256
+
257
+ def sanity_checks
258
+ unless(git.branches.map(&:name).include?(config[:git_branch]))
259
+ raise "Requested git branch (#{config[:git_branch]}) does not exist"
260
+ else
261
+ ui.warn "Checking out requested working branch: #{config[:git_branch]}"
262
+ @git.checkout(config[:git_branch]) if config[:git_branch]
263
+ end
264
+ unless(git.remotes.map(&:name).include?(config[:git_remote_name]))
265
+ raise "Specified remote #{config[:git_remote_name]} not found"
266
+ end
267
+ end
268
+
269
+ def determine_commit_span
270
+ if(@first_commit.nil? || @last_commit.nil?)
271
+ if(name_args.size > 0)
272
+ if(name_args.first.start_with?('#'))
273
+ @first_commit, @last_commit = discover_commits(name_args.first)
274
+ elsif(name_args.first.include?(".."))
275
+ @first_commit, @last_commit = name_args.first.split("..")
276
+ else
277
+ @first_commit = "#{name_args.first}^1"
278
+ @last_commit = name_args.first
279
+ end
280
+ else
281
+ @first_commit, @last_commit = discover_commits
282
+ end
283
+ end
284
+ [@first_commit, @last_commit]
285
+ end
286
+
287
+ def discover_commits(pull_num = nil)
288
+ match = "pull request #{pull_num}"
289
+ commit = git.log.detect{|log| log.message.include?(match)}
290
+ if(commit)
291
+ ["#{commit}^1",commit]
292
+ else
293
+ raise "Unable to locate last pull request"
294
+ end
295
+ end
296
+
297
+ def discover_changed(type, first_commit, last_commit)
298
+ changed = []
299
+ git.diff(first_commit, last_commit).stats[:files].keys.each do |path|
300
+ if(path.start_with?(type.to_s))
301
+ changed << path.sub(/^#{type.to_s}\/?/, '')
302
+ end
303
+ end
304
+ changed.uniq
305
+ end
306
+
307
+ def upload_changes(type, changed)
308
+ raise "Unsupported upload change type: #{type}" unless [:roles, :data_bags].include?(type.to_sym)
309
+ ui.highline.say "#{ui.highline.color("Uploading #{type.to_s.gsub('_', ' ')}:", HighLine::GREEN)} "
310
+ unless(changed.empty?)
311
+ changed.each do |item|
312
+ send("upload_#{type}", item)
313
+ ui.highline.say "#{File.basename(item).sub(/\.(rb|json)/, '')} "
314
+ end
315
+ else
316
+ ui.highline.say "no #{type.to_s.gsub('_', ' ').sub(/s$/, '')} changes detected "
317
+ end
318
+ ui.highline.say "\n"
319
+ end
320
+
321
+ def loader(type)
322
+ if(type == :roles)
323
+ @role_loader ||= Chef::Knife::Core::ObjectLoader.new(Chef::Role, ui)
324
+ elsif(type == :data_bags)
325
+ @role_loader ||= Chef::Knife::Core::ObjectLoader.new(Chef::DataBag, ui)
326
+ else
327
+ raise 'Unsupported load type'
328
+ end
329
+ end
330
+
331
+ end
332
+ end
@@ -0,0 +1,54 @@
1
+ require 'knife-table/helpers'
2
+
3
+ module KnifeTable
4
+ class TableSet < Chef::Knife
5
+
6
+ include KnifeTable::Helpers
7
+
8
+ deps do
9
+ require 'git'
10
+ require 'chef/knife/core/object_loader'
11
+ end
12
+
13
+ banner 'knife table set NEW_FEATURE_OR_FIX'
14
+
15
+ option :cookbooks,
16
+ :short => '-c [COOKBOOK,COOKBOOK,...]',
17
+ :long => '--cookbooks [COOKBOOK,COOKBOOK,...]',
18
+ :description => 'Automatically bump patch version on provided cookbooks'
19
+
20
+ option :branch_prefix,
21
+ :short => '-p PREFIX',
22
+ :long => '--branch-prefix PREFIX',
23
+ :description => 'Set prefix for branch name',
24
+ :default => 'WIP-'
25
+
26
+ option :bump_type,
27
+ :short => '-b TYPE',
28
+ :long => '--bump-type TYPE',
29
+ :description => 'Type of version bump (major, minor, patch)',
30
+ :default => 'patch'
31
+
32
+ def initialize(*args)
33
+ super
34
+ @cookbooks = config[:cookbooks].to_s.split(',').map(&:strip)
35
+ end
36
+
37
+ def run
38
+ ui.msg ui.highline.color("#{' ' * 10}** Knife Table: New place setting **", [HighLine::GREEN, HighLine::BOLD])
39
+ branch_name = "#{config[:branch_prefix]}#{name_args.join('_').downcase}"
40
+ ui.highline.say "Creating new work branch (#{branch_name}): "
41
+ git.branch(branch_name).create
42
+ ui.highline.say "done"
43
+ git.checkout(branch_name)
44
+
45
+ unless(@cookbooks.empty?)
46
+ bumper = KnifeSpork::SporkBump.new
47
+ @cookbooks.each do |cookbook|
48
+ bumper.patch(cookbook_path, cookbook, config[:bump_type])
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1 @@
1
+ require 'knife-table/version'
@@ -0,0 +1,11 @@
1
+ module KnifeTable
2
+ module Helpers
3
+ def git
4
+ @git ||= Git.open(File.dirname(cookbook_path))
5
+ end
6
+
7
+ def cookbook_path
8
+ Chef::Config[:cookbook_path].first
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module KnifeTable
2
+ VERSION = '0.0.1'
3
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: knife-table
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Chris Roberts
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: knife-spork
16
+ requirement: &16438040 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *16438040
25
+ description: Chef's table
26
+ email: chrisroberts.code@gmail.com
27
+ executables: []
28
+ extensions: []
29
+ extra_rdoc_files: []
30
+ files:
31
+ - CHANGELOG.md
32
+ - README.md
33
+ - knife-table.gemspec
34
+ - lib/knife-table/version.rb
35
+ - lib/knife-table/helpers.rb
36
+ - lib/chef/knife/data_holder.rb
37
+ - lib/chef/knife/table_serve.rb
38
+ - lib/chef/knife/table_clear.rb
39
+ - lib/chef/knife/table_set.rb
40
+ - lib/knife-table.rb
41
+ homepage: http://github.com/heavywater/knife-table
42
+ licenses: []
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 1.8.17
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: Help chef set and serve the table
65
+ test_files: []
66
+ has_rdoc: