knife-github 0.0.3 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,2 +1,5 @@
1
1
  *.gem
2
2
  pkg/*
3
+ .ruby-version
4
+ .ruby-gemset
5
+ .rvmrc
data/README.md CHANGED
@@ -12,7 +12,6 @@ You can configure the following attributes within your knife.rb
12
12
  knife[:github_organizations] = [ 'customer-cookbooks', 'central-cookbooks' ]
13
13
  knife[:github_link] = 'ssh'
14
14
  knife[:github_api_version] = 'v3'
15
- knife[:github_cache] = 900
16
15
  knife[:github_ssl_verify_mode] = 'verify_none'
17
16
 
18
17
  ###### github_url
@@ -30,11 +29,15 @@ Options are <tt>ssh</tt> <tt>git</tt> <tt>http</tt> <tt>https</tt> <tt>svn</tt>
30
29
  ###### github_api_version \<optional\>
31
30
  The current and default version of the api is <tt>v3</tt> but this will allow you to target older versions if needed.
32
31
 
33
- ###### github_cache \<optional\>
34
- This will be the lifetime of the cache files in seconds, default <tt>900</tt>. Cache files will be created into the: ~/.chef directory.
35
- We use cache files to offload the api calls and increase the performance for additional executions.
36
-
37
32
  ###### github_ssl_verify_mode \<optional\>
38
33
  The plugin is using the underlying knife http implementation, hence it will have the same options to handle ssl.
39
- Currently the options are: <tt>verify_peer</tt> <tt>verify_none</tt>
34
+ Currently the options are: <tt>verify_peer</tt> <tt>verify_none</tt>
35
+
36
+ Other
37
+ =====
38
+
39
+ Cache files will be created into the: ~/.chef directory.
40
+ We use cache files to offload the api calls and increase the performance for additional executions.
41
+ Updated to any repo inside the organization will cause the cache files to update.
42
+ But in case of any problems, the cache files can be safely deleted.
40
43
 
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/knife-github.gemspec CHANGED
@@ -18,5 +18,6 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
+ s.add_dependency "mixlib-versioning", ">= 1.0.0"
21
22
  # s.add_dependency "chef", "~> 11.0.0"
22
23
  end
@@ -17,6 +17,7 @@
17
17
  #
18
18
 
19
19
  # require 'chef/knife'
20
+ require "knife-github/version"
20
21
 
21
22
  class Chef
22
23
  class Knife
@@ -27,6 +28,7 @@ class Chef
27
28
 
28
29
  deps do
29
30
  require 'chef/mixin/shell_out'
31
+ require 'mixlib/versioning'
30
32
  end
31
33
 
32
34
  option :github_url,
@@ -51,10 +53,15 @@ class Chef
51
53
  :description => "SSL verify mode: verify_peer, verify_none (default: verify_peer)",
52
54
  :boolean => true
53
55
 
54
- option :github_cache,
55
- :long => "--github_cache MIN",
56
- :description => "Max life-time for local cache files in minutes (default: 900)"
56
+ option :github_tmp,
57
+ :long => "--github_tmp PATH",
58
+ :description => "A path where temporary files for the diff function will be made default: /tmp/gitdiff)"
57
59
 
60
+ option :github_no_update,
61
+ :long => "--github_no_update",
62
+ :description => "Turn github update checking off",
63
+ :boolean => true
64
+
58
65
  def validate_base_options
59
66
  unless locate_config_value('github_url')
60
67
  ui.error "Github URL not specified"
@@ -64,22 +71,47 @@ class Chef
64
71
  ui.error "Github organization(s) not specified"
65
72
  exit 1
66
73
  end
74
+ unless locate_config_value('github_no_update')
75
+ check_gem_version
76
+ end
67
77
 
68
78
  @github_url = locate_config_value("github_url")
69
79
  @github_organizations = locate_config_value("github_organizations")
70
- @github_cache = (locate_config_value("github_cache") || 900).to_i
71
80
  @github_link = locate_config_value("github_link") || 'ssh'
72
81
  @github_api_version = locate_config_value("github_api_version") || 'v3'
73
82
  @github_ssl_verify_mode = locate_config_value("github_ssl_verify_mode") || 'verify_peer'
83
+ @github_tmp = locate_config_value("github_tmp") || '/var/tmp/gitdiff'
84
+ @github_tmp = "#{@github_tmp}#{Process.pid}"
85
+ end
86
+
87
+ def check_gem_version
88
+ url = 'http://rubygems.org/api/v1/gems/knife-github.json'
89
+ result = `curl -L -s #{url}`
90
+ begin
91
+ json = JSON.parse(result)
92
+ webversion = Mixlib::Versioning.parse(json['version'])
93
+ thisversion = Mixlib::Versioning.parse(::Knife::Github::VERSION)
94
+ if webversion > thisversion
95
+ ui.info "INFO: New version (#{webversion.to_s}) of knife-github is available!"
96
+ ui.info "INFO: Turn off this message with --github_no_update or add knife[:github_no_update] = true to your configuration"
97
+ end
98
+ Chef::Log.debug("local_gem_version : " + thisversion.to_s)
99
+ Chef::Log.debug("repo_gem_version : " + webversion.to_s)
100
+ Chef::Log.debug("repo_downloads : " + json['version_downloads'].to_s)
101
+ Chef::Log.debug("repo_total_downloads : " + json['downloads'].to_s)
102
+
103
+ rescue
104
+ ui.info "INFO: Cannot verify gem version information from rubygems.org"
105
+ ui.info "INFO: Turn off this message with --github_no_update or add knife[:github_no_update] = true to your configuration"
106
+ end
74
107
  end
75
108
 
76
109
  def display_debug_info
77
- Chef::Log.debug("github_url: " + @github_url.to_s)
78
- Chef::Log.debug("github_org: " + @github_organizations.to_s)
79
- Chef::Log.debug("github_api: " + @github_api_version.to_s)
80
- Chef::Log.debug("github_link: " + @github_link.to_s)
81
- Chef::Log.debug("github_cache: " + @github_cache.to_s)
82
- Chef::Log.debug("github_ssl_mode: " + @github_ssl_verify_mode.to_s)
110
+ Chef::Log.debug("github_url : " + @github_url.to_s)
111
+ Chef::Log.debug("github_org : " + @github_organizations.to_s)
112
+ Chef::Log.debug("github_api : " + @github_api_version.to_s)
113
+ Chef::Log.debug("github_link : " + @github_link.to_s)
114
+ Chef::Log.debug("github_ssl_mode : " + @github_ssl_verify_mode.to_s)
83
115
  end
84
116
 
85
117
  def locate_config_value(key)
@@ -87,8 +119,9 @@ class Chef
87
119
  config[key] || Chef::Config[:knife][key]
88
120
  end
89
121
 
90
- def get_github_link(link)
91
- git_link = case link
122
+ def get_repo_clone_link
123
+ link = locate_config_value('github_link')
124
+ repo_link = case link
92
125
  when 'ssh' then 'ssh_url'
93
126
  when 'http' then 'clone_url'
94
127
  when 'https' then 'clone_url'
@@ -97,34 +130,203 @@ class Chef
97
130
  when 'git' then 'git_url'
98
131
  else 'ssh_url'
99
132
  end
100
- git_link
133
+ return repo_link
134
+ end
135
+
136
+ def get_all_repos(orgs)
137
+ # Parse every org and merge all into one hash
138
+ repos = {}
139
+ orgs.each do |org|
140
+ get_repos(org).each { |repo| name = repo['name'] ; repos["#{name}"] = repo }
141
+ end
142
+ repos
143
+ end
144
+
145
+ def get_repos(org)
146
+ dns_name = get_dns_name(@github_url)
147
+ file_cache = "#{ENV['HOME']}/.chef/.#{dns_name.downcase}_#{org.downcase}"
148
+
149
+ if File.exists?(file_cache + ".json")
150
+ json = JSON.parse(File.read(file_cache + ".json"))
151
+ json_updated = Time.parse(json['updated_at'])
152
+ Chef::Log.info("#{org} - cache created at : " + json_updated.to_s)
153
+ repo_updated = get_org_updated_time(org)
154
+ Chef::Log.info("#{org} - repos updated at : " + repo_updated.to_s)
155
+
156
+ unless json_updated >= repo_updated
157
+ # update cache file
158
+ create_cache_file(file_cache + ".cache", org)
159
+ create_cache_json(file_cache + ".json", org)
160
+ end
161
+ else
162
+ create_cache_file(file_cache + ".cache", org)
163
+ create_cache_json(file_cache + ".json", org)
164
+ end
165
+
166
+ # use cache files
167
+ JSON.parse(File.read(file_cache + ".cache"))
168
+ end
169
+
170
+ def create_cache_json(file, org)
171
+ Chef::Log.debug("Updating the cache file: #{file}")
172
+ url = @github_url + "/api/" + @github_api_version + "/orgs/" + org
173
+ params = {'response' => 'json'}
174
+ result = send_request(url, params)
175
+ File.open(file, 'w') { |f| f.write(JSON.pretty_generate(result)) }
176
+ end
177
+
178
+ def create_cache_file(file, org)
179
+ Chef::Log.debug("Updating the cache file: #{file}")
180
+ result = get_repos_github(org)
181
+ File.open(file, 'w') { |f| f.write(JSON.pretty_generate(result)) }
182
+ end
183
+
184
+ def get_org_updated_time(org)
185
+ url = @github_url + "/api/" + @github_api_version + "/orgs/" + org
186
+ params = {'response' => 'json'}
187
+ result = send_request(url, params)
188
+ Time.parse(result['updated_at'])
189
+ end
190
+
191
+ def get_repos_github(org)
192
+ # Get all repo's for the org from github
193
+ arr = []
194
+ page = 1
195
+ url = @github_url + "/api/" + @github_api_version + "/orgs/" + org + "/repos"
196
+ while true
197
+ params = {'response' => 'json', 'page' => page }
198
+ result = send_request(url, params)
199
+ break if result.nil? || result.count < 1
200
+ result.each { |key|
201
+ if key['tags_url']
202
+ tags = get_tags(key)
203
+ key['tags'] = tags unless tags.nil? || tags.empty?
204
+ key['latest_tag'] = get_latest_tag(tags)
205
+ arr << key
206
+ else
207
+ arr << key
208
+ end
209
+ }
210
+ page = page + 1
211
+ end
212
+ arr
213
+ end
214
+
215
+ def get_tags(repo)
216
+ params = {'response' => 'json'}
217
+ tags = send_request(repo['tags_url'], params)
218
+ tags
219
+ end
220
+
221
+ def get_latest_tag(tags)
222
+ return "" if tags.nil? || tags.empty?
223
+ tags_arr =[]
224
+ tags.each do |tag|
225
+ tags_arr.push(Mixlib::Versioning.parse(tag['name'])) if tag['name'] =~ /^(\d*)\.(\d*)\.(\d*)$/
226
+ end
227
+ return "" if tags_arr.nil? || tags_arr.empty?
228
+ return tags_arr.sort.last.to_s
229
+ end
230
+
231
+ def get_dns_name(url)
232
+ url = url.downcase.gsub("http://","") if url.downcase.start_with?("http://")
233
+ url = url.downcase.gsub("https://","") if url.downcase.start_with?("https://")
234
+ url
101
235
  end
102
236
 
103
237
  def send_request(url, params = {})
104
- params['response'] = 'json'
105
-
106
- params_arr = []
107
- params.sort.each { |elem|
108
- params_arr << elem[0].to_s + '=' + CGI.escape(elem[1].to_s).gsub('+', '%20').gsub(' ','%20')
109
- }
110
- data = params_arr.join('&')
111
-
112
- github_url = "#{url}?#{data}"
113
- # Chef::Log.debug("URL: #{github_url}")
114
-
115
- uri = URI.parse(github_url)
238
+ unless params.empty?
239
+ params_arr = []
240
+ params.sort.each { |elem|
241
+ params_arr << elem[0].to_s + '=' + CGI.escape(elem[1].to_s).gsub('+', '%20').gsub(' ','%20')
242
+ }
243
+ data = params_arr.join('&')
244
+ url = "#{url}?#{data}"
245
+ end
246
+
247
+ if @github_ssl_verify_mode == "verify_none"
248
+ config[:ssl_verify_mode] = :verify_none
249
+ elsif @github_ssl_verify_mode == "verify_peer"
250
+ config[:ssl_verify_mode] = :verify_peer
251
+ end
252
+
253
+ Chef::Log.debug("URL: " + url.to_s)
254
+
255
+ uri = URI.parse(url)
116
256
  req_body = Net::HTTP::Get.new(uri.request_uri)
117
257
  request = Chef::REST::RESTRequest.new("GET", uri, req_body, headers={})
118
-
258
+
119
259
  response = request.call
120
-
121
- if !response.is_a?(Net::HTTPOK) then
260
+
261
+ unless response.is_a?(Net::HTTPOK) then
122
262
  puts "Error #{response.code}: #{response.message}"
123
263
  puts JSON.pretty_generate(JSON.parse(response.body))
124
264
  puts "URL: #{url}"
125
265
  exit 1
126
266
  end
127
- json = JSON.parse(response.body)
267
+
268
+ begin
269
+ json = JSON.parse(response.body)
270
+ rescue
271
+ ui.warn "The result on the RESTRequest is not in json format"
272
+ ui.warn "Output: " + response.body
273
+ exit 1
274
+ end
275
+ return json
276
+ end
277
+
278
+ def get_clone(url, cookbook)
279
+ if ! File.directory? @github_tmp
280
+ Dir.mkdir("#{@github_tmp}")
281
+ end
282
+ Dir.mkdir("#{@github_tmp}/git")
283
+ ui.info("Getting #{@cookbook_name} from #{url}")
284
+ output = `git clone #{url} #{@github_tmp}/git/#{cookbook} 2>&1`
285
+ if $?.exitstatus != 0
286
+ Chef::Log.error("Could not clone the repository for: #{cookbook}")
287
+ FileUtils.remove_entry(@github_tmp)
288
+ exit 1
289
+ end
290
+ return true
291
+ end
292
+
293
+ def add_tag(version)
294
+ cpath = cookbook_path_valid?(@cookbook_name, false)
295
+ Dir.chdir(cpath)
296
+ Chef::Log.debug "Adding tag"
297
+ output = `git tag -a "#{version}" -m "Added tag #{version}" 2>&1`
298
+ if $?.exitstatus != 0
299
+ Chef::Log.error("Could not add tag for: #{@cookbook_name}")
300
+ FileUtils.remove_entry(@github_tmp)
301
+ exit 1
302
+ end
303
+ end
304
+
305
+ def cookbook_path_valid?(cookbook_name, check_exists)
306
+ cookbook_path = config[:cookbook_path] || Chef::Config[:cookbook_path]
307
+ if cookbook_path.nil? || cookbook_path.empty?
308
+ Chef::Log.error("Please specify a cookbook path")
309
+ exit 1
310
+ end
311
+
312
+ unless File.exists?(cookbook_path.first) && File.directory?(cookbook_path.first)
313
+ Chef::Log.error("Cannot find the directory: #{cookbook_path.first}")
314
+ exit 1
315
+ end
316
+
317
+ cookbook_path = File.join(cookbook_path.first,cookbook_name)
318
+ if check_exists
319
+ if File.exists?(cookbook_path)
320
+ ui.info("Processing [S] #{cookbook_name}")
321
+ Chef::Log.info("Path to #{cookbook_path} already exists, skipping.")
322
+ return nil
323
+ end
324
+ else
325
+ if ! File.exists?(cookbook_path)
326
+ return nil
327
+ end
328
+ end
329
+ return cookbook_path
128
330
  end
129
331
 
130
332
  end
@@ -0,0 +1,102 @@
1
+ #
2
+ # Author:: Sander Botman (<sbotman@schubergphilis.com>)
3
+ # Copyright:: Copyright (c) 2013 Sander Botman.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+
20
+ require 'chef/knife'
21
+
22
+ class Chef
23
+ class Knife
24
+ module GithubBaseList
25
+
26
+ def self.included(includer)
27
+ includer.class_eval do
28
+
29
+ option :fields,
30
+ :long => "--fields 'NAME, NAME'",
31
+ :description => "The fields to output, comma-separated"
32
+
33
+ option :fieldlist,
34
+ :long => "--fieldlist",
35
+ :description => "The available fields to output/filter",
36
+ :boolean => true
37
+
38
+ option :noheader,
39
+ :long => "--noheader",
40
+ :description => "Removes header from output",
41
+ :boolean => true
42
+
43
+ def display_info(data, columns, match = [])
44
+ object_list = []
45
+
46
+ if config[:fields]
47
+ config[:fields].split(',').each { |n| object_list << ui.color(("#{n}").capitalize.strip, :bold) }
48
+ else
49
+ columns.each { |c| r = c.split(","); object_list << ui.color(("#{r.last}").strip, :bold) }
50
+ end
51
+
52
+ col = object_list.count
53
+ object_list = [] if config[:noheader]
54
+
55
+ data.each do |k,v|
56
+ if config[:fields]
57
+ config[:fields].downcase.split(',').each { |n| object_list << ((v["#{n}".strip]).to_s || 'n/a') }
58
+ else
59
+ color = :white
60
+ if config[:mismatch] && !match.empty? && !config[:all]
61
+ matches = []; match.each { |m| matches << v[m].to_s }
62
+ next if matches.uniq.count == 1
63
+ color = :yellow
64
+ end
65
+ columns.each { |c| r = c.split(","); object_list << ui.color((v["#{r.first}"]).to_s, color) || 'n/a' }
66
+ end
67
+ end
68
+
69
+ puts ui.list(object_list, :uneven_columns_across, col)
70
+ display_object_fields(data) if locate_config_value(:fieldlist)
71
+ end
72
+
73
+ def display_object_fields(object)
74
+ exit 1 if object.nil? || object.empty?
75
+ object_fields = [
76
+ ui.color('Key', :bold),
77
+ ui.color('Type', :bold),
78
+ ui.color('Value', :bold)
79
+ ]
80
+ object.first.each do |n|
81
+ if n.class == Hash
82
+ n.keys.each do |k,v|
83
+ object_fields << ui.color(k, :yellow, :bold)
84
+ object_fields << n[k].class.to_s
85
+ if n[k].kind_of?(Array)
86
+ object_fields << '<Array>'
87
+ elsif n[k].kind_of?(Hash)
88
+ object_fields << '<Hash>'
89
+ else
90
+ object_fields << ("#{n[k]}").strip.to_s
91
+ end
92
+ end
93
+ end
94
+ end
95
+ puts "\n"
96
+ puts ui.list(object_fields, :uneven_columns_across, 3)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,120 @@
1
+ #
2
+ # Author:: Sander Botman (<sbotman@schubergphilis.com>)
3
+ # Copyright:: Copyright (c) 2013 Sander Botman.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ # ---------------------------------------------------------------------------- #
19
+ # Abstract
20
+ # ---------------------------------------------------------------------------- #
21
+ # This code is intended to help you cleaning up your locate repo's.
22
+ # It will check if your repo if insync with the github and will not touch if
23
+ # this is not the case. Then it will check if you have any branches local
24
+ # and not on the github.
25
+ #
26
+ # If it cannot find any uncommitted changes, it will safely remove your repo.
27
+ # It's good practice to cleanup and re-download repos because this way they
28
+ # can move from organization to organization.
29
+ # ---------------------------------------------------------------------------- #
30
+ #
31
+ require 'chef/knife'
32
+
33
+ module KnifeGithubCleanup
34
+ class GithubCleanup < Chef::Knife
35
+
36
+ deps do
37
+ require 'chef/knife/github_base'
38
+ include Chef::Knife::GithubBase
39
+ require 'chef/mixin/shell_out'
40
+ end
41
+
42
+ banner "knife github cleanup [REPO] (options)"
43
+ category "github"
44
+
45
+ option :all,
46
+ :short => "-a",
47
+ :long => "--all",
48
+ :description => "Delete all repos from cookbook path.",
49
+ :boolean => true
50
+
51
+ option :force,
52
+ :short => "-f",
53
+ :long => "--force",
54
+ :description => "Force deletion even if commits are still present.",
55
+ :boolean => true
56
+
57
+ def run
58
+
59
+ #executing shell commands
60
+ extend Chef::Mixin::ShellOut
61
+
62
+ # validate base options from base module.
63
+ validate_base_options
64
+
65
+ # Display information if debug mode is on.
66
+ display_debug_info
67
+
68
+ # Gather all repo information from github.
69
+ all_repos = get_all_repos(@github_organizations.reverse)
70
+
71
+ # Get all chef cookbooks and versions (hopefully chef does the error handeling).
72
+ cookbooks = rest.get_rest("/cookbooks?num_version=1")
73
+
74
+ # Get the cookbook names from the command line
75
+ @cookbook_name = name_args.first unless name_args.empty?
76
+ if @cookbook_name
77
+ # repo = all_repos.select { |k,v| v["name"] == @cookbook_name }
78
+ repo_cleanup(@cookbook_name)
79
+ elsif config[:all]
80
+ cookbooks.each do |c,v|
81
+ repo_cleanup(c)
82
+ end
83
+ else
84
+ Chef::Log.error("Please specify a repo name")
85
+ end
86
+ end
87
+
88
+ def repo_cleanup(repo)
89
+ cookbook_path = config[:cookbook_path] || Chef::Config[:cookbook_path]
90
+ cookbook = File.join(cookbook_path.first,repo)
91
+ if File.exists?(cookbook)
92
+ if repo_status_clean?(repo, cookbook)
93
+ # delete the repo
94
+ ui.info("Processing [D] #{repo}")
95
+ FileUtils.remove_entry(cookbook)
96
+ end
97
+ else
98
+ puts "cannot find repo path for: #{repo}" unless config[:all]
99
+ end
100
+ end
101
+
102
+ def repo_status_clean?(repo, cookbook)
103
+ shell_out!("git fetch", :cwd => cookbook)
104
+ status = shell_out!("git status", :cwd => cookbook)
105
+ unless status.stdout == "# On branch master\nnothing to commit (working directory clean)\n"
106
+ ui.info("Processing [C] #{repo} (Action needed!)")
107
+ status.stdout.lines.each { |l| puts l.sub( /^/, " ") }
108
+ return false
109
+ end
110
+ log = shell_out!("git log --branches --not --remotes --simplify-by-decoration --decorate --oneline", :cwd => cookbook)
111
+ unless log.stdout.empty?
112
+ ui.info("Processing [B] #{repo} (Action needed!)")
113
+ ui.info(" Please check your branches, one of them has unsaved changes")
114
+ log.stdout.lines.each { |l| puts l.sub( /^/, " ") }
115
+ return false
116
+ end
117
+ return true
118
+ end
119
+ end
120
+ end