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