blob: c2568efba0fa6abf9750360d68f56dd0b9fe0cf3 [file] [log] [blame]
Yong Nib2e4bfa2017-05-09 18:12:10 -07001#!/usr/bin/env python2.7
Jan Tattermusch7897ae92017-06-07 22:57:36 +02002# Copyright 2017 gRPC authors.
Yong Nib2e4bfa2017-05-09 18:12:10 -07003#
Jan Tattermusch7897ae92017-06-07 22:57:36 +02004# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
Yong Nib2e4bfa2017-05-09 18:12:10 -07007#
Jan Tattermusch7897ae92017-06-07 22:57:36 +02008# http://www.apache.org/licenses/LICENSE-2.0
Yong Nib2e4bfa2017-05-09 18:12:10 -07009#
Jan Tattermusch7897ae92017-06-07 22:57:36 +020010# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
Yong Nib2e4bfa2017-05-09 18:12:10 -070015"""Build and upload docker images to Google Container Registry per matrix."""
16
17from __future__ import print_function
18
19import argparse
20import atexit
21import multiprocessing
22import os
23import shutil
24import subprocess
25import sys
26import tempfile
27
28# Langauage Runtime Matrix
29import client_matrix
30
ncteisene4bef082017-12-11 16:51:34 -080031python_util_dir = os.path.abspath(
32 os.path.join(os.path.dirname(__file__), '../run_tests/python_utils'))
Yong Nib2e4bfa2017-05-09 18:12:10 -070033sys.path.append(python_util_dir)
34import dockerjob
35import jobset
36
37_IMAGE_BUILDER = 'tools/run_tests/dockerize/build_interop_image.sh'
38_LANGUAGES = client_matrix.LANG_RUNTIME_MATRIX.keys()
39# All gRPC release tags, flattened, deduped and sorted.
ncteisene4bef082017-12-11 16:51:34 -080040_RELEASES = sorted(
41 list(
42 set(
43 client_matrix.get_release_tag_name(info)
44 for lang in client_matrix.LANG_RELEASE_MATRIX.values()
45 for info in lang)))
Yong Nib2e4bfa2017-05-09 18:12:10 -070046
47# Destination directory inside docker image to keep extra info from build time.
48_BUILD_INFO = '/var/local/build_info'
49
50argp = argparse.ArgumentParser(description='Run interop tests.')
ncteisene4bef082017-12-11 16:51:34 -080051argp.add_argument(
52 '--gcr_path',
53 default='gcr.io/grpc-testing',
54 help='Path of docker images in Google Container Registry')
Yong Nib2e4bfa2017-05-09 18:12:10 -070055
ncteisene4bef082017-12-11 16:51:34 -080056argp.add_argument(
57 '--release',
58 default='master',
59 choices=['all', 'master'] + _RELEASES,
60 help='github commit tag to checkout. When building all '
61 'releases defined in client_matrix.py, use "all". Valid only '
62 'with --git_checkout.')
Yong Nib2e4bfa2017-05-09 18:12:10 -070063
ncteisene4bef082017-12-11 16:51:34 -080064argp.add_argument(
65 '-l',
66 '--language',
67 choices=['all'] + sorted(_LANGUAGES),
68 nargs='+',
69 default=['all'],
70 help='Test languages to build docker images for.')
Yong Nib2e4bfa2017-05-09 18:12:10 -070071
ncteisene4bef082017-12-11 16:51:34 -080072argp.add_argument(
73 '--git_checkout',
74 action='store_true',
75 help='Use a separate git clone tree for building grpc stack. '
76 'Required when using --release flag. By default, current'
77 'tree and the sibling will be used for building grpc stack.')
Yong Nib2e4bfa2017-05-09 18:12:10 -070078
ncteisene4bef082017-12-11 16:51:34 -080079argp.add_argument(
80 '--git_checkout_root',
81 default='/export/hda3/tmp/grpc_matrix',
82 help='Directory under which grpc-go/java/main repo will be '
83 'cloned. Valid only with --git_checkout.')
Yong Nib2e4bfa2017-05-09 18:12:10 -070084
ncteisene4bef082017-12-11 16:51:34 -080085argp.add_argument(
86 '--keep',
87 action='store_true',
88 help='keep the created local images after uploading to GCR')
Yong Nib2e4bfa2017-05-09 18:12:10 -070089
ncteisene4bef082017-12-11 16:51:34 -080090argp.add_argument(
91 '--reuse_git_root',
92 default=False,
93 action='store_const',
94 const=True,
95 help='reuse the repo dir. If False, the existing git root '
96 'directory will removed before a clean checkout, because '
97 'reusing the repo can cause git checkout error if you switch '
98 'between releases.')
Yong Nib2e4bfa2017-05-09 18:12:10 -070099
Jan Tattermuschc093a7f2018-05-30 17:43:56 +0200100argp.add_argument(
101 '--upload_images',
102 action='store_true',
Jan Tattermuschf1f456b2018-05-31 11:45:44 +0200103 help='If set, images will be uploaded to container registry after building.'
104)
Jan Tattermuschc093a7f2018-05-30 17:43:56 +0200105
Yong Nib2e4bfa2017-05-09 18:12:10 -0700106args = argp.parse_args()
107
ncteisene4bef082017-12-11 16:51:34 -0800108
Yong Nib2e4bfa2017-05-09 18:12:10 -0700109def add_files_to_image(image, with_files, label=None):
ncteisene4bef082017-12-11 16:51:34 -0800110 """Add files to a docker image.
Yong Nib2e4bfa2017-05-09 18:12:10 -0700111
112 image: docker image name, i.e. grpc_interop_java:26328ad8
113 with_files: additional files to include in the docker image.
114 label: label string to attach to the image.
115 """
ncteisene4bef082017-12-11 16:51:34 -0800116 tag_idx = image.find(':')
117 if tag_idx == -1:
118 jobset.message(
119 'FAILED', 'invalid docker image %s' % image, do_newline=True)
120 sys.exit(1)
121 orig_tag = '%s_' % image
122 subprocess.check_output(['docker', 'tag', image, orig_tag])
Yong Nib2e4bfa2017-05-09 18:12:10 -0700123
ncteisene4bef082017-12-11 16:51:34 -0800124 lines = ['FROM ' + orig_tag]
125 if label:
126 lines.append('LABEL %s' % label)
Yong Nib2e4bfa2017-05-09 18:12:10 -0700127
ncteisene4bef082017-12-11 16:51:34 -0800128 temp_dir = tempfile.mkdtemp()
129 atexit.register(lambda: subprocess.call(['rm', '-rf', temp_dir]))
Yong Nib2e4bfa2017-05-09 18:12:10 -0700130
ncteisene4bef082017-12-11 16:51:34 -0800131 # Copy with_files inside the tmp directory, which will be the docker build
132 # context.
133 for f in with_files:
134 shutil.copy(f, temp_dir)
135 lines.append('COPY %s %s/' % (os.path.basename(f), _BUILD_INFO))
Yong Nib2e4bfa2017-05-09 18:12:10 -0700136
ncteisene4bef082017-12-11 16:51:34 -0800137 # Create a Dockerfile.
138 with open(os.path.join(temp_dir, 'Dockerfile'), 'w') as f:
139 f.write('\n'.join(lines))
Yong Nib2e4bfa2017-05-09 18:12:10 -0700140
ncteisene4bef082017-12-11 16:51:34 -0800141 jobset.message('START', 'Repackaging %s' % image, do_newline=True)
142 build_cmd = ['docker', 'build', '--rm', '--tag', image, temp_dir]
143 subprocess.check_output(build_cmd)
144 dockerjob.remove_image(orig_tag, skip_nonexistent=True)
145
Yong Nib2e4bfa2017-05-09 18:12:10 -0700146
Adele Zhoubcd23cd2017-11-01 15:42:00 -0700147def build_image_jobspec(runtime, env, gcr_tag, stack_base):
ncteisene4bef082017-12-11 16:51:34 -0800148 """Build interop docker image for a language with runtime.
Yong Nib2e4bfa2017-05-09 18:12:10 -0700149
150 runtime: a <lang><version> string, for example go1.8.
151 env: dictionary of env to passed to the build script.
152 gcr_tag: the tag for the docker image (i.e. v1.3.0).
Adele Zhoubcd23cd2017-11-01 15:42:00 -0700153 stack_base: the local gRPC repo path.
Yong Nib2e4bfa2017-05-09 18:12:10 -0700154 """
ncteisene4bef082017-12-11 16:51:34 -0800155 basename = 'grpc_interop_%s' % runtime
156 tag = '%s/%s:%s' % (args.gcr_path, basename, gcr_tag)
157 build_env = {'INTEROP_IMAGE': tag, 'BASE_NAME': basename, 'TTY_FLAG': '-t'}
158 build_env.update(env)
159 image_builder_path = _IMAGE_BUILDER
160 if client_matrix.should_build_docker_interop_image_from_release_tag(lang):
161 image_builder_path = os.path.join(stack_base, _IMAGE_BUILDER)
162 build_job = jobset.JobSpec(
163 cmdline=[image_builder_path],
164 environ=build_env,
165 shortname='build_docker_%s' % runtime,
166 timeout_seconds=30 * 60)
167 build_job.tag = tag
168 return build_job
169
Yong Nib2e4bfa2017-05-09 18:12:10 -0700170
171def build_all_images_for_lang(lang):
ncteisene4bef082017-12-11 16:51:34 -0800172 """Build all docker images for a language across releases and runtimes."""
173 if not args.git_checkout:
174 if args.release != 'master':
Jan Tattermuschf1f456b2018-05-31 11:45:44 +0200175 print(
176 'Cannot use --release without also enabling --git_checkout.\n')
Jan Tattermuschc093a7f2018-05-30 17:43:56 +0200177 sys.exit(1)
178 releases = [args.release]
Yong Nib2e4bfa2017-05-09 18:12:10 -0700179 else:
ncteisene4bef082017-12-11 16:51:34 -0800180 if args.release == 'all':
181 releases = client_matrix.get_release_tags(lang)
182 else:
183 # Build a particular release.
Mehrdad Afshari87cd9942018-01-02 14:40:00 -0800184 if args.release not in ['master'
185 ] + client_matrix.get_release_tags(lang):
ncteisene4bef082017-12-11 16:51:34 -0800186 jobset.message(
187 'SKIPPED',
188 '%s for %s is not defined' % (args.release, lang),
189 do_newline=True)
190 return []
191 releases = [args.release]
Yong Nib2e4bfa2017-05-09 18:12:10 -0700192
ncteisene4bef082017-12-11 16:51:34 -0800193 images = []
194 for release in releases:
195 images += build_all_images_for_release(lang, release)
196 jobset.message(
197 'SUCCESS',
198 'All docker images built for %s at %s.' % (lang, releases),
199 do_newline=True)
200 return images
201
Yong Nib2e4bfa2017-05-09 18:12:10 -0700202
203def build_all_images_for_release(lang, release):
ncteisene4bef082017-12-11 16:51:34 -0800204 """Build all docker images for a release across all runtimes."""
205 docker_images = []
206 build_jobs = []
Yong Nib2e4bfa2017-05-09 18:12:10 -0700207
ncteisene4bef082017-12-11 16:51:34 -0800208 env = {}
209 # If we not using current tree or the sibling for grpc stack, do checkout.
210 stack_base = ''
211 if args.git_checkout:
212 stack_base = checkout_grpc_stack(lang, release)
213 var = {
214 'go': 'GRPC_GO_ROOT',
215 'java': 'GRPC_JAVA_ROOT',
216 'node': 'GRPC_NODE_ROOT'
217 }.get(lang, 'GRPC_ROOT')
218 env[var] = stack_base
Yong Nib2e4bfa2017-05-09 18:12:10 -0700219
ncteisene4bef082017-12-11 16:51:34 -0800220 for runtime in client_matrix.LANG_RUNTIME_MATRIX[lang]:
221 job = build_image_jobspec(runtime, env, release, stack_base)
222 docker_images.append(job.tag)
223 build_jobs.append(job)
Yong Nib2e4bfa2017-05-09 18:12:10 -0700224
ncteisene4bef082017-12-11 16:51:34 -0800225 jobset.message('START', 'Building interop docker images.', do_newline=True)
226 print('Jobs to run: \n%s\n' % '\n'.join(str(j) for j in build_jobs))
Yong Nib2e4bfa2017-05-09 18:12:10 -0700227
ncteisene4bef082017-12-11 16:51:34 -0800228 num_failures, _ = jobset.run(
229 build_jobs,
230 newline_on_success=True,
231 maxjobs=multiprocessing.cpu_count())
232 if num_failures:
233 jobset.message(
234 'FAILED', 'Failed to build interop docker images.', do_newline=True)
235 docker_images_cleanup.extend(docker_images)
236 sys.exit(1)
Yong Nib2e4bfa2017-05-09 18:12:10 -0700237
ncteisene4bef082017-12-11 16:51:34 -0800238 jobset.message(
239 'SUCCESS',
240 'All docker images built for %s at %s.' % (lang, release),
241 do_newline=True)
Yong Nib2e4bfa2017-05-09 18:12:10 -0700242
ncteisene4bef082017-12-11 16:51:34 -0800243 if release != 'master':
244 commit_log = os.path.join(stack_base, 'commit_log')
245 if os.path.exists(commit_log):
246 for image in docker_images:
247 add_files_to_image(image, [commit_log], 'release=%s' % release)
248 return docker_images
249
Yong Nib2e4bfa2017-05-09 18:12:10 -0700250
251def cleanup():
ncteisene4bef082017-12-11 16:51:34 -0800252 if not args.keep:
253 for image in docker_images_cleanup:
254 dockerjob.remove_image(image, skip_nonexistent=True)
255
Yong Nib2e4bfa2017-05-09 18:12:10 -0700256
257docker_images_cleanup = []
258atexit.register(cleanup)
259
ncteisene4bef082017-12-11 16:51:34 -0800260
Alex Polcyn84263292017-11-15 00:32:50 +0000261def maybe_apply_patches_on_git_tag(stack_base, lang, release):
ncteisene4bef082017-12-11 16:51:34 -0800262 files_to_patch = []
263 for release_info in client_matrix.LANG_RELEASE_MATRIX[lang]:
264 if client_matrix.get_release_tag_name(release_info) == release:
Menghan Licaada992017-12-12 17:46:21 -0800265 if release_info[release] is not None:
266 files_to_patch = release_info[release].get('patch')
267 break
ncteisene4bef082017-12-11 16:51:34 -0800268 if not files_to_patch:
269 return
270 patch_file_relative_path = 'patches/%s_%s/git_repo.patch' % (lang, release)
271 patch_file = os.path.abspath(
272 os.path.join(os.path.dirname(__file__), patch_file_relative_path))
273 if not os.path.exists(patch_file):
ncteisen173c4772017-12-11 16:52:44 -0800274 jobset.message('FAILED',
275 'expected patch file |%s| to exist' % patch_file)
ncteisene4bef082017-12-11 16:51:34 -0800276 sys.exit(1)
Alex Polcyn84263292017-11-15 00:32:50 +0000277 subprocess.check_output(
ncteisene4bef082017-12-11 16:51:34 -0800278 ['git', 'apply', patch_file], cwd=stack_base, stderr=subprocess.STDOUT)
Jan Tattermuschd851d822018-05-30 18:59:51 +0200279
280 # TODO(jtattermusch): this really would need simplification and refactoring
281 # - "git add" and "git commit" can easily be done in a single command
282 # - it looks like the only reason for the existence of the "files_to_patch"
283 # entry is to perform "git add" - which is clumsy and fragile.
284 # - we only allow a single patch with name "git_repo.patch". A better design
285 # would be to allow multiple patches that can have more descriptive names.
ncteisene4bef082017-12-11 16:51:34 -0800286 for repo_relative_path in files_to_patch:
287 subprocess.check_output(
288 ['git', 'add', repo_relative_path],
289 cwd=stack_base,
290 stderr=subprocess.STDOUT)
291 subprocess.check_output(
292 [
293 'git', 'commit', '-m',
294 ('Hack performed on top of %s git '
295 'tag in order to build and run the %s '
296 'interop tests on that tag.' % (lang, release))
297 ],
Alex Polcyn84263292017-11-15 00:32:50 +0000298 cwd=stack_base,
299 stderr=subprocess.STDOUT)
ncteisene4bef082017-12-11 16:51:34 -0800300
Alex Polcyn84263292017-11-15 00:32:50 +0000301
Yong Nib2e4bfa2017-05-09 18:12:10 -0700302def checkout_grpc_stack(lang, release):
ncteisene4bef082017-12-11 16:51:34 -0800303 """Invokes 'git check' for the lang/release and returns directory created."""
304 assert args.git_checkout and args.git_checkout_root
Yong Nib2e4bfa2017-05-09 18:12:10 -0700305
ncteisene4bef082017-12-11 16:51:34 -0800306 if not os.path.exists(args.git_checkout_root):
307 os.makedirs(args.git_checkout_root)
Yong Nib2e4bfa2017-05-09 18:12:10 -0700308
ncteisene4bef082017-12-11 16:51:34 -0800309 repo = client_matrix.get_github_repo(lang)
310 # Get the subdir name part of repo
311 # For example, 'git@github.com:grpc/grpc-go.git' should use 'grpc-go'.
312 repo_dir = os.path.splitext(os.path.basename(repo))[0]
313 stack_base = os.path.join(args.git_checkout_root, repo_dir)
Yong Nib2e4bfa2017-05-09 18:12:10 -0700314
ncteisene4bef082017-12-11 16:51:34 -0800315 # Clean up leftover repo dir if necessary.
316 if not args.reuse_git_root and os.path.exists(stack_base):
317 jobset.message('START', 'Removing git checkout root.', do_newline=True)
318 shutil.rmtree(stack_base)
Adele Zhoua3664682017-11-14 17:30:32 -0800319
ncteisene4bef082017-12-11 16:51:34 -0800320 if not os.path.exists(stack_base):
321 subprocess.check_call(
322 ['git', 'clone', '--recursive', repo],
323 cwd=os.path.dirname(stack_base))
Yong Nib2e4bfa2017-05-09 18:12:10 -0700324
ncteisene4bef082017-12-11 16:51:34 -0800325 # git checkout.
326 jobset.message(
327 'START',
328 'git checkout %s from %s' % (release, stack_base),
329 do_newline=True)
330 # We should NEVER do checkout on current tree !!!
331 assert not os.path.dirname(__file__).startswith(stack_base)
332 output = subprocess.check_output(
333 ['git', 'checkout', release], cwd=stack_base, stderr=subprocess.STDOUT)
334 maybe_apply_patches_on_git_tag(stack_base, lang, release)
335 commit_log = subprocess.check_output(['git', 'log', '-1'], cwd=stack_base)
336 jobset.message(
337 'SUCCESS',
338 'git checkout',
339 '%s: %s' % (str(output), commit_log),
340 do_newline=True)
Yong Nib2e4bfa2017-05-09 18:12:10 -0700341
ncteisene4bef082017-12-11 16:51:34 -0800342 # Write git log to commit_log so it can be packaged with the docker image.
343 with open(os.path.join(stack_base, 'commit_log'), 'w') as f:
344 f.write(commit_log)
345 return stack_base
346
Yong Nib2e4bfa2017-05-09 18:12:10 -0700347
348languages = args.language if args.language != ['all'] else _LANGUAGES
349for lang in languages:
ncteisene4bef082017-12-11 16:51:34 -0800350 docker_images = build_all_images_for_lang(lang)
351 for image in docker_images:
Jan Tattermuschc093a7f2018-05-30 17:43:56 +0200352 if args.upload_images:
353 jobset.message('START', 'Uploading %s' % image, do_newline=True)
354 # docker image name must be in the format <gcr_path>/<image>:<gcr_tag>
355 assert image.startswith(args.gcr_path) and image.find(':') != -1
356 subprocess.call(['gcloud', 'docker', '--', 'push', image])
357 else:
358 # Uploading (and overwriting images) by default can easily break things.
Jan Tattermuschf1f456b2018-05-31 11:45:44 +0200359 print('Not uploading image %s, run with --upload_images to upload.'
360 % image)