File: //snap/google-cloud-cli/current/lib/googlecloudsdk/command_lib/functions/local/util.py
# -*- 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])