Merge branch '6592-retry-if-cleanupfail' closes #6592
[arvados.git] / docker / api / update-gitolite.rb
1 #!/usr/bin/env ruby
2
3 require 'rubygems'
4 require 'pp'
5 require 'arvados'
6 require 'tempfile'
7 require 'yaml'
8 require 'fileutils'
9
10 # This script does the actual gitolite config management on disk.
11 #
12 # Ward Vandewege <ward@curoverse.com>
13
14 # Default is development
15 production = ARGV[0] == "production"
16
17 ENV["RAILS_ENV"] = "development"
18 ENV["RAILS_ENV"] = "production" if production
19
20 DEBUG = 1
21
22 # load and merge in the environment-specific application config info
23 # if present, overriding base config parameters as specified
24 path = File.dirname(__FILE__) + '/config/arvados-clients.yml'
25 if File.exists?(path) then
26   cp_config = YAML.load_file(path)[ENV['RAILS_ENV']]
27 else
28   puts "Please create a\n " + File.dirname(__FILE__) + "/config/arvados-clients.yml\n file"
29   exit 1
30 end
31
32 gitolite_url = cp_config['gitolite_url']
33 gitolite_arvados_git_user_key = cp_config['gitolite_arvados_git_user_key']
34
35 gitolite_tmpdir = File.join(File.absolute_path(File.dirname(__FILE__)),
36                             cp_config['gitolite_tmp'])
37 gitolite_admin = File.join(gitolite_tmpdir, 'gitolite-admin')
38 gitolite_admin_keydir = File.join(gitolite_admin, 'keydir')
39 gitolite_keydir = File.join(gitolite_admin, 'keydir', 'arvados')
40
41 ENV['ARVADOS_API_HOST'] = cp_config['arvados_api_host']
42 ENV['ARVADOS_API_TOKEN'] = cp_config['arvados_api_token']
43 if cp_config['arvados_api_host_insecure']
44   ENV['ARVADOS_API_HOST_INSECURE'] = 'true'
45 else
46   ENV.delete('ARVADOS_API_HOST_INSECURE')
47 end
48
49 def ensure_directory(path, mode)
50   begin
51     Dir.mkdir(path, mode)
52   rescue Errno::EEXIST
53   end
54 end
55
56 def replace_file(path, contents)
57   unlink_now = true
58   dirname, basename = File.split(path)
59   FileUtils.mkpath(dirname)
60   new_file = Tempfile.new([basename, ".tmp"], dirname)
61   begin
62     new_file.write(contents)
63     new_file.flush
64     File.rename(new_file, path)
65     unlink_now = false
66   ensure
67     new_file.close(unlink_now)
68   end
69 end
70
71 def file_has_contents?(path, contents)
72   begin
73     IO.read(path) == contents
74   rescue Errno::ENOENT
75     false
76   end
77 end
78
79 module TrackCommitState
80   module ClassMethods
81     # Note that all classes that include TrackCommitState will have
82     # @@need_commit = true if any of them set it.  Since this flag reports
83     # a boolean state of the underlying git repository, that's OK in the
84     # current implementation.
85     @@need_commit = false
86
87     def changed?
88       @@need_commit
89     end
90
91     def ensure_in_git(path, contents)
92       unless file_has_contents?(path, contents)
93         replace_file(path, contents)
94         system("git", "add", path)
95         @@need_commit = true
96       end
97     end
98   end
99
100   def ensure_in_git(path, contents)
101     self.class.ensure_in_git(path, contents)
102   end
103
104   def self.included(base)
105     base.extend(ClassMethods)
106   end
107 end
108
109 class UserSSHKeys
110   include TrackCommitState
111
112   def initialize(user_keys_map, key_dir)
113     @user_keys_map = user_keys_map
114     @key_dir = key_dir
115     @installed = {}
116   end
117
118   def install(filename, pubkey)
119     unless pubkey.nil?
120       key_path = File.join(@key_dir, filename)
121       ensure_in_git(key_path, pubkey)
122     end
123     @installed[filename] = true
124   end
125
126   def ensure_keys_for_user(user_uuid)
127     return unless key_list = @user_keys_map.delete(user_uuid)
128     key_list.map { |k| k[:public_key] }.compact.each_with_index do |pubkey, ii|
129       # Handle putty-style ssh public keys
130       pubkey.sub!(/^(Comment: "r[^\n]*\n)(.*)$/m,'ssh-rsa \2 \1')
131       pubkey.sub!(/^(Comment: "d[^\n]*\n)(.*)$/m,'ssh-dss \2 \1')
132       pubkey.gsub!(/\n/,'')
133       pubkey.strip!
134       install("#{user_uuid}@#{ii}.pub", pubkey)
135     end
136   end
137
138   def installed?(filename)
139     @installed[filename]
140   end
141 end
142
143 class Repository
144   include TrackCommitState
145
146   @@aliases = {}
147
148   def initialize(arv_repo, user_keys)
149     @arv_repo = arv_repo
150     @user_keys = user_keys
151   end
152
153   def self.ensure_system_config(conf_root)
154     ensure_in_git(File.join(conf_root, "conf", "gitolite.conf"),
155                   %Q{include "auto/*.conf"\ninclude "admin/*.conf"\n})
156     ensure_in_git(File.join(conf_root, "arvadosaliases.pl"), alias_config)
157
158     conf_path = File.join(conf_root, "conf", "admin", "arvados.conf")
159     conf_file = %Q{
160 @arvados_git_user = arvados_git_user
161
162 repo gitolite-admin
163      RW           = @arvados_git_user
164
165 }
166     ensure_directory(File.dirname(conf_path), 0755)
167     ensure_in_git(conf_path, conf_file)
168   end
169
170   def ensure_config(conf_root)
171     if name and (File.exist?(auto_conf_path(conf_root, name)))
172       # This gitolite installation knows the repository by name, rather than
173       # UUID.  Leave it configured that way until a separate migration is run.
174       basename = name
175     else
176       basename = uuid
177       @@aliases[name] = uuid unless name.nil?
178     end
179     conf_file = "\nrepo #{basename}\n"
180     @arv_repo[:user_permissions].sort.each do |user_uuid, perm|
181       conf_file += "\t#{perm[:gitolite_permissions]}\t= #{user_uuid}\n"
182       @user_keys.ensure_keys_for_user(user_uuid)
183     end
184     ensure_in_git(auto_conf_path(conf_root, basename), conf_file)
185   end
186
187   private
188
189   def auto_conf_path(conf_root, basename)
190     File.join(conf_root, "conf", "auto", "#{basename}.conf")
191   end
192
193   def uuid
194     @arv_repo[:uuid]
195   end
196
197   def name
198     if @arv_repo[:name].nil?
199       nil
200     else
201       @clean_name ||=
202         @arv_repo[:name].sub(/^[^A-Za-z]+/, "").gsub(/[^\w\.\/]/, "")
203     end
204   end
205
206   def self.alias_config
207     conf_s = "{\n"
208     @@aliases.sort.each do |(repo_name, repo_uuid)|
209       conf_s += "\t'#{repo_name}' \t=> '#{repo_uuid}',\n"
210     end
211     conf_s += "};\n"
212     conf_s
213   end
214 end
215
216 begin
217   # Get our local gitolite-admin repo up to snuff
218   if not File.exists?(gitolite_admin) then
219     ensure_directory(gitolite_tmpdir, 0700)
220     Dir.chdir(gitolite_tmpdir)
221     `git clone #{gitolite_url}`
222     Dir.chdir(gitolite_admin)
223   else
224     Dir.chdir(gitolite_admin)
225     `git pull`
226   end
227
228   arv = Arvados.new
229   permissions = arv.repository.get_all_permissions
230
231   ensure_directory(gitolite_keydir, 0700)
232   admin_user_ssh_keys = UserSSHKeys.new(permissions[:user_keys], gitolite_admin_keydir)
233   # Make sure the arvados_git_user key is installed; put it in gitolite_admin_keydir
234   # because that is where gitolite will try to put it if we do not.
235   admin_user_ssh_keys.install('arvados_git_user.pub', gitolite_arvados_git_user_key)
236
237   user_ssh_keys = UserSSHKeys.new(permissions[:user_keys], gitolite_keydir)
238   permissions[:repositories].each do |repo_record|
239     repo = Repository.new(repo_record, user_ssh_keys)
240     repo.ensure_config(gitolite_admin)
241   end
242   Repository.ensure_system_config(gitolite_admin)
243
244   # Clean up public key files that should not be present
245   Dir.chdir(gitolite_keydir)
246   stale_keys = Dir.glob('*.pub').reject do |key_file|
247     user_ssh_keys.installed?(key_file)
248   end
249   if stale_keys.any?
250     stale_keys.each { |key_file| puts "Extra file #{key_file}" }
251     system("git", "rm", "--quiet", *stale_keys)
252   end
253
254   if UserSSHKeys.changed? or Repository.changed? or stale_keys.any?
255     message = "#{Time.now().to_s}: update from API"
256     Dir.chdir(gitolite_admin)
257     `git add --all`
258     `git commit -m '#{message}'`
259     `git push`
260   end
261
262 rescue => bang
263   puts "Error: " + bang.to_s
264   puts bang.backtrace.join("\n")
265   exit 1
266 end
267