223 lines
7.4 KiB
Python
223 lines
7.4 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2023 Google LLC. All Rights Reserved.
|
|
#
|
|
# 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.
|
|
"""Utilities used by gcloud functions local development."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import json
|
|
import re
|
|
import textwrap
|
|
from googlecloudsdk.core import exceptions as core_exceptions
|
|
from googlecloudsdk.core import execution_utils
|
|
from googlecloudsdk.core.util import files
|
|
|
|
import six
|
|
|
|
_INSTALLATION_GUIDE = textwrap.dedent("""\
|
|
You must install Docker and Pack to run this command.
|
|
To install Docker and Pack, please follow this guide:
|
|
https://cloud.google.com/functions/1stgendocs/running/functions-emulator""")
|
|
_DOCKER = files.FindExecutableOnPath('docker')
|
|
_PACK = files.FindExecutableOnPath('pack')
|
|
_APPENGINE_BUILDER = 'gcr.io/serverless-runtimes/google-{}-full/builder/{}'
|
|
_V1_BUILDER = 'gcr.io/buildpacks/builder:v1'
|
|
_GOOGLE_22_BUILDER = 'gcr.io/buildpacks/builder:google-22'
|
|
_RUNTIME_MINVERSION_UBUNTU_22 = {'python': 310, 'nodejs': 18, 'go': 116,
|
|
'java': 17, 'php': 82, 'ruby': 32,
|
|
'dotnet': 6}
|
|
|
|
|
|
class MissingExecutablesException(core_exceptions.Error):
|
|
"""Executables for local development are not found."""
|
|
|
|
|
|
class ContainerNotFoundException(core_exceptions.Error):
|
|
"""Docker container is not found."""
|
|
|
|
|
|
class DockerExecutionException(core_exceptions.Error):
|
|
"""Docker executable exited with non-zero code."""
|
|
|
|
|
|
class PackExecutionException(core_exceptions.Error):
|
|
"""Pack executable exited with non-zero code."""
|
|
|
|
|
|
def ValidateDependencies():
|
|
if _DOCKER is None or _PACK is None:
|
|
raise MissingExecutablesException(_INSTALLATION_GUIDE)
|
|
|
|
|
|
def RunPack(name, builder, runtime, entry_point, path, build_env_vars):
|
|
"""Runs Pack Build with the command built from arguments of the command parser.
|
|
|
|
Args:
|
|
name: Name of the image build.
|
|
builder: Name of the builder by the flag.
|
|
runtime: Runtime specified by flag.
|
|
entry_point: Entry point of the function specified by flag.
|
|
path: Source of the zip file.
|
|
build_env_vars: Build environment variables.
|
|
Raises:
|
|
PackExecutionException: if the exit code of the execution is non-zero.
|
|
"""
|
|
pack_cmd = [_PACK, 'build', '--builder']
|
|
|
|
# Always use language-specific builder when builder not provided.
|
|
if not builder:
|
|
[language, version] = re.findall(r'(\D+|\d+)', runtime)
|
|
if int(version) >= _RUNTIME_MINVERSION_UBUNTU_22[language]:
|
|
builder = (_GOOGLE_22_BUILDER if language == 'dotnet'
|
|
else _APPENGINE_BUILDER.format(22, language))
|
|
else:
|
|
builder = (_V1_BUILDER if language == 'dotnet'
|
|
else _APPENGINE_BUILDER.format(18, language))
|
|
pack_cmd.append(builder)
|
|
|
|
if build_env_vars:
|
|
_AddEnvVars(pack_cmd, build_env_vars)
|
|
|
|
pack_cmd.extend(['--env', 'GOOGLE_FUNCTION_TARGET=' + entry_point])
|
|
pack_cmd.extend(['--path', path])
|
|
pack_cmd.extend(['-q', name])
|
|
status = execution_utils.Exec(pack_cmd, no_exit=True)
|
|
if status:
|
|
raise PackExecutionException(
|
|
status, 'Pack failed to build the container image.')
|
|
|
|
|
|
def RunDockerContainer(name, port, env_vars, labels):
|
|
"""Runs the Docker container (detached mode) with specified port and name.
|
|
|
|
If the name already exists, it will be removed.
|
|
|
|
Args:
|
|
name: The name of the container to run.
|
|
port: The port for the container to run on.
|
|
env_vars: The container environment variables.
|
|
labels: Docker labels to store flags and environment variables.
|
|
|
|
Raises:
|
|
DockerExecutionException: if the exit code of the execution is non-zero.
|
|
"""
|
|
if ContainerExists(name):
|
|
RemoveDockerContainer(name)
|
|
docker_cmd = [_DOCKER, 'run', '-d']
|
|
docker_cmd.extend(['-p', six.text_type(port) + ':8080'])
|
|
if env_vars:
|
|
_AddEnvVars(docker_cmd, env_vars)
|
|
for k, v in labels.items():
|
|
docker_cmd.extend(['--label', '{}={}'.format(k, json.dumps(v))])
|
|
docker_cmd.extend(['--name', name, name])
|
|
status = execution_utils.Exec(docker_cmd, no_exit=True)
|
|
if status:
|
|
raise DockerExecutionException(
|
|
status, 'Docker failed to run container ' + name)
|
|
|
|
|
|
def RemoveDockerContainer(name):
|
|
"""Removes the Docker container with specified name.
|
|
|
|
Args:
|
|
name: The name of the Docker container to delete.
|
|
|
|
Raises:
|
|
DockerExecutionException: if the exit code of the execution is non-zero.
|
|
"""
|
|
delete_cmd = [_DOCKER, 'rm', '-f', name]
|
|
status = execution_utils.Exec(delete_cmd, no_exit=True)
|
|
if status:
|
|
raise DockerExecutionException(
|
|
status, 'Docker failed to execute: failed to remove container ' + name)
|
|
|
|
|
|
def ContainerExists(name):
|
|
"""Returns True if the Docker container with specified name exists.
|
|
|
|
Args:
|
|
name: The name of the Docker container.
|
|
|
|
Returns:
|
|
bool: True if the container exists, False otherwise.
|
|
|
|
Raises:
|
|
DockerExecutionException: if the exit code of the execution is non-zero.
|
|
"""
|
|
list_cmd = [_DOCKER, 'ps', '-q', '-f', 'name=' + name]
|
|
out = []
|
|
capture_out = lambda stdout: out.append(stdout.strip())
|
|
status = execution_utils.Exec(list_cmd, out_func=capture_out, no_exit=True)
|
|
if status:
|
|
raise DockerExecutionException(
|
|
status, 'Docker failed to execute: failed to list container ' + name)
|
|
return bool(out[0])
|
|
|
|
|
|
def FindContainerPort(name):
|
|
"""Returns the port of the Docker container with specified name.
|
|
|
|
Args:
|
|
name: The name of the Docker container.
|
|
|
|
Returns:
|
|
str: The port number of the Docker container.
|
|
|
|
Raises:
|
|
DockerExecutionException: if the exit code of the execution is non-zero
|
|
or if the port of the container does not exist.
|
|
"""
|
|
mapping = """{{range $p, $conf := .NetworkSettings.Ports}}\
|
|
{{(index $conf 0).HostPort}}{{end}}"""
|
|
find_port = [_DOCKER, 'inspect', '--format=' + mapping, name]
|
|
out = []
|
|
capture_out = lambda stdout: out.append(stdout.strip())
|
|
status = execution_utils.Exec(find_port, out_func=capture_out, no_exit=True)
|
|
if status:
|
|
raise DockerExecutionException(
|
|
status, 'Docker failed to execute: failed to find port for ' + name)
|
|
return out[0]
|
|
|
|
|
|
def GetDockerContainerLabels(name):
|
|
"""Returns the labels of the Docker container with specified name.
|
|
|
|
Args:
|
|
name: The name of the Docker container.
|
|
|
|
Returns:
|
|
dict: The labels for the docker container in json format.
|
|
|
|
Raises:
|
|
DockerExecutionException: if the exit code of the execution is non-zero
|
|
or if the port of the container does not exist.
|
|
"""
|
|
if not ContainerExists(name):
|
|
return {}
|
|
find_labels = [_DOCKER, 'inspect', '--format={{json .Config.Labels}}', name]
|
|
out = []
|
|
capture_out = lambda stdout: out.append(stdout.strip())
|
|
status = execution_utils.Exec(find_labels, out_func=capture_out, no_exit=True)
|
|
if status:
|
|
raise DockerExecutionException(
|
|
status, 'Docker failed to execute: failed to labels for ' + name)
|
|
return json.loads(out[0])
|
|
|
|
|
|
def _AddEnvVars(cmd_args, env_vars):
|
|
for key, value in env_vars.items():
|
|
cmd_args.extend(['--env', key + '=' + value])
|