blob: 224acb01431e6e6bf7e54cc1cda9c7b60cc6944d [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Manages a Go VM Runtime process running inside of a docker container.
"""
import logging
import os
import shutil
import tempfile
import google
from google.appengine.tools.devappserver2 import go_application
from google.appengine.tools.devappserver2 import instance
from google.appengine.tools.devappserver2 import vm_runtime_proxy
DEBUG_PORT = 5858
VM_SERVICE_PORT = 8181
DEFAULT_DOCKER_FILE = """FROM google/golang
ADD . /app
RUN /app/_ah/build.sh
EXPOSE 8080
CMD []
WORKDIR /app
ENTRYPOINT ["/app/_ah/exe"]
"""
# Where to look for go-app-builder, which is needed for copying
# into the Docker image for building the Go App Engine app.
# There is no need to add '.exe' here because it is always a Linux executable.
_GO_APP_BUILDER = os.path.join(
go_application.GOROOT, 'pkg', 'tool', 'linux_amd64', 'go-app-builder')
class GoVMRuntimeProxy(instance.RuntimeProxy):
"""Manages a Go VM Runtime process running inside of a docker container.
The Go VM Runtime forwards all requests to the Go application instance.
"""
def __init__(self, docker_client, runtime_config_getter,
module_configuration):
"""Initializer for VMRuntimeProxy.
Args:
docker_client: docker.Client object to communicate with Docker daemon.
runtime_config_getter: A function that can be called without arguments
and returns the runtime_config_pb2.Config containing the configuration
for the runtime.
module_configuration: An application_configuration.ModuleConfiguration
instance respresenting the configuration of the module that owns the
runtime.
"""
super(GoVMRuntimeProxy, self).__init__()
self._runtime_config_getter = runtime_config_getter
self._module_configuration = module_configuration
port_bindings = {
DEBUG_PORT: None,
VM_SERVICE_PORT: None,
}
self._vm_runtime_proxy = vm_runtime_proxy.VMRuntimeProxy(
docker_client=docker_client,
runtime_config_getter=runtime_config_getter,
module_configuration=module_configuration,
port_bindings=port_bindings)
def handle(self, environ, start_response, url_map, match, request_id,
request_type):
"""Handle request to Go runtime.
Serves this request by forwarding to the Go application instance via
HttpProxy.
Args:
environ: An environ dict for the request as defined in PEP-333.
start_response: A function with semantics defined in PEP-333.
url_map: An appinfo.URLMap instance containing the configuration for the
handler matching this request.
match: A re.MatchObject containing the result of the matched URL pattern.
request_id: A unique string id associated with the request.
request_type: The type of the request. See instance.*_REQUEST module
constants.
Yields:
A sequence of strings containing the body of the HTTP response.
"""
it = self._vm_runtime_proxy.handle(environ, start_response, url_map,
match, request_id, request_type)
for data in it:
yield data
def start(self):
logging.info('Starting Go VM Deployment process')
try:
application_dir = os.path.abspath(
self._module_configuration.application_root)
# - copy the application to a new temporary directory (follow symlinks)
# - copy used parts of $GOPATH to the temporary directory
# - copy or create a Dockerfile in the temporary directory
# - build & deploy the docker container
with TempDir('go_deployment_dir') as temp_dir:
dst_deployment_dir = temp_dir
dst_application_dir = temp_dir
try:
_copytree(application_dir, dst_application_dir,
self._module_configuration.skip_files)
except shutil.Error as e:
logging.error('Error copying tree: %s', e)
for src, unused_dst, unused_error in e.args[0]:
if os.path.islink(src):
linkto = os.readlink(src)
if not os.path.exists(linkto):
logging.error('Dangling symlink in Go project. '
'Path %s links to %s', src, os.readlink(src))
raise
except OSError as e:
logging.error('Failed to copy dir: %s', e.strerror)
raise
extras = go_application.get_app_extras_for_vm(
self._module_configuration)
for dest, src in extras:
try:
dest = os.path.join(dst_deployment_dir, dest)
dirname = os.path.dirname(dest)
if not os.path.exists(dirname):
os.makedirs(dirname)
shutil.copy(src, dest)
except OSError as e:
logging.error('Failed to copy %s to %s', src, dest)
raise
# Make the _ah subdirectory for the app engine tools.
ah_dir = os.path.join(dst_deployment_dir, '_ah')
try:
os.mkdir(ah_dir)
except OSError as e:
logging.error('Failed to create %s: %s', ah_dir, e.strerror)
raise
# Copy gab.
try:
gab_dest = os.path.join(ah_dir, 'gab')
shutil.copy(_GO_APP_BUILDER, gab_dest)
except OSError as e:
logging.error('Failed to copy %s to %s', _GO_APP_BUILDER, gab_dest)
raise
# Write build script.
nobuild_files = '^' + str(self._module_configuration.nobuild_files)
gab_args = [
'/app/_ah/gab',
'-app_base', '/app',
'-arch', '6',
'-dynamic',
'-goroot', '/goroot',
'-nobuild_files', nobuild_files,
'-unsafe',
'-binary_name', '_ah_exe',
'-work_dir', '/tmp/work',
'-vm',
]
gab_args.extend(
go_application.list_go_files(self._module_configuration))
gab_args.extend([x[0] for x in extras])
dst_build = os.path.join(ah_dir, 'build.sh')
with open(dst_build, 'w') as fd:
fd.write('#!/bin/bash\n')
fd.write('set -e\n')
fd.write('mkdir -p /tmp/work\n')
fd.write(' '.join(gab_args) + '\n')
fd.write('mv /tmp/work/_ah_exe /app/_ah/exe\n')
fd.write('rm -rf /tmp/work\n')
fd.write('echo Done.\n')
os.chmod(dst_build, 0777)
# Write default Dockerfile if none found.
dst_dockerfile = os.path.join(dst_application_dir, 'Dockerfile')
if not os.path.exists(dst_dockerfile):
with open(dst_dockerfile, 'w') as fd:
fd.write(DEFAULT_DOCKER_FILE)
self._vm_runtime_proxy.start(dockerfile_dir=dst_deployment_dir)
logging.info(
'GoVM vmservice available at http://127.0.0.1:%s/ !',
self._vm_runtime_proxy.PortBinding(VM_SERVICE_PORT))
except Exception as e:
logging.info('Go VM Deployment process failed: %s', str(e))
raise
def quit(self):
self._vm_runtime_proxy.quit()
class TempDir(object):
"""Creates a temporary directory."""
def __init__(self, prefix=''):
self._temp_dir = None
self._prefix = prefix
def __enter__(self):
self._temp_dir = tempfile.mkdtemp(self._prefix)
return self._temp_dir
def __exit__(self, *_):
shutil.rmtree(self._temp_dir, ignore_errors=True)
def _copytree(src, dst, skip_files, symlinks=False):
"""Copies src tree to dst (except those matching skip_files).
Args:
src: string name of source directory to copy from.
dst: string name of destination directory to copy to.
skip_files: RegexStr of files to skip from appinfo.py.
symlinks: optional bool determines if symbolic links are followed.
"""
# Ignore files that match the skip_files RegexStr.
# TODO: skip_files expects the full path relative to the app root, so
# this may need fixing.
def ignored_files(unused_dir, filenames):
return [filename for filename in filenames if skip_files.match(filename)]
for item in os.listdir(src):
s = os.path.join(src, item)
if skip_files.match(item):
logging.info('skipping file %s', s)
continue
d = os.path.join(dst, item)
if os.path.isdir(s):
shutil.copytree(s, d, symlinks, ignore=ignored_files)
else:
shutil.copy2(s, d)