blob: c148aead4494df1e43077311bf66cfabcf380978 [file] [log] [blame]
# Copyright 2018 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Helper script to deploy the App Engine application."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import contextlib
import os
import jinja2
from chromite.lib import commandline
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import osutils
# The templates' names.
_APP_TEMPLATE_YAML = 'app_template.yaml'
_APP_YAML = 'app.yaml'
_OPENAPI_TEMPLATE_YAML = 'openapi-appengine_template.yaml'
_OPENAPI_YAML = 'openapi-appengine.yaml'
@contextlib.contextmanager
def _PrepareAppFolder(options):
"""Copies this folder and its symlink'd dependencies into a temporary dir.
Returns:
A contextmanager that yields a temporary directory and cleans up afterward.
"""
with osutils.TempDir() as tempdir:
# This is rsync in 'archive' mode, but symlinks are followed to copy actual
# files/directories.
rsync_cmd = ['rsync', '-qrLgotD',
'--exclude', '*.pyc',
'--exclude', '__pycache__',
'--exclude', '*.git']
for path in options.skip_paths:
rsync_cmd.extend(['--exclude', path])
cros_build_lib.RunCommand(rsync_cmd + ['.', tempdir],
cwd=options.project_path)
yield tempdir
def _GenerateAppYaml(tempdir, app_template, injected_keys):
"""Generates app.yaml by filling a template.
Args:
tempdir: The tempdir where the project was copied to.
app_template: A string file name, indicating the template to fill in.
injected_keys: A dict, indicating the keys to be injected.
"""
env = jinja2.Environment(loader=jinja2.FileSystemLoader(tempdir))
contents = env.get_template(app_template).stream(injected_keys)
contents.dump(os.path.join(tempdir, _APP_YAML))
def _GenerateOpenAPIYaml(tempdir, openapi_template, injected_keys):
"""Generates openapi-appengine.yaml by filling a template.
Args:
tempdir: The tempdir where the project was copied to.
openapi_template: A string file name, indicating he template to fill in.
injected_keys: A dict, indicating the keys to be injected.
"""
env = jinja2.Environment(loader=jinja2.FileSystemLoader(tempdir))
contents = env.get_template(openapi_template).stream(injected_keys)
contents.dump(os.path.join(tempdir, _OPENAPI_YAML))
def _DeployEndpoint(tempdir, options):
"""Deploy Endpoint Service.
Args:
tempdir: The tempdir where the project was copied to.
options: The parsed options to deploy.
"""
logging.info('Deploying Endpoint:')
injected_keys = {'endpoints_service': options.endpoints_service}
_GenerateOpenAPIYaml(tempdir, options.openapi_template, injected_keys)
cros_build_lib.RunCommand(
['gcloud', 'endpoints', 'services', 'deploy', _OPENAPI_YAML,
'--project=%s' % options.project_id],
cwd=tempdir,
mute_output=False)
def _GetNewestEndpoint(tempdir, endpoints_service):
"""Get the newest deployed Endpoint id.
Args:
tempdir: The tempdir where the project was copied to.
endpoints_service: The deployed endpoint service.
Returns:
A string, indicating the newest endpoint config or None if no endpoints
service is required.
"""
if not endpoints_service:
return None
result = cros_build_lib.RunCommand(
['gcloud', 'endpoints', 'configs', 'list',
'--service=%s' % endpoints_service, '--limit=1'], cwd=tempdir,
capture_output=True)
logging.info('Endpoints configs list:\n%s', result.output)
config = result.output.splitlines()[1]
return config.split(' ')[0].strip()
def _GenerateRequirements(tempdir):
"""Generates a requirements.txt file.
Appengine uses the requirements.txt file to build the docker image.
Args:
tempdir: The tempdir where the project was copied to.
"""
path = os.path.join(tempdir, 'requirements.txt')
cros_build_lib.RunCommand(['pipenv', 'lock', '-r'],
log_stdout_to_file=path, cwd=tempdir)
def _DeployApp(tempdir, config_id, options):
"""Deploy GAE App Service.
Args:
tempdir: The tempdir where the project was copied to.
config_id: the most recent endpoint id for this app. If it's None, it means
no endpoint service is needed for this app.
options: The parsed options to deploy.
"""
logging.info('Deploying GAE app:')
injected_keys = {}
if config_id:
injected_keys = {'endpoints_service': options.endpoints_service,
'endpoints_service_config_id': config_id}
_GenerateAppYaml(tempdir, options.app_template, injected_keys)
_GenerateRequirements(tempdir)
cros_build_lib.RunCommand(
['gcloud', 'app', 'deploy', _APP_YAML,
'--project=%s' % options.project_id],
cwd=tempdir, mute_output=False)
def _MakeParser():
"""Return parser for deploy_app."""
parser = commandline.ArgumentParser()
parser.add_argument(
'--project_path', help='The location of the project to deploy',
required=True)
parser.add_argument(
'--project_id', help='The project to deploy', required=True)
parser.add_argument(
'--app_template', help='The app template to fill in',
default=_APP_TEMPLATE_YAML)
parser.add_argument(
'--skip_paths', action='append', help='The paths to skip in deployment')
parser.add_argument(
'--endpoints_service', help='The endpoint service to deploy')
parser.add_argument(
'--openapi_template', help='The openapi template to fill in',
default=_OPENAPI_TEMPLATE_YAML)
parser.add_argument(
'--skip_endpoint', help='Skip endpoint deployment.', action='store_true')
parser.add_argument(
'--skip_app', help='Skip app deployment.', action='store_true')
return parser
def _VerifyOptions(options):
"""Verify the passed-in options.
Args:
options: The parsed options to verify.
Returns:
Boolean, True if verification passes, False otherwise.
"""
if options.endpoints_service and not options.openapi_template:
logging.error('Please specify openAPI template with --openapi_template '
'in deploying endpoints.')
return False
if options.openapi_template and not options.endpoints_service:
logging.error('Please specify endpoints service with --endpoints_service '
'in deploying endpoints.')
return False
if (options.endpoints_service and
options.project_id not in options.endpoints_service):
logging.error('The project "%s" is not matched to the endpoints service '
'"%s".', options.project_id, options.endpoints_service)
return False
return True
def main(argv):
"""Deploys the endpoints & app."""
deploy_parser = _MakeParser()
options = deploy_parser.parse_args(argv)
if not _VerifyOptions(options):
deploy_parser.print_help()
return
with _PrepareAppFolder(options) as tempdir:
logging.info('Deployment directory: %s', tempdir)
if options.endpoints_service and not options.skip_endpoint:
_DeployEndpoint(tempdir, options)
if not options.skip_app:
endpoint_config_id = _GetNewestEndpoint(tempdir,
options.endpoints_service)
_DeployApp(tempdir, endpoint_config_id, options)