knife-github 0.0.3 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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