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