21700: Install Bundler system-wide in Rails postinst
[arvados.git] / services / api / script / migrate-gitolite-to-uuid-storage.rb
1 #!/usr/bin/env ruby
2 # Copyright (C) The Arvados Authors. All rights reserved.
3 #
4 # SPDX-License-Identifier: AGPL-3.0
5
6 #
7 # Prior to April 2015, Arvados Gitolite integration stored repositories by
8 # name.  To improve user repository management, we switched to storing
9 # repositories by UUID, and aliasing them to names.  This makes it easy to
10 # have rich name hierarchies, and allow users to rename repositories.
11 #
12 # This script will migrate a name-based Gitolite configuration to a UUID-based
13 # one.  To use it:
14 #
15 # 1. Change the value of REPOS_DIR below, if needed.
16 # 2. Install this script in the same directory as `update-gitolite.rb`.
17 # 3. Ensure that no *other* users can access Gitolite: edit gitolite's
18 #    authorized_keys file so it only contains the arvados_git_user key,
19 #    and disable the update-gitolite cron job.
20 # 4. Run this script: `ruby migrate-gitolite-to-uuid-storage.rb production`.
21 # 5. Undo step 3.
22
23 require 'rubygems'
24 require 'pp'
25 require 'arvados'
26 require 'tempfile'
27 require 'yaml'
28
29 REPOS_DIR = "/var/lib/gitolite/repositories"
30
31 # Default is development
32 production = ARGV[0] == "production"
33
34 ENV["RAILS_ENV"] = "development"
35 ENV["RAILS_ENV"] = "production" if production
36
37 DEBUG = 1
38
39 # load and merge in the environment-specific application config info
40 # if present, overriding base config parameters as specified
41 path = File.dirname(__FILE__) + '/config/arvados-clients.yml'
42 if File.exist?(path) then
43   cp_config = File.open(path) do |f|
44     YAML.safe_load(f, filename: path)[ENV['RAILS_ENV']]
45   end
46 else
47   puts "Please create a\n " + File.dirname(__FILE__) + "/config/arvados-clients.yml\n file"
48   exit 1
49 end
50
51 gitolite_url = cp_config['gitolite_url']
52 gitolite_arvados_git_user_key = cp_config['gitolite_arvados_git_user_key']
53
54 gitolite_tmpdir = File.join(File.absolute_path(File.dirname(__FILE__)),
55                             cp_config['gitolite_tmp'])
56 gitolite_admin = File.join(gitolite_tmpdir, 'gitolite-admin')
57 gitolite_keydir = File.join(gitolite_admin, 'keydir', 'arvados')
58
59 ENV['ARVADOS_API_HOST'] = cp_config['arvados_api_host']
60 ENV['ARVADOS_API_TOKEN'] = cp_config['arvados_api_token']
61 if cp_config['arvados_api_host_insecure']
62   ENV['ARVADOS_API_HOST_INSECURE'] = 'true'
63 else
64   ENV.delete('ARVADOS_API_HOST_INSECURE')
65 end
66
67 def ensure_directory(path, mode)
68   begin
69     Dir.mkdir(path, mode)
70   rescue Errno::EEXIST
71   end
72 end
73
74 def replace_file(path, contents)
75   unlink_now = true
76   dirname, basename = File.split(path)
77   new_file = Tempfile.new([basename, ".tmp"], dirname)
78   begin
79     new_file.write(contents)
80     new_file.flush
81     File.rename(new_file, path)
82     unlink_now = false
83   ensure
84     new_file.close(unlink_now)
85   end
86 end
87
88 def file_has_contents?(path, contents)
89   begin
90     IO.read(path) == contents
91   rescue Errno::ENOENT
92     false
93   end
94 end
95
96 module TrackCommitState
97   module ClassMethods
98     # Note that all classes that include TrackCommitState will have
99     # @@need_commit = true if any of them set it.  Since this flag reports
100     # a boolean state of the underlying git repository, that's OK in the
101     # current implementation.
102     @@need_commit = false
103
104     def changed?
105       @@need_commit
106     end
107
108     def ensure_in_git(path, contents)
109       unless file_has_contents?(path, contents)
110         replace_file(path, contents)
111         system("git", "add", path)
112         @@need_commit = true
113       end
114     end
115   end
116
117   def ensure_in_git(path, contents)
118     self.class.ensure_in_git(path, contents)
119   end
120
121   def self.included(base)
122     base.extend(ClassMethods)
123   end
124 end
125
126 class Repository
127   include TrackCommitState
128
129   @@aliases = {}
130
131   def initialize(arv_repo)
132     @arv_repo = arv_repo
133   end
134
135   def self.ensure_system_config(conf_root)
136     ensure_in_git(File.join(conf_root, "arvadosaliases.pl"), alias_config)
137   end
138
139   def self.rename_repos(repos_root)
140     @@aliases.each_pair do |uuid, name|
141       begin
142         File.rename(File.join(repos_root, "#{name}.git/"),
143                     File.join(repos_root, "#{uuid}.git"))
144       rescue Errno::ENOENT
145       end
146       if name == "arvados"
147         Dir.chdir(repos_root) { File.symlink("#{uuid}.git/", "arvados.git") }
148       end
149     end
150   end
151
152   def ensure_config(conf_root)
153     return if name.nil?
154     @@aliases[uuid] = name
155     name_conf_path = auto_conf_path(conf_root, name)
156     return unless File.exist?(name_conf_path)
157     conf_file = IO.read(name_conf_path)
158     conf_file.gsub!(/^repo #{Regexp.escape(name)}$/m, "repo #{uuid}")
159     ensure_in_git(auto_conf_path(conf_root, uuid), conf_file)
160     File.unlink(name_conf_path)
161     system("git", "rm", "--quiet", name_conf_path)
162   end
163
164   private
165
166   def auto_conf_path(conf_root, basename)
167     File.join(conf_root, "conf", "auto", "#{basename}.conf")
168   end
169
170   def uuid
171     @arv_repo[:uuid]
172   end
173
174   def name
175     if @arv_repo[:name].nil?
176       nil
177     else
178       @clean_name ||=
179         @arv_repo[:name].sub(/^[^A-Za-z]+/, "").gsub(/[^\w\.\/]/, "")
180     end
181   end
182
183   def self.alias_config
184     conf_s = "{\n"
185     @@aliases.sort.each do |(repo_name, repo_uuid)|
186       conf_s += "\t'#{repo_name}' \t=> '#{repo_uuid}',\n"
187     end
188     conf_s += "};\n"
189     conf_s
190   end
191 end
192
193 begin
194   # Get our local gitolite-admin repo up to snuff
195   if not File.exist?(gitolite_admin) then
196     ensure_directory(gitolite_tmpdir, 0700)
197     Dir.chdir(gitolite_tmpdir)
198     `git clone #{gitolite_url}`
199     Dir.chdir(gitolite_admin)
200   else
201     Dir.chdir(gitolite_admin)
202     `git pull`
203   end
204
205   arv = Arvados.new
206   permissions = arv.repository.get_all_permissions
207
208   permissions[:repositories].each do |repo_record|
209     repo = Repository.new(repo_record)
210     repo.ensure_config(gitolite_admin)
211   end
212   Repository.ensure_system_config(gitolite_admin)
213
214   message = "#{Time.now().to_s}: migrate to storing repositories by UUID"
215   Dir.chdir(gitolite_admin)
216   `git add --all`
217   `git commit -m '#{message}'`
218   Repository.rename_repos(REPOS_DIR)
219   `git push`
220
221 rescue => bang
222   puts "Error: " + bang.to_s
223   puts bang.backtrace.join("\n")
224   exit 1
225 end
226