forked from instructure/canvas-lms
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker_composer.rb
More file actions
272 lines (229 loc) · 8.16 KB
/
Copy pathdocker_composer.rb
File metadata and controls
272 lines (229 loc) · 8.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require "yaml"
require "fileutils"
require "English"
Thread.abort_on_exception = true
ENV["RAILS_ENV"] = "test"
DEFAULT_COMPOSE_PROJECT_NAME = "canvas"
ENV["COMPOSE_PROJECT_NAME"] ||= DEFAULT_COMPOSE_PROJECT_NAME
ENV["PGVERSION"] ||= "9.5"
ENV["BASE_DOCKER_VOLUME_ARCHIVE"] ||= ""
# max of any build type
ENV["MASTER_RUNNERS"] = "6" if ENV["PUBLISH_DOCKER_ARTIFACTS"]
ENV["PREPARE_TEST_DATABASE"] ||= "1"
DOCKER_CACHE_S3_BUCKET = ENV.fetch("DOCKER_CACHE_S3_BUCKET")
DOCKER_CACHE_S3_REGION = ENV.fetch("DOCKER_CACHE_S3_REGION")
if ENV["VERBOSE"] != "1"
$stderr = STDERR.clone
STDOUT.reopen "/dev/null"
STDERR.reopen "/dev/null"
end
class DockerComposer
class << self
# :cry: not the same as canvas docker-compose ... yet
RUNNER_IDS = Array.new(ENV['MASTER_RUNNERS'].to_i) { |i| i == 0 ? "" : i + 1 }
COMPOSE_FILES = %w[docker-compose.yml docker-compose.override.yml docker-compose.jenkins.yml]
def run
nuke_old_crap
pull_cached_images
launch_services
migrate if run_migrations?
push_artifacts if push_artifacts?
dump_structure if ENV["DUMP_STRUCTURE"]
ensure
nuke_old_crap if ENV["COMPOSE_PROJECT_NAME"] != DEFAULT_COMPOSE_PROJECT_NAME
end
def nuke_old_crap
docker_compose "kill"
docker_compose "rm -fv"
docker "volume rm $(docker volume ls -q | grep #{ENV["COMPOSE_PROJECT_NAME"]}_) || :"
end
def pull_cached_images
return if File.exist?("/tmp/.canvas_pulled_built_images")
pull_simple_images
pull_built_images
FileUtils.touch "/tmp/.canvas_pulled_built_images"
end
def pull_simple_images
puts "Pulling simple images..."
simple_services.each do |key|
docker "pull #{service_config(key)["image"]}"
end
end
# port of dockerBase image caching
def pull_built_images
puts "Pulling built images..."
built_services.each do |key|
path = "s3://#{DOCKER_CACHE_S3_BUCKET}/canvas-lms/canvas_#{key}-ci.tar"
system "aws s3 cp --only-show-errors --region #{DOCKER_CACHE_S3_REGION} #{path} - | docker load || :"
end
end
# port of dockerBase image caching
def push_built_images
puts "Pushing built images..."
built_services.each do |key|
name = "canvas_#{key}"
path = "s3://#{DOCKER_CACHE_S3_BUCKET}/canvas-lms/canvas_#{key}-ci.tar"
system "docker save #{name} $(docker history -q #{name} | grep -v missing) | aws s3 cp --only-show-errors --region #{DOCKER_CACHE_S3_REGION} - #{path}"
end
end
# e.g. postgres
def built_services
services.select { |key| service_config(key)["build"] }
end
# e.g. redis
def simple_services
services - built_services
end
def launch_services
docker_compose "build #{services.join(" ")}"
prepare_volumes
start_services
db_prepare unless using_snapshot? # already set up, just need to migrate
end
def dump_structure
dump_file = "/tmp/#{ENV["COMPOSE_PROJECT_NAME"]}_structure.sql"
File.delete dump_file if File.exist?(dump_file)
FileUtils.touch dump_file
docker "exec -i #{ENV["COMPOSE_PROJECT_NAME"]}_postgres_1 pg_dump -s -x -O -U postgres -n public canvas_test_ > #{dump_file}"
end
# data_loader will fetch postgres + cassandra volumes from s3, if
# there are any (BASE_DOCKER_VOLUME_ARCHIVE), so that the db is all
# migrated and ready to go
def prepare_volumes
docker_compose_up "data_loader"
wait_for "data_loader"
end
def db_prepare
# pg db setup happens in create-dbs.sh (when we docker-compose up),
# but cassandra doesn't have a similar hook
cassandra_setup
end
def cassandra_setup
puts "Creating keyspaces..."
docker "exec -i #{ENV["COMPOSE_PROJECT_NAME"]}_cassandra_1 /create-keyspaces #{cassandra_keyspaces.join(" ")}"
end
def cassandra_keyspaces
YAML.load_file("config/cassandra.yml")["test"].keys
end
# each service can define its own /wait-for-it
def wait_for(service)
docker "exec -i #{ENV["COMPOSE_PROJECT_NAME"]}_#{service}_1 sh -c \"[ ! -x /wait-for-it ] || /wait-for-it\""
end
def docker_compose_up(services = self.services.join(" "))
docker_compose "up -d #{services} && docker ps"
end
def wait_for_services
parallel_each(services) { |service| wait_for service }
end
def stop_services
docker_compose "stop #{(services - ["data_loader"]).join(" ")} && docker ps"
end
def start_services
puts "Starting all services..."
docker_compose_up
wait_for_services
end
def migrate
puts "Running migrations..."
tasks = []
tasks << "ci:disable_structure_dump"
tasks << "db:migrate"
tasks << "ci:prepare_test_shards" if ENV["PREPARE_TEST_DATABASE"] == "1"
tasks << "ci:discard_past_quiz_event_partitions"
tasks << "canvas:quizzes:create_event_partitions"
tasks << "ci:reset_database" if ENV["PREPARE_TEST_DATABASE"] == "1"
tasks = tasks.join(" ")
parallel_each(RUNNER_IDS) do |runner_id|
result = `TEST_ENV_NUMBER=#{runner_id} bundle exec rake #{tasks} 2>&1`
if $CHILD_STATUS != 0
$stderr.puts "ERROR: Migrations failed!\n\nLast 1000 lines:"
$stderr.puts result.lines.last(1000).join
exit(1)
end
end
end
# push the docker volume archives for the worker nodes, and possibly
# also push the built images and the path to the volume archives if
# this is a post-merge build
def push_artifacts
puts "Pushing artifacts..."
stop_services # shut it down cleanly before we commit
archive_path = ENV["PUSH_DOCKER_VOLUME_ARCHIVE"]
publish_vars_path = "s3://#{DOCKER_CACHE_S3_BUCKET}/canvas-lms/docker_vars/#{ENV["PGVERSION"]}/" + `git rev-parse HEAD` if publish_artifacts?
docker "exec -i #{ENV["COMPOSE_PROJECT_NAME"]}_data_loader_1 /push-volumes #{archive_path} #{publish_vars_path}"
push_built_images if publish_artifacts?
start_services
end
# opt-in by default, worker nodes explicitly opt out (since we'll have
# just done it on the master)
def run_migrations?
ENV["RUN_MIGRATIONS"] != "0"
end
# master does this so slave will be fast
def push_artifacts?
ENV["PUSH_DOCKER_VOLUME_ARCHIVE"]
end
# post-merge only
def publish_artifacts?
ENV["PUBLISH_DOCKER_ARTIFACTS"]
end
def using_snapshot?
!ENV["BASE_DOCKER_VOLUME_ARCHIVE"].empty?
end
def parallel_each(items)
items.map { |item| Thread.new { yield item } }.map(&:join)
end
def docker(args)
system "docker #{args}" or raise("`docker #{args}` failed")
end
def docker_compose(args)
file_args = COMPOSE_FILES.map { |f| "-f #{f}" }.join(" ")
system "docker-compose #{file_args} #{args}" or raise("`docker-compose #{args}` failed")
end
def service_config(key)
config["services"][key]
end
def services
own_config["services"].keys
end
def own_config
@own_config ||= YAML.load_file(COMPOSE_FILES.last)
end
def config
@config ||= begin
merger = proc do |key, v1, v2|
Hash === v1 && Hash === v2 ?
v1.merge(v2, &merger) :
Array === v1 && Array === v2 ?
v1.concat(v2) :
v2
end
COMPOSE_FILES.inject({}) do |config, file|
config.merge(YAML.load_file(file), &merger)
end
end
end
end
end
begin
DockerComposer.run
rescue
$stderr.puts "ERROR: #{$ERROR_INFO}"
exit 1
end