feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
# -*- 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 call commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import parser_extensions
from googlecloudsdk.core import properties
from googlecloudsdk.core import requests as core_requests
from googlecloudsdk.core.util import times
_CONTENT_TYPE = 'Content-Type'
# Required and Optional CloudEvent attributes
# https://github.com/cloudevents/spec/blob/v1.0.1/spec.md
_FIELDS = (
'id', 'source', 'specversion', 'type', 'dataschema', 'subject', 'time'
)
# v2 HTTP triggered functions interpret an empty Content-Type header as leaving
# the request body empty, therefore default the content type as json.
_DEFAULT_HEADERS = {_CONTENT_TYPE: 'application/json'}
def _StructuredToBinaryData(request_data_json):
"""Convert CloudEvents structured format to binary format.
Args:
request_data_json: dict, the parsed request body data
Returns:
cloudevent_data: str, the CloudEvent expected data with attributes in header
cloudevent_headers: dict, the CloudEvent headers
"""
cloudevent_headers = {}
cloudevent_data = None
for key, value in list(request_data_json.items()):
normalized_key = key.lower()
if normalized_key == 'data':
cloudevent_data = value
elif normalized_key in _FIELDS:
cloudevent_headers['ce-'+normalized_key] = value
elif normalized_key == 'datacontenttype':
cloudevent_headers[_CONTENT_TYPE] = value
else:
cloudevent_headers[normalized_key] = value
if _CONTENT_TYPE not in cloudevent_headers:
cloudevent_headers[_CONTENT_TYPE] = 'application/json'
return json.dumps(cloudevent_data), cloudevent_headers
def MakePostRequest(url, args, extra_headers=None):
# type:(str, parser_extensions.Namespace, dict[str, str]) -> str
"""Makes an HTTP Post Request to the specified url with data and headers from args.
Args:
url: The URL to send the post request to
args: The arguments from the command line parser
extra_headers: Extra headers to add to the HTTP post request
Returns:
str: The HTTP response content
"""
request_data = None
headers = _DEFAULT_HEADERS
if args.data:
request_data = args.data
headers = _DEFAULT_HEADERS
elif args.cloud_event:
request_data, headers = _StructuredToBinaryData(
json.loads(args.cloud_event))
if extra_headers:
headers = dict(headers.items() | extra_headers.items())
requests_session = core_requests.GetSession()
response = requests_session.post(
url=url,
data=request_data,
headers=headers)
response.raise_for_status()
return response.content
def UpdateHttpTimeout(args, function, api_version, release_track):
"""Update core/http_timeout using args and function timeout.
Args:
args: The arguments from the command line parser
function: function definition
api_version: v1 or v2
release_track: ALPHA, BETA, or GA
"""
if release_track in [base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA]:
timeout = 540 if api_version == 'v1' else 3600
if args.timeout:
timeout = int(args.timeout)
elif api_version == 'v1' and function.timeout:
timeout = int(
times.ParseDuration(
function.timeout, default_suffix='s'
).total_seconds
+ 30
)
elif (
api_version == 'v2'
and function.serviceConfig
and function.serviceConfig.timeoutSeconds
):
timeout = int(function.serviceConfig.timeoutSeconds) + 30
properties.VALUES.core.http_timeout.Set(timeout)

View File

@@ -0,0 +1,53 @@
# -*- 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.
"""Version-agnostic errors to raise for gcloud functions commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.core import exceptions
class FunctionsError(exceptions.Error):
"""Base exception for user recoverable Functions errors."""
class SourceArgumentError(calliope_exceptions.InvalidArgumentException):
"""Exception for errors related to using the --source argument."""
def __init__(self, message):
super(SourceArgumentError, self).__init__('--source', message)
class OversizedDeploymentError(FunctionsError):
"""Exception to indicate the deployment is too big."""
def __init__(self, actual_size, max_allowed_size):
super(OversizedDeploymentError, self).__init__(
'Uncompressed deployment is {}, bigger than maximum allowed size of {}.'
.format(actual_size, max_allowed_size)
)
class IgnoreFileNotFoundError(calliope_exceptions.InvalidArgumentException):
"""Exception for when file specified by --ignore-file is not found."""
def __init__(self, message):
super(IgnoreFileNotFoundError, self).__init__('--ignore-file', message)
class SourceUploadError(FunctionsError):
"""Exception for source upload failures."""

View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""Core labels_util extended with GCF specific behavior."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.command_lib.util.args import labels_util
class Diff(labels_util.Diff):
"""Computes changes to labels.
Similar to the core class, but it allows specifying labels that will be
added if not present or removed either via --remove-labels or
--clear-labels.
"""
@classmethod
def FromUpdateArgs(cls, args, enable_clear=True, required_labels=None):
"""Initializes a Diff based on the arguments in AddUpdateLabelsFlags.
Args:
args: The parsed args
enable_clear: whether --clear-labels flag is enabled
required_labels: dict of labels that will be added unless they're removed
explicitly or via clear
Returns:
The label updates Diff that needs to be applied to the exisiting labels.
"""
if enable_clear:
clear = args.clear_labels
if clear:
# don't add labels on clear
required_labels = {}
else:
clear = None
addition = required_labels if required_labels else {}
if args.update_labels:
addition.update(args.update_labels)
return cls(addition, args.remove_labels, clear)

View File

@@ -0,0 +1,45 @@
# -*- 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.
"""Helpers for flags in commands for Google Cloud Functions local development."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
def AddDeploymentNameFlag(parser):
parser.add_argument(
'NAME',
nargs=1,
help='Name of the locally deployed Google Cloud function.',
)
def AddPortFlag(parser):
parser.add_argument(
'--port',
default=8080,
help='Port for the deployment to run on. The default port is 8080 '
+ 'for new local deployments.',
)
def AddBuilderFlag(parser):
parser.add_argument(
'--builder',
help=('Name of the builder to use for pack, e.g. '
+ '`gcr.io/serverless-runtimes/google-22-full/builder/go`.'),
)

View File

@@ -0,0 +1,222 @@
# -*- 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])

View File

@@ -0,0 +1,97 @@
# -*- 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.
"""Cloud Run utility library for GCF."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v2 import util as api_util
from googlecloudsdk.api_lib.run import global_methods
from googlecloudsdk.command_lib.run import connection_context
from googlecloudsdk.command_lib.run import serverless_operations
from googlecloudsdk.core import resources
_CLOUD_RUN_SERVICE_COLLECTION_K8S = 'run.namespaces.services'
_CLOUD_RUN_SERVICE_COLLECTION_ONE_PLATFORM = 'run.projects.locations.services'
def AddOrRemoveInvokerBinding(function, member, add_binding=True, is_gen2=True):
"""Add the IAM binding for the invoker role on the function's Cloud Run service.
Args:
function: cloudfunctions_v2_messages.Function, a GCF v2 function.
member: str, The user to bind the Invoker role to.
add_binding: bool, Whether to add to or remove from the IAM policy.
is_gen2: bool, Whether the function is a 2nd gen function. If false, the
function is a 1st gen function undergoing upgrade.
Returns:
A google.iam.v1.Policy
"""
service_ref_one_platform = _GetOnePlatformServiceRef(function, is_gen2)
run_connection_context = _GetRunRegionalConnectionContext(
service_ref_one_platform.locationsId
)
with serverless_operations.Connect(run_connection_context) as operations:
return operations.AddOrRemoveIamPolicyBinding(
_GetK8sServiceRef(service_ref_one_platform.Name()),
add_binding=add_binding,
member=member,
role=serverless_operations.ALLOW_UNAUTH_POLICY_BINDING_ROLE,
)
def GetService(function):
"""Get the Cloud Run service for the given function."""
service_ref_one_platform = _GetOnePlatformServiceRef(function)
run_connection_context = _GetRunRegionalConnectionContext(
service_ref_one_platform.locationsId
)
with serverless_operations.Connect(run_connection_context) as operations:
return operations.GetService(
_GetK8sServiceRef(service_ref_one_platform.Name())
)
def _GetRunRegionalConnectionContext(location):
return connection_context.RegionalConnectionContext(
location,
global_methods.SERVERLESS_API_NAME,
global_methods.SERVERLESS_API_VERSION,
)
def _GetOnePlatformServiceRef(function, is_gen2=True):
service_name = (
function.serviceConfig.service
if is_gen2
else function.upgradeInfo.serviceConfig.service
)
return resources.REGISTRY.ParseRelativeName(
service_name, _CLOUD_RUN_SERVICE_COLLECTION_ONE_PLATFORM
)
def _GetK8sServiceRef(service_name):
return resources.REGISTRY.ParseRelativeName(
'namespaces/{}/services/{}'.format(api_util.GetProject(), service_name),
_CLOUD_RUN_SERVICE_COLLECTION_K8S,
)

View File

@@ -0,0 +1,517 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""Utility for configuring and parsing secrets args."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
import re
import textwrap
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope.arg_parsers import ArgumentTypeError
from googlecloudsdk.command_lib.util.args import map_util
from googlecloudsdk.core import log
import six
_SECRET_PATH_PATTERN = re.compile(
'^(/+[a-zA-Z0-9-_.]*[a-zA-Z0-9-_]+)+'
'((/*:(/*[a-zA-Z0-9-_.]*[a-zA-Z0-9-_]+)+)'
'|(/+[a-zA-Z0-9-_.]*[a-zA-Z0-9-_]+))$'
)
_DEFAULT_PROJECT_SECRET_REF_PATTERN = re.compile(
'^(?P<secret>[a-zA-Z0-9-_]+):(?P<version>[1-9][0-9]*|latest)$'
)
_SECRET_VERSION_RESOURCE_REF_PATTERN = re.compile(
'^projects/([^/]+)/secrets/([a-zA-Z0-9-_]+)/versions/([1-9][0-9]*|latest)$'
)
_SECRET_VERSION_REF_PATTERN = re.compile(
'^projects/(?P<project>[^/]+)/secrets/(?P<secret>[a-zA-Z0-9-_]+)'
':(?P<version>[1-9][0-9]*|latest)$'
)
_DEFAULT_PROJECT_IDENTIFIER = '*'
_SECRET_VERSION_SECRET_RESOURCE_PATTERN = re.compile(
'^(?P<secret_resource>projects/[^/]+/secrets/[a-zA-Z0-9-_]+)'
'/versions/(?P<version>[1-9][0-9]*|latest)$'
)
_SECRET_RESOURCE_PATTERN = re.compile(
'^projects/(?P<project>[^/]+)/secrets/(?P<secret>[a-zA-Z0-9-_]+)$'
)
def _CanonicalizePath(secret_path):
"""Canonicalizes secret path to the form `/mount_path:/secret_file_path`.
Gcloud secret path is more restrictive than the backend (shortn/_bwgb3xdRxL).
Paths are reduced to their canonical forms before the request is made.
Args:
secret_path: Complete path to the secret.
Returns:
Canonicalized secret path.
"""
secret_path = re.sub(r'/+', '/', secret_path)
seperator = ':' if ':' in secret_path else '/'
mount_path, _, secret_file_path = secret_path.rpartition(seperator)
# Strip trailing / from mount path and beginning / from file path if present.
mount_path = re.sub(r'/$', '', mount_path)
secret_file_path = re.sub(r'^/', '', secret_file_path)
return '{}:/{}'.format(mount_path, secret_file_path)
def _SecretsKeyType(key):
"""Validates and canonicalizes secrets key configuration.
Args:
key: Secrets key configuration.
Returns:
Canonicalized secrets key configuration.
Raises:
ArgumentTypeError: Secrets key configuration is not valid.
"""
if not key.strip():
raise ArgumentTypeError(
'Secret environment variable names/secret paths cannot be empty.'
)
canonicalized_key = key
if _SECRET_PATH_PATTERN.search(key):
canonicalized_key = _CanonicalizePath(key)
else:
if '/' in key:
log.warning(
"'{}' will be interpreted as a secret environment variable "
"name as it doesn't match the pattern for a secret path "
"'/mount_path:/secret_file_path'.".format(key)
)
if key.startswith('X_GOOGLE_') or key in [
'GOOGLE_ENTRYPOINT',
'GOOGLE_FUNCTION_TARGET',
'GOOGLE_RUNTIME',
'GOOGLE_RUNTIME_VERSION',
]:
raise ArgumentTypeError(
"Secret environment variable name '{}' is reserved for internal "
'use.'.format(key)
)
return canonicalized_key
def _CanonicalizeValue(value):
"""Canonicalizes secret value reference to the secret version resource name.
Output format: `projects/{project}/secrets/{secret}/versions/{version}`.
The project in the above reference will be * if the user used a default
project secret.
Args:
value: Secret value reference as a string.
Returns:
Canonicalized secret value reference.
"""
dp_secret_ref_match = _DEFAULT_PROJECT_SECRET_REF_PATTERN.search(value)
secret_version_res_ref_match = _SECRET_VERSION_RESOURCE_REF_PATTERN.search(
value
)
secret_version_ref_match = _SECRET_VERSION_REF_PATTERN.search(value)
if dp_secret_ref_match:
return 'projects/{project}/secrets/{secret}/versions/{version}'.format(
project=_DEFAULT_PROJECT_IDENTIFIER,
secret=dp_secret_ref_match.group('secret'),
version=dp_secret_ref_match.group('version'),
)
elif secret_version_res_ref_match:
return value
elif secret_version_ref_match:
return 'projects/{project}/secrets/{secret}/versions/{version}'.format(
project=secret_version_ref_match.group('project'),
secret=secret_version_ref_match.group('secret'),
version=secret_version_ref_match.group('version'),
)
raise ArgumentTypeError(
"Secrets value configuration must match the pattern 'SECRET:VERSION' or "
"'projects/{{PROJECT}}/secrets/{{SECRET}}:{{VERSION}}' or "
"'projects/{{PROJECT}}/secrets/{{SECRET}}/versions/{{VERSION}}' "
"where VERSION is a number or the label 'latest' [{}]".format(value)
)
def _SecretsValueType(value):
"""Validates secrets value configuration.
The restrictions for gcloud are strict when compared to GCF to accommodate
future changes without making it confusing for the user.
Args:
value: Secrets value configuration.
Returns:
Secrets value configuration as a string.
Raises:
ArgumentTypeError: Secrets value configuration is not valid.
"""
if '=' in value:
raise ArgumentTypeError(
"Secrets value configuration cannot contain '=' [{}]".format(value)
)
return _CanonicalizeValue(value)
def _SecretsDiffer(project1, secret1, project2, secret2):
"""Returns true if the two secrets differ.
The secrets can be considered as different if either the secret name is
different or the project is different with the secret name being the same. If
one project is represented using the project number and the other is
represented using its project id, then it may not be possible to determine if
the two projects are the same, so the validation is relaxed.
Args:
project1: Project ID or number of the first secret.
secret1: Secret name of the first secret.
project2: Project ID or number of the second secret.
secret2: Secret name of the second secret.
Returns:
True if the two secrets differ, False otherwise.
"""
return secret1 != secret2 or (
project1 != project2
and project1.isdigit() == project2.isdigit()
and project1 != _DEFAULT_PROJECT_IDENTIFIER
and project2 != _DEFAULT_PROJECT_IDENTIFIER
)
def _ValidateSecrets(secrets_dict):
"""Additional secrets validations that require the entire dict.
Args:
secrets_dict: Secrets configuration dict to validate.
"""
mount_path_to_secret = collections.defaultdict(list)
for key, value in six.iteritems(secrets_dict):
if _SECRET_PATH_PATTERN.search(key):
mount_path = key.split(':')[0]
secret_res1 = _SECRET_VERSION_SECRET_RESOURCE_PATTERN.search(value).group(
'secret_resource'
)
if mount_path in mount_path_to_secret:
secret_res_match1 = _SECRET_RESOURCE_PATTERN.search(secret_res1)
project1 = secret_res_match1.group('project')
secret1 = secret_res_match1.group('secret')
for secret_res2 in mount_path_to_secret[mount_path]:
secret_res_match2 = _SECRET_RESOURCE_PATTERN.search(secret_res2)
project2 = secret_res_match2.group('project')
secret2 = secret_res_match2.group('secret')
if _SecretsDiffer(project1, secret1, project2, secret2):
raise ArgumentTypeError(
'More than one secret is configured for the mount path '
"'{mount_path}' [violating secrets: {secret1},{secret2}]."
.format(
mount_path=mount_path,
secret1=secret1
if project1 == _DEFAULT_PROJECT_IDENTIFIER
else secret_res1,
secret2=secret2
if project2 == _DEFAULT_PROJECT_IDENTIFIER
else secret_res2,
)
)
else:
mount_path_to_secret[mount_path].append(secret_res1)
class ArgSecretsDict(arg_parsers.ArgDict):
"""ArgDict customized for holding secrets configuration."""
def __init__(
self,
key_type=None,
value_type=None,
spec=None,
min_length=0,
max_length=None,
allow_key_only=False,
required_keys=None,
operators=None,
):
"""Initializes the base ArgDict by forwarding the parameters."""
super(ArgSecretsDict, self).__init__(
key_type=key_type,
value_type=value_type,
spec=spec,
min_length=min_length,
max_length=max_length,
allow_key_only=allow_key_only,
required_keys=required_keys,
operators=operators,
)
def __call__(self, arg_value): # pylint:disable=missing-docstring
secrets_dict = collections.OrderedDict(
sorted(six.iteritems(super(ArgSecretsDict, self).__call__(arg_value)))
)
_ValidateSecrets(secrets_dict)
return secrets_dict
def ConfigureFlags(parser):
"""Add flags for configuring secret environment variables and secret volumes.
Args:
parser: Argument parser.
"""
kv_metavar = (
'SECRET_ENV_VAR=SECRET_VALUE_REF,'
'/secret_path=SECRET_VALUE_REF,'
'/mount_path:/secret_file_path=SECRET_VALUE_REF'
)
k_metavar = 'SECRET_ENV_VAR,/secret_path,/mount_path:/secret_file_path'
flag_group = parser.add_mutually_exclusive_group()
flag_group.add_argument(
'--set-secrets',
metavar=kv_metavar,
action=arg_parsers.UpdateAction,
type=ArgSecretsDict(
key_type=_SecretsKeyType, value_type=_SecretsValueType
),
help="""
List of secret environment variables and secret volumes to configure.
Existing secrets configuration will be overwritten.
You can reference a secret value referred to as `SECRET_VALUE_REF` in the
help text in the following ways.
* Use `${SECRET}:${VERSION}` if you are referencing a secret in the same
project, where `${SECRET}` is the name of the secret in secret manager
(not the full resource name) and `${VERSION}` is the version of the
secret which is either a `positive integer` or the label `latest`.
For example, use `SECRET_FOO:1` to reference version `1` of the secret
`SECRET_FOO` which exists in the same project as the function.
* Use `projects/${PROJECT}/secrets/${SECRET}/versions/${VERSION}` or
`projects/${PROJECT}/secrets/${SECRET}:${VERSION}` to reference a secret
version using the full resource name, where `${PROJECT}` is either the
project number (`preferred`) or the project ID of the project which
contains the secret, `${SECRET}` is the name of the secret in secret
manager (not the full resource name) and `${VERSION}` is the version of
the secret which is either a `positive integer` or the label `latest`.
For example, use `projects/1234567890/secrets/SECRET_FOO/versions/1` or
`projects/project_id/secrets/SECRET_FOO/versions/1` to reference version
`1` of the secret `SECRET_FOO` that exists in the project `1234567890`
or `project_id` respectively.
This format is useful when the secret exists in a different project.
To configure the secret as an environment variable, use
`SECRET_ENV_VAR=SECRET_VALUE_REF`. To use the value of the secret, read
the environment variable `SECRET_ENV_VAR` as you would normally do in the
function's programming language.
We recommend using a `numeric` version for secret environment variables
as any updates to the secret value are not reflected until new clones
start.
To mount the secret within a volume use `/secret_path=SECRET_VALUE_REF` or
`/mount_path:/secret_file_path=SECRET_VALUE_REF`. To use the value of the
secret, read the file at `/secret_path` as you would normally do in the
function's programming language.
For example, `/etc/secrets/secret_foo=SECRET_FOO:latest` or
`/etc/secrets:/secret_foo=SECRET_FOO:latest` will make the value of the
`latest` version of the secret `SECRET_FOO` available in a file
`secret_foo` under the directory `/etc/secrets`. `/etc/secrets` will be
considered as the `mount path` and will `not` be available for any other
volume.
We recommend referencing the `latest` version when using secret volumes so
that the secret's value changes are reflected immediately.""",
)
update_remove_flag_group = flag_group.add_argument_group(
help=textwrap.dedent(
"""\
Only `--update-secrets` and `--remove-secrets` can be used together. If
both are specified, then `--remove-secrets` will be applied first."""
)
)
update_remove_flag_group.add_argument(
'--update-secrets',
metavar=kv_metavar,
action=arg_parsers.UpdateAction,
type=ArgSecretsDict(
key_type=_SecretsKeyType, value_type=_SecretsValueType
),
help="""
List of secret environment variables and secret volumes to update.
Existing secrets configuration not specified in this list will be
preserved.""",
)
update_remove_flag_group.add_argument(
'--remove-secrets',
metavar=k_metavar,
action=arg_parsers.UpdateAction,
type=arg_parsers.ArgList(element_type=_SecretsKeyType),
help="""
List of secret environment variable names and secret paths to remove.
Existing secrets configuration of secret environment variable names and
secret paths not specified in this list will be preserved.
To remove a secret environment variable, use the name of the environment
variable `SECRET_ENV_VAR`.
To remove a file within a secret volume or the volume itself, use the
secret path as the key (either `/secret_path` or
`/mount_path:/secret_file_path`).""",
)
flag_group.add_argument(
'--clear-secrets',
action='store_true',
help='Remove all secret environment variables and volumes.',
)
def IsArgsSpecified(args):
"""Returns true if at least one of the flags for secrets is specified.
Args:
args: Argparse namespace.
Returns:
True if at least one of the flags for secrets is specified.
"""
secrets_flags = {
'--set-secrets',
'--update-secrets',
'--remove-secrets',
'--clear-secrets',
}
specified_flags = set(args.GetSpecifiedArgNames())
return bool(specified_flags.intersection(secrets_flags))
def SplitSecretsDict(secrets_dict):
"""Splits the secrets dict into sorted ordered dicts for each secret type.
Args:
secrets_dict: Secrets configuration dict.
Returns:
A tuple (secret_env_vars, secret_volumes) of sorted ordered dicts for each
secret type.
"""
secret_volumes = {
k: v
for k, v in six.iteritems(secrets_dict)
if _SECRET_PATH_PATTERN.search(k)
}
secret_env_vars = {
k: v for k, v in six.iteritems(secrets_dict) if k not in secret_volumes
}
return (
collections.OrderedDict(sorted(six.iteritems(secret_env_vars))),
collections.OrderedDict(sorted(six.iteritems(secret_volumes))),
)
def CanonicalizeKey(key):
"""Canonicalizes secrets configuration key.
Args:
key: Secrets configuration key.
Returns:
Canonicalized secrets configuration key.
"""
if _SECRET_PATH_PATTERN.search(key):
return _CanonicalizePath(key)
return key
def _SubstituteDefaultProject(
secret_version_ref, default_project_id, default_project_number
):
"""Replaces the default project number in place of * or project ID.
Args:
secret_version_ref: Secret value reference.
default_project_id: The project ID of the project to which the function is
deployed.
default_project_number: The project number of the project to which the
function is deployed.
Returns:
Secret value reference with * or project ID replaced by the default project.
"""
return re.sub(
r'projects/([*]|{project_id})/'.format(project_id=default_project_id),
'projects/{project_number}/'.format(
project_number=default_project_number
),
secret_version_ref,
)
def ApplyFlags(old_secrets, args, default_project_id, default_project_number):
"""Applies the current flags to existing secrets configuration.
Args:
old_secrets: Existing combined secrets configuration dict.
args: Argparse namespace.
default_project_id: The project ID of the project to which the function is
deployed.
default_project_number: The project number of the project to which the
function is deployed.
Returns:
new_secrets: A new combined secrets configuration dict generated by
applying the flags to the existing secrets configuration.
Raises:
ArgumentTypeError: Generated secrets configuration is invalid.
"""
secret_flags = map_util.GetMapFlagsFromArgs('secrets', args)
new_secrets = map_util.ApplyMapFlags(old_secrets, **secret_flags)
new_secrets = {
secrets_key: _SubstituteDefaultProject(
secrets_value, default_project_id, default_project_number
)
for secrets_key, secrets_value in six.iteritems(new_secrets)
}
new_secrets = collections.OrderedDict(sorted(six.iteritems(new_secrets)))
# Handles the case when the newly configured secrets could conflict with
# existing secrets.
_ValidateSecrets(new_secrets)
return new_secrets

View File

@@ -0,0 +1,219 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Service account utils."""
import re
from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib.cloudbuild import cloudbuild_util
from googlecloudsdk.api_lib.cloudresourcemanager import projects_api
from googlecloudsdk.command_lib.functions import run_util
from googlecloudsdk.command_lib.projects import util as project_util
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_io
_BUILDER_ROLE = 'roles/cloudbuild.builds.builder'
_EDITOR_ROLE = 'roles/editor'
_RUN_INVOKER_ROLE = 'roles/run.invoker'
_PREDEFINE_ROLES_WITH_ROUTE_INVOKER_PERMISSION = [
'roles/run.admin',
'roles/run.developer',
_RUN_INVOKER_ROLE,
'roles/run.servicesInvoker',
'roles/run.sourceDeveloper',
]
_ROUTE_INVOKER_PERMISSION = 'run.routes.invoke'
_GCE_SA = '{project_number}-compute@developer.gserviceaccount.com'
def GetDefaultBuildServiceAccount(project_id, region='global'):
"""Gets the default build service account for a project."""
client = cloudbuild_util.GetClientInstance()
name = f'projects/{project_id}/locations/{region}/defaultServiceAccount'
return client.projects_locations.GetDefaultServiceAccount(
client.MESSAGES_MODULE.CloudbuildProjectsLocationsGetDefaultServiceAccountRequest(
name=name
)
).serviceAccountEmail
def _ExtractServiceAccountEmail(service_account):
"""Extracts the service account email from the service account resource."""
match = re.search(r'/serviceAccounts/([^/]+)$', service_account)
if match:
return match.group(1)
else:
return None
def ValidateDefaultBuildServiceAccountAndPromptWarning(
project_id, region, build_service_account=None
):
"""Util to validate the default build service account permission.
Prompt a warning to users if cloudbuild.builds.builder is missing.
Args:
project_id: id of project
region: region to deploy the function
build_service_account: user provided build service account. It will be None,
if user doesn't provide it.
"""
if build_service_account is None:
build_service_account = _ExtractServiceAccountEmail(
GetDefaultBuildServiceAccount(project_id, region)
)
project_number = project_util.GetProjectNumber(project_id)
if build_service_account == _GCE_SA.format(project_number=project_number):
try:
iam_policy = projects_api.GetIamPolicy(
project_util.ParseProject(project_id)
)
except apitools_exceptions.HttpForbiddenError:
log.warning(
(
'Your account does not have permission to check or bind IAM'
' policies to project [%s]. If the deployment fails, ensure [%s]'
' has the role [%s] before retrying.'
),
project_id,
build_service_account,
_BUILDER_ROLE,
)
return
account_string = f'serviceAccount:{build_service_account}'
contained_roles = [
binding.role
for binding in iam_policy.bindings
if account_string in binding.members
]
if (
_BUILDER_ROLE not in contained_roles
and _EDITOR_ROLE not in contained_roles
and console_io.CanPrompt()
):
# Previously default compute engine service account was granted editor
# role when it was provisioned, which naturally granted all of the
# permission required to finish a build. Nowadays, editor role is not
# granted by default anymore. We want to suggest users having
# roles/cloudbuild.builds.builder instead to make sure build can be
# completed successfully.
console_io.PromptContinue(
default=False,
cancel_on_no=True,
prompt_string=(
f'\nThe default build service account [{build_service_account}]'
f' is missing the [{_BUILDER_ROLE}] role. This may cause issues'
' when deploying a function. You could fix it by running the'
' command: \ngcloud projects add-iam-policy-binding'
f' {project_id} \\\n'
f' --member=serviceAccount:{project_number}-compute@developer.gserviceaccount.com'
' \\\n --role=roles/cloudbuild.builds.builder \nFor more'
' information, please refer to:'
' https://cloud.google.com/functions/docs/troubleshooting#build-service-account.\n'
' Would you like to continue?'
),
)
def ValidateAndBindTriggerServiceAccount(
function,
project_id,
trigger_service_account,
is_gen2=False,
):
"""Validates trigger service account has route.invoker permission on project.
If missing, prompt user to add the run invoker role on the function's Cloud
Run service.
Args:
function: the function to add the binding to
project_id: the project id to validate
trigger_service_account: the trigger service account to validate
is_gen2: whether the function is a gen2 function
"""
project_number = project_util.GetProjectNumber(project_id)
trigger_service_account = (
trigger_service_account
if trigger_service_account
else _GCE_SA.format(project_number=project_number)
)
try:
iam_policy = projects_api.GetIamPolicy(
project_util.ParseProject(project_id)
)
if _ShouldBindInvokerRole(iam_policy, trigger_service_account):
run_util.AddOrRemoveInvokerBinding(
function,
f'serviceAccount:{trigger_service_account}',
add_binding=True,
is_gen2=is_gen2,
)
log.status.Print('Role successfully bound.\n')
except apitools_exceptions.HttpForbiddenError:
log.warning(
'Your account does not have permission to check or bind IAM'
' policies to project [%s]. If your function encounters'
' authentication errors, ensure [%s] has the role [%s].',
project_id,
trigger_service_account,
_RUN_INVOKER_ROLE,
)
def _ShouldBindInvokerRole(iam_policy, service_account):
"""Prompts user to bind the invoker role if missing."""
custom_role_detected = False
account_string = f'serviceAccount:{service_account}'
for binding in iam_policy.bindings:
if account_string not in binding.members:
continue
if binding.role in _PREDEFINE_ROLES_WITH_ROUTE_INVOKER_PERMISSION:
return False
elif not binding.role.startswith('roles/'):
# A custom role starts with "projects/" or "organizations/" while a
# predefined role starts with "roles/".
custom_role_detected = True
prompt_string = (
f'Your trigger service account [{service_account}] is missing'
f' the [{_RUN_INVOKER_ROLE}] role. This will cause authentication'
' errors when running the function.\n'
)
if custom_role_detected:
prompt_string = (
f'Your trigger service account [{service_account}] likely'
f' lacks the [{_ROUTE_INVOKER_PERMISSION}] permission, which will'
' cause authentication errors. Since this service account uses a'
' custom role, please verify that the custom role includes this'
" permission. If not, you'll need to either add this permission to"
f' the custom role, or grant the [{_RUN_INVOKER_ROLE}] role to the'
' service account directly.\n'
)
should_bind = console_io.CanPrompt() and console_io.PromptContinue(
default=False,
cancel_on_no=True,
prompt_string=prompt_string
+ ' Do you want to add the invoker binding to the IAM policy of'
' your Cloud Run function?',
)
if not should_bind:
log.warning(prompt_string)
return should_bind

View File

@@ -0,0 +1,314 @@
# -*- 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.
"""Version-agnostic utilities for function source code."""
from __future__ import absolute_import
from __future__ import annotations
from __future__ import division
from __future__ import unicode_literals
import abc
import http
import os
import random
import re
import string
import time
from typing import Dict
from apitools.base.py import exceptions as http_exceptions
from apitools.base.py import http_wrapper
from apitools.base.py import transfer
from apitools.base.py import util as http_util
from googlecloudsdk.api_lib.storage import storage_api
from googlecloudsdk.api_lib.storage import storage_util
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.command_lib.functions import exceptions
from googlecloudsdk.command_lib.util import gcloudignore
from googlecloudsdk.core import log
from googlecloudsdk.core import resources
from googlecloudsdk.core import transports
from googlecloudsdk.core.util import archive
from googlecloudsdk.core.util import files as file_utils
# List of required files for each runtime per
# https://cloud.google.com/functions/docs/writing#directory-structure
# To keep things simple we don't check for file extensions, just required files.
# Every language except dotnet and java have a required file with an invariant
# name.
class RequiredFilesStrategy(object, metaclass=abc.ABCMeta):
"""Abstract base class for required files validation strategy."""
@abc.abstractmethod
def Validate(self, files_in_source_dir: list[str], runtime: str) -> None:
raise NotImplementedError()
class AllFilesPresentStrategy(RequiredFilesStrategy):
"""Strategy that requires all specified files to be present."""
def __init__(self, required_files: list[str]):
super(AllFilesPresentStrategy, self).__init__()
self._required_files = required_files
def Validate(self, files_in_source_dir: list[str], runtime: str) -> None:
for f in self._required_files:
if f not in files_in_source_dir:
raise exceptions.SourceArgumentError(
f'Provided source directory does not have file [{f}] which is '
f'required for [{runtime}]. Did you specify the right source?'
)
class PythonRequiredFilesStrategy(RequiredFilesStrategy):
"""Strategy for Python runtime required files validation."""
def Validate(self, files_in_source_dir: list[str], runtime: str) -> None:
if 'main.py' not in files_in_source_dir:
raise exceptions.SourceArgumentError(
'Provided source directory does not have file [main.py] which is '
f'required for [{runtime}]. Did you specify the right source?'
)
if (
'requirements.txt' not in files_in_source_dir
and 'pyproject.toml' not in files_in_source_dir
):
raise exceptions.SourceArgumentError(
'Provided source directory does not have file [requirements.txt or '
f'pyproject.toml] which is required for [{runtime}]. Did you '
'specify the right source?'
)
_REQUIRED_SOURCE_STRATEGIES = {
'dotnet': AllFilesPresentStrategy([]),
'go': AllFilesPresentStrategy(['go.mod']),
'java': AllFilesPresentStrategy([]),
'nodejs': AllFilesPresentStrategy(['package.json']),
'php': AllFilesPresentStrategy(['index.php', 'composer.json']),
'python': PythonRequiredFilesStrategy(),
'ruby': AllFilesPresentStrategy(['app.rb', 'Gemfile']),
}
def _GcloudIgnoreCreationPredicate(directory: str) -> bool:
return gcloudignore.AnyFileOrDirExists(
directory, gcloudignore.GIT_FILES + ['node_modules']
)
def _GetChooser(
path: str, ignore_file: str | None = None
) -> gcloudignore.FileChooser:
default_ignore_file = gcloudignore.DEFAULT_IGNORE_FILE + '\nnode_modules\n'
return gcloudignore.GetFileChooserForDir(
path,
default_ignore_file=default_ignore_file,
gcloud_ignore_creation_predicate=_GcloudIgnoreCreationPredicate,
ignore_file=ignore_file,
)
def _ValidateDirectoryExistsOrRaise(directory: str) -> None:
"""Validates that the given directory exists.
Args:
directory: a local path to the directory provided by user.
Returns:
The argument provided, if found valid.
Raises:
SourceArgumentError: If the user provided an invalid directory.
"""
if not os.path.exists(directory):
raise exceptions.SourceArgumentError('Provided directory does not exist')
if not os.path.isdir(directory):
raise exceptions.SourceArgumentError(
'Provided path does not point to a directory'
)
def _ValidateUnpackedSourceSize(
path: str, ignore_file: str | None = None
) -> None:
"""Validate size of unpacked source files."""
chooser = _GetChooser(path, ignore_file)
predicate = chooser.IsIncluded
try:
size_b = file_utils.GetTreeSizeBytes(path, predicate=predicate)
except OSError as e:
raise exceptions.FunctionsError(
'Error building source archive from path [{path}]. '
'Could not validate source files: [{error}]. '
'Please ensure that path [{path}] contains function code or '
'specify another directory with --source'.format(path=path, error=e)
)
size_limit_mb = 512
size_limit_b = size_limit_mb * 2**20
if size_b > size_limit_b:
raise exceptions.OversizedDeploymentError(
str(size_b) + 'B', str(size_limit_b) + 'B'
)
def ValidateDirectoryHasRequiredRuntimeFiles(source: str, runtime: str) -> None:
"""Validates the given source directory has the required runtime files."""
_ValidateDirectoryExistsOrRaise(source)
versionless_runtime = re.sub(r'[0-9]', '', runtime)
files_in_source_dir = os.listdir(source)
strategy = _REQUIRED_SOURCE_STRATEGIES.get(versionless_runtime)
if strategy:
strategy.Validate(files_in_source_dir, runtime)
def CreateSourcesZipFile(
zip_dir: str,
source_path: str,
ignore_file: str | None = None,
enforce_size_limit=False,
) -> str:
"""Prepare zip file with source of the function to upload.
Args:
zip_dir: str, directory in which zip file will be located. Name of the file
will be `fun.zip`.
source_path: str, directory containing the sources to be zipped.
ignore_file: custom ignore_file name. Override .gcloudignore file to
customize files to be skipped.
enforce_size_limit: if set, enforces that the unpacked source size is less
than or equal to 512 MB.
Returns:
Path to the zip file.
Raises:
FunctionsError
"""
_ValidateDirectoryExistsOrRaise(source_path)
if ignore_file and not os.path.exists(os.path.join(source_path, ignore_file)):
raise exceptions.IgnoreFileNotFoundError(
'File {0} referenced by --ignore-file does not exist.'.format(
ignore_file
)
)
if enforce_size_limit:
_ValidateUnpackedSourceSize(source_path, ignore_file)
zip_file_name = os.path.join(zip_dir, 'fun.zip')
try:
chooser = _GetChooser(source_path, ignore_file)
predicate = chooser.IsIncluded
archive.MakeZipFromDir(zip_file_name, source_path, predicate=predicate)
except ValueError as e:
raise exceptions.FunctionsError(
'Error creating a ZIP archive with the source code '
'for directory {0}: {1}'.format(source_path, str(e))
)
return zip_file_name
def _GenerateRemoteZipFileName(function_ref: resources.Resource) -> str:
region = function_ref.locationsId
name = function_ref.functionsId
suffix = ''.join(random.choice(string.ascii_lowercase) for _ in range(12))
return '{0}-{1}-{2}.zip'.format(region, name, suffix)
def UploadToStageBucket(
source_zip: str, function_ref: resources.Resource, stage_bucket: str
) -> storage_util.ObjectReference:
"""Uploads the given source ZIP file to the provided staging bucket.
Args:
source_zip: the source ZIP file to upload.
function_ref: the function resource reference.
stage_bucket: the name of GCS bucket to stage the files to.
Returns:
dest_object: a reference to the uploaded Cloud Storage object.
"""
zip_file = _GenerateRemoteZipFileName(function_ref)
bucket_ref = storage_util.BucketReference.FromArgument(stage_bucket)
dest_object = storage_util.ObjectReference.FromBucketRef(bucket_ref, zip_file)
try:
storage_api.StorageClient().CopyFileToGCS(source_zip, dest_object)
except calliope_exceptions.BadFileException:
raise exceptions.SourceUploadError(
'Failed to upload the function source code to the bucket {0}'.format(
stage_bucket
)
)
return dest_object
def _UploadFileToGeneratedUrlCheckResponse(
response: http_wrapper.Response,
) -> http_wrapper.CheckResponse:
if response.status_code == http.HTTPStatus.FORBIDDEN:
raise http_exceptions.HttpForbiddenError.FromResponse(response)
return http_wrapper.CheckResponse(response)
def UploadToGeneratedUrl(
source_zip: str, url: str, extra_headers: Dict[str, str] | None = None
) -> None:
"""Upload the given source ZIP file to provided generated URL.
Args:
source_zip: the source ZIP file to upload.
url: the signed Cloud Storage URL to upload to.
extra_headers: extra headers to attach to the request.
"""
extra_headers = extra_headers or {}
upload = transfer.Upload.FromFile(source_zip, mime_type='application/zip')
def _UploadRetryFunc(retry_args: http_wrapper.ExceptionRetryArgs) -> None:
if isinstance(retry_args.exc, http_exceptions.HttpForbiddenError):
log.debug('Caught delayed permission propagation error, retrying')
http_wrapper.RebuildHttpConnections(retry_args.http)
time.sleep(
http_util.CalculateWaitForRetry(
retry_args.num_retries, max_wait=retry_args.max_retry_wait
)
)
else:
upload.retry_func(retry_args)
try:
upload_request = http_wrapper.Request(
url,
http_method='PUT',
headers={'content-type': 'application/zip', **extra_headers},
)
upload_request.body = upload.stream.read()
response = http_wrapper.MakeRequest(
transports.GetApitoolsTransport(),
upload_request,
retry_func=_UploadRetryFunc,
check_response_func=_UploadFileToGeneratedUrlCheckResponse,
retries=upload.num_retries,
)
finally:
upload.stream.close()
if response.status_code // 100 != 2:
raise exceptions.SourceUploadError(
'Failed to upload the function source code to signed url: {url}. '
'Status: [{code}:{detail}]'.format(
url=url, code=response.status_code, detail=response.content
)
)

View File

@@ -0,0 +1,98 @@
# -*- 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.
"""Cross-version utility classes and functions for gcloud functions commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import abc
import re
from typing import Any
from googlecloudsdk.api_lib.functions.v1 import util as api_util_v1
from googlecloudsdk.api_lib.functions.v2 import client as client_v2
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import parser_extensions
from googlecloudsdk.command_lib.functions import flags
import six
class FunctionResourceCommand(six.with_metaclass(abc.ABCMeta, base.Command)):
"""Mix-in for single function resource commands that work with both v1 or v2.
Which version of the command to run is determined by the following precedence:
1. Explicit setting via the --gen2/--no-gen2 flags or functions/gen2 property.
2. The generation of the function if it exists.
3. The v2 API by default if the function doesn't exist.
Subclasses should add the function resource arg and --gen2 flag.
"""
def __init__(self, *args, **kwargs):
super(FunctionResourceCommand, self).__init__(*args, **kwargs)
self._v2_function = None
@abc.abstractmethod
def _RunV1(self, args: parser_extensions.Namespace) -> Any:
"""Runs the command against the v1 API."""
@abc.abstractmethod
def _RunV2(self, args: parser_extensions.Namespace) -> Any:
"""Runs the command against the v2 API."""
@api_util_v1.CatchHTTPErrorRaiseHTTPException
def Run(self, args: parser_extensions.Namespace) -> Any:
"""Runs the command.
Args:
args: The arguments this command was invoked with.
Returns:
The result of the command.
Raises:
HttpException: If an HttpError occurs.
"""
if flags.ShouldUseGen2():
return self._RunV2(args)
if flags.ShouldUseGen1():
return self._RunV1(args)
client = client_v2.FunctionsClient(self.ReleaseTrack())
self._v2_function = client.GetFunction(
args.CONCEPTS.name.Parse().RelativeName()
)
if self._v2_function:
if str(self._v2_function.environment) == 'GEN_2':
return self._RunV2(args)
else:
return self._RunV1(args)
return self._RunV2(args)
def FormatTimestamp(timestamp):
"""Formats a timestamp which will be presented to a user.
Args:
timestamp: Raw timestamp string in RFC3339 UTC "Zulu" format.
Returns:
Formatted timestamp string.
"""
return re.sub(r'(\.\d{3})\d*Z$', r'\1', timestamp.replace('T', ' '))

View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions add-iam-policy-binding` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v1 import util
def Run(args):
"""Adds a binding to the IAM policy for a Google Cloud Function.
Args:
args: an argparse namespace. All the arguments that were provided to this
command invocation.
Returns:
The updated IAM policy.
"""
function_ref = args.CONCEPTS.name.Parse()
return util.AddFunctionIamPolicyBinding(function_ref.RelativeName(),
args.member, args.role)

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions call` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v1 import util
from googlecloudsdk.command_lib.functions import call_util
def Run(args, release_track=None):
"""Call a v1 Google Cloud Function."""
client = util.GetApiClientInstance()
function_ref = args.CONCEPTS.name.Parse()
# Do not retry calling function - most likely user want to know that the
# call failed and debug.
client.projects_locations_functions.client.num_retries = 0
messages = client.MESSAGES_MODULE
function = client.projects_locations_functions.Get(
messages.CloudfunctionsProjectsLocationsFunctionsGetRequest(
name=function_ref.RelativeName()
)
)
call_util.UpdateHttpTimeout(args, function, 'v1', release_track)
return client.projects_locations_functions.Call(
messages.CloudfunctionsProjectsLocationsFunctionsCallRequest(
name=function_ref.RelativeName(),
callFunctionRequest=messages.CallFunctionRequest(data=args.data)))

View File

@@ -0,0 +1,71 @@
# -*- 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.
"""This file provides util to decorate output of functions command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import itertools
from apitools.base.py import encoding
def decorate_v1_function_with_v2_api_info(v1_func, v2_func):
# type: (v1_messages.CloudFunction, v2_messages.Function) -> dict[str, Any]
"""Decorate gen1 function in v1 API format with additional info from its v2 API format.
Currently only the `environment` and `upgradeInfo` fields are copied over.
Args:
v1_func: A gen1 function retrieved from v1 API.
v2_func: The same gen1 function but as returned by the v2 API.
Returns:
The given Gen1 function encoded as a dict in the v1 format but with the
`upgradeInfo` and `environment` properties from the v2 format added.
"""
v1_dict = encoding.MessageToDict(v1_func)
if v2_func and v2_func.environment:
v1_dict["environment"] = str(v2_func.environment)
if v2_func and v2_func.upgradeInfo:
v1_dict["upgradeInfo"] = encoding.MessageToDict(v2_func.upgradeInfo)
return v1_dict
def decorate_v1_generator_with_v2_api_info(v1_generator, v2_generator):
"""Decorate gen1 functions in v1 API format with additional info from its v2 API format.
Currently only the `environment` and `upgradeInfo` fields are copied over.
Args:
v1_generator: Generator, generating gen1 function retrieved from v1 API.
v2_generator: Generator, generating gen1 function retrieved from v2 API.
Yields:
Gen1 function encoded as a dict with upgrade info decorated.
"""
gen1_generator = sorted(
itertools.chain(v1_generator, v2_generator), key=lambda f: f.name
)
for _, func_gen in itertools.groupby(gen1_generator, key=lambda f: f.name):
func_list = list(func_gen)
if len(func_list) < 2:
# If this is v2 function, upgrade info should have been included.
# No decoration needed. Yield directly.
# If this is v1 function, no corresponding v2 function is found,
# so there is no upgrade info we have use to decorate. Yield directly.
yield func_list[0]
else:
v1_func, v2_func = func_list
yield decorate_v1_function_with_v2_api_info(v1_func, v2_func)

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions delete` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v1 import exceptions
from googlecloudsdk.api_lib.functions.v1 import operations
from googlecloudsdk.api_lib.functions.v1 import util
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_io
def Run(args):
"""Delete a Google Cloud Function."""
client = util.GetApiClientInstance()
messages = client.MESSAGES_MODULE
function_ref = args.CONCEPTS.name.Parse()
function_url = function_ref.RelativeName()
prompt_message = '1st gen function [{0}] will be deleted.'.format(
function_url
)
if not console_io.PromptContinue(message=prompt_message):
raise exceptions.FunctionsError('Deletion aborted by user.')
op = client.projects_locations_functions.Delete(
messages.CloudfunctionsProjectsLocationsFunctionsDeleteRequest(
name=function_url
)
)
operations.Wait(op, messages, client)
log.DeletedResource(function_url)

View File

@@ -0,0 +1,832 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions deploy` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
from apitools.base.py import encoding
from googlecloudsdk.api_lib.compute import utils
from googlecloudsdk.api_lib.functions import api_enablement
from googlecloudsdk.api_lib.functions import cmek_util
from googlecloudsdk.api_lib.functions import secrets as secrets_util
from googlecloudsdk.api_lib.functions.v1 import env_vars as env_vars_api_util
from googlecloudsdk.api_lib.functions.v1 import exceptions as function_exceptions
from googlecloudsdk.api_lib.functions.v1 import util as api_util
from googlecloudsdk.api_lib.functions.v2 import client as v2_client
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.calliope.arg_parsers import ArgumentTypeError
from googlecloudsdk.command_lib.functions import flags
from googlecloudsdk.command_lib.functions import secrets_config
from googlecloudsdk.command_lib.functions import service_account_util
from googlecloudsdk.command_lib.functions.v1.deploy import enum_util
from googlecloudsdk.command_lib.functions.v1.deploy import labels_util
from googlecloudsdk.command_lib.functions.v1.deploy import source_util
from googlecloudsdk.command_lib.functions.v1.deploy import trigger_util
from googlecloudsdk.command_lib.projects import util as project_util
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.command_lib.util.args import map_util
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import console_io
from six.moves import urllib
_BUILD_NAME_REGEX = re.compile(
r'projects\/(?P<projectnumber>[^\/]+)\/locations'
r'\/(?P<region>[^\/]+)\/builds\/(?P<buildid>[^\/]+)'
)
_GEN2_ONLY_FLAGS = {
# flag: feature_name
'binary_authorization': 'Binary authorization',
'clear_binary_authorization': 'Binary authorization',
'network': 'Direct VPC',
'subnet': 'Direct VPC',
'clear_network': 'Direct VPC',
'network_tags': 'Direct VPC',
'clear_network_tags': 'Direct VPC',
'direct_vpc_egress': 'Direct VPC',
}
def _ApplyBuildEnvVarsArgsToFunction(function, args):
"""Determines if build environment variables have to be updated.
It compares the cli args with the existing build environment variables to
compute the resulting build environment variables.
Args:
function: CloudFunction message to be checked and filled with build env vars
based on the flags
args: all cli args
Returns:
updated_fields: update mask containing the list of fields that are
considered for updating based on the cli args and existing variables
"""
updated_fields = []
old_build_env_vars = env_vars_api_util.GetEnvVarsAsDict(
function.buildEnvironmentVariables
)
build_env_var_flags = map_util.GetMapFlagsFromArgs('build-env-vars', args)
new_build_env_vars = map_util.ApplyMapFlags(
old_build_env_vars, **build_env_var_flags
)
if old_build_env_vars != new_build_env_vars:
build_env_vars_type_class = (
api_util.GetApiMessagesModule().CloudFunction.BuildEnvironmentVariablesValue
)
function.buildEnvironmentVariables = (
env_vars_api_util.DictToEnvVarsProperty(
build_env_vars_type_class, new_build_env_vars
)
)
updated_fields.append('buildEnvironmentVariables')
return updated_fields
def _ApplyEnvVarsArgsToFunction(function, args):
"""Determines if environment variables have to be updated.
It compares the cli args with the existing environment variables to compute
the resulting build environment variables.
Args:
function: CloudFunction message to be checked and filled with env vars based
on the flags
args: all cli args
Returns:
updated_fields: update mask containing the list of fields that are
considered for updating based on the cli args and existing variables
"""
updated_fields = []
old_env_vars = env_vars_api_util.GetEnvVarsAsDict(
function.environmentVariables
)
env_var_flags = map_util.GetMapFlagsFromArgs('env-vars', args)
new_env_vars = map_util.ApplyMapFlags(old_env_vars, **env_var_flags)
if old_env_vars != new_env_vars:
env_vars_type_class = (
api_util.GetApiMessagesModule().CloudFunction.EnvironmentVariablesValue
)
function.environmentVariables = env_vars_api_util.DictToEnvVarsProperty(
env_vars_type_class, new_env_vars
)
updated_fields.append('environmentVariables')
return updated_fields
def _LogSecretsPermissionMessage(project, service_account_email):
"""Logs a warning message asking the user to grant access to secrets.
This will be removed once access checker is added.
Args:
project: Project id.
service_account_email: Runtime service account email.
"""
if not service_account_email:
service_account_email = '{project}@appspot.gserviceaccount.com'.format(
project=project
)
message = (
'This deployment uses secrets. Ensure that the runtime service '
"account '{sa}' has access to the secrets. You can do that by "
"granting the permission 'roles/secretmanager.secretAccessor' to "
'the runtime service account on the project or secrets.\n'
)
command = (
'E.g. gcloud projects add-iam-policy-binding {project} --member='
"'serviceAccount:{sa}' --role='roles/secretmanager.secretAccessor'"
)
# TODO(b/185133105): Log this message for secret access failures only
log.warning(
(message + command).format(project=project, sa=service_account_email)
)
def _ApplySecretsArgsToFunction(function, args):
"""Populates cloud function message with secrets payload if applicable.
It compares the CLI args with the existing secrets configuration to compute
the effective secrets configuration.
Args:
function: Cloud function message to be checked and populated.
args: All CLI arguments.
Returns:
updated_fields: update mask containing the list of fields to be updated.
"""
if not secrets_config.IsArgsSpecified(args):
return []
old_secrets = secrets_util.GetSecretsAsDict(
function.secretEnvironmentVariables, function.secretVolumes
)
new_secrets = {}
try:
new_secrets = secrets_config.ApplyFlags(
old_secrets,
args,
_GetProject(),
project_util.GetProjectNumber(_GetProject()),
)
except ArgumentTypeError as error:
exceptions.reraise(function_exceptions.FunctionsError(error))
if new_secrets:
_LogSecretsPermissionMessage(_GetProject(), function.serviceAccountEmail)
old_secret_env_vars, old_secret_volumes = secrets_config.SplitSecretsDict(
old_secrets
)
new_secret_env_vars, new_secret_volumes = secrets_config.SplitSecretsDict(
new_secrets
)
updated_fields = []
if old_secret_env_vars != new_secret_env_vars:
function.secretEnvironmentVariables = secrets_util.SecretEnvVarsToMessages(
new_secret_env_vars, api_util.GetApiMessagesModule()
)
updated_fields.append('secretEnvironmentVariables')
if old_secret_volumes != new_secret_volumes:
function.secretVolumes = secrets_util.SecretVolumesToMessages(
new_secret_volumes, api_util.GetApiMessagesModule()
)
updated_fields.append('secretVolumes')
return updated_fields
def _ApplyCMEKArgsToFunction(function_ref, function, args):
"""Configures CMEK related fields for the Cloud Function.
It sets or clears the kms_key_name and docker_repository fields based on the
CLI args.
Args:
function_ref: Function resource.
function: Cloud function message to be configured.
args: All CLI arguments.
Returns:
updated_fields: update mask containing the list of fields to be updated.
Raises:
InvalidArgumentException: If the specified KMS key or Docker repository is
not compatible with the function.
RequiredArgumentException: If Docker repository is not specified when KMS
key is configured.
"""
updated_fields = []
if args.IsSpecified('kms_key') or args.IsSpecified('clear_kms_key'):
old_kms_key = function.kmsKeyName
function.kmsKeyName = None if args.clear_kms_key else args.kms_key
if function.kmsKeyName != old_kms_key:
if function.kmsKeyName:
cmek_util.ValidateKMSKeyForFunction(function.kmsKeyName, function_ref)
updated_fields.append('kmsKeyName')
if args.IsSpecified('docker_repository') or args.IsSpecified(
'clear_docker_repository'
):
old_docker_repository = function.dockerRepository
new_docker_repository = (
None
if args.IsSpecified('clear_docker_repository')
else cmek_util.NormalizeDockerRepositoryFormat(args.docker_repository)
)
function.dockerRepository = new_docker_repository
if function.dockerRepository != old_docker_repository:
if function.dockerRepository:
cmek_util.ValidateDockerRepositoryForFunction(
function.dockerRepository, function_ref
)
updated_fields.append('dockerRepository')
if function.kmsKeyName and not function.dockerRepository:
raise calliope_exceptions.RequiredArgumentException(
'--docker-repository',
(
'A Docker repository must be specified when a KMS key is configured'
' for the function.'
),
)
return updated_fields
def _ApplyDockerRegistryArgsToFunction(function, args):
"""Populates the `docker_registry` field of a Cloud Function message.
Args:
function: Cloud function message to be checked and populated.
args: All CLI arguments.
Returns:
updated_fields: update mask containing the list of fields to be updated.
Raises:
InvalidArgumentException: If Container Registry is specified for a CMEK
deployment (CMEK is only supported by Artifact Registry).
"""
updated_fields = []
if args.IsSpecified('docker_registry'):
kms_key = function.kmsKeyName
if args.IsSpecified('kms_key') or args.IsSpecified('clear_kms_key'):
kms_key = None if args.clear_kms_key else args.kms_key
if kms_key is not None and args.docker_registry == 'container-registry':
raise calliope_exceptions.InvalidArgumentException(
'--docker-registry',
(
'CMEK deployments are not supported by Container Registry.'
'Please either use Artifact Registry or do not specify a KMS key '
'for the function (you may need to clear it).'
),
)
function.dockerRegistry = enum_util.ParseDockerRegistry(
args.docker_registry
)
updated_fields.append('dockerRegistry')
return updated_fields
def _DefaultDockerRegistryIfUnspecified(function, all_updated_fields):
"""Sets the default for `docker_registry` field of a Cloud Function message.
Args:
function: Cloud function message to be checked and populated.
all_updated_fields: List of all fields that are being updated within the
deployment request.
Returns:
updated_fields: update mask containing the list of fields to be updated.
"""
updated_fields = []
# Set the default only if the request is not completely empty.
if all_updated_fields and 'dockerRegistry' not in all_updated_fields:
function.dockerRegistry = enum_util.ParseDockerRegistry('artifact-registry')
updated_fields.append('dockerRegistry')
return updated_fields
def _PromptToEnableArtifactRegistryIfRequired(cli_args):
"""Checks if the deployment needs Artifact Registry and prompts to enable it.
Args:
cli_args: CLI arguments passed to the deployment request.
"""
if (
cli_args.IsSpecified('docker_registry')
and cli_args.docker_registry == 'container-registry'
):
return
api_enablement.PromptToEnableApiIfDisabled(
'artifactregistry.googleapis.com', enable_by_default=True
)
def _GetActiveKMSKey(function, args):
"""Retrieves the KMS key for the function.
This is either the KMS key provided via the kms-key flag or the KMS key
configured for the existing function if any.
Args:
function: existing cloud function if any.
args: CLI arguments.
Returns:
kms_key: KMS key if any.
"""
kms_key = function.kmsKeyName
if args.IsSpecified('kms_key') or args.IsSpecified('clear_kms_key'):
kms_key = None if args.clear_kms_key else args.kms_key
return kms_key
def _ApplyBuildpackStackArgsToFunction(function, args, track):
"""Populates the `buildpack_stack` field of a Cloud Function message.
Args:
function: Cloud function message to be populated.
args: All CLI arguments.
track: release track.
Returns:
updated_fields: update mask containing the list of fields to be updated.
"""
if track is not base.ReleaseTrack.ALPHA:
return []
updated_fields = []
if args.IsSpecified('buildpack_stack'):
function.buildpackStack = args.buildpack_stack
updated_fields.append('buildpack_stack')
return updated_fields
def _ApplyBuildServiceAccountToFunction(function, args):
"""Populates the `build_service_account` field of a Cloud Function message.
Args:
function: Cloud function message to be populated.
args: All CLI arguments.
Returns:
updated_fields: update mask containing the list of fields to be updated.
"""
updated_fields = []
if args.IsSpecified('build_service_account') or args.IsSpecified(
'clear_build_service_account'
):
function.buildServiceAccount = args.build_service_account
updated_fields.append('build_service_account')
return updated_fields
def _CreateBindPolicyCommand(function_ref):
template = (
'gcloud functions add-iam-policy-binding %s --region=%s '
'--member=allUsers --role=roles/cloudfunctions.invoker'
)
return template % (function_ref.Name(), function_ref.locationsId)
def _CreateStackdriverURLforBuildLogs(build_id, project_id):
query_param = (
'resource.type=build\nresource.labels.build_id=%s\n'
'logName=projects/%s/logs/cloudbuild' % (build_id, project_id)
)
return (
'https://console.cloud.google.com/logs/viewer?'
'project=%s&advancedFilter=%s'
% (project_id, urllib.parse.quote(query_param, safe=''))
)
def _GetProject():
return properties.VALUES.core.project.GetOrFail()
def _CreateCloudBuildLogURL(build_name):
matched_groups = _BUILD_NAME_REGEX.match(build_name).groupdict()
return (
'https://console.cloud.google.com/'
'cloud-build/builds;region=%s/%s?project=%s'
% (
matched_groups['region'],
matched_groups['buildid'],
matched_groups['projectnumber'],
)
)
def _ValidateGen2OnlyFlag(args):
"""Validate flags that are only supported in GCFv2."""
for flag, feature in _GEN2_ONLY_FLAGS.items():
if args.IsKnownAndSpecified(flag):
raise calliope_exceptions.InvalidArgumentException(
'--' + flag.replace('_', '-'),
'{} is not supported for 1st gen Cloud Functions.'.format(feature),
)
def Run(args, track=None):
"""Run a function deployment with the given args."""
flags.ValidateV1TimeoutFlag(args)
_ValidateGen2OnlyFlag(args)
# Check for labels that start with `deployment`, which is not allowed.
labels_util.CheckNoDeploymentLabels('--remove-labels', args.remove_labels)
labels_util.CheckNoDeploymentLabels('--update-labels', args.update_labels)
# Check that exactly one trigger type is specified properly.
trigger_util.ValidateTriggerArgs(
args.trigger_event,
args.trigger_resource,
args.IsSpecified('retry'),
args.IsSpecified('trigger_http'),
)
trigger_params = trigger_util.GetTriggerEventParams(
args.trigger_http,
args.trigger_bucket,
args.trigger_topic,
args.trigger_event,
args.trigger_resource,
)
function_ref = args.CONCEPTS.name.Parse()
function_url = function_ref.RelativeName()
messages = api_util.GetApiMessagesModule(track)
# Get an existing function or create a new one.
function = api_util.GetFunction(function_url)
is_new_function = function is None
had_vpc_connector = (
bool(function.vpcConnector) if not is_new_function else False
)
had_http_trigger = (
bool(function.httpsTrigger) if not is_new_function else False
)
if is_new_function:
trigger_util.CheckTriggerSpecified(args)
function = messages.CloudFunction()
function.name = function_url
elif trigger_params:
# If the new deployment would implicitly change the trigger_event type
# raise error
trigger_util.CheckLegacyTriggerUpdate(
function.eventTrigger, trigger_params['trigger_event']
)
# Keep track of which fields are updated in the case of patching.
updated_fields = []
# Populate function properties based on args.
if args.entry_point:
function.entryPoint = args.entry_point
updated_fields.append('entryPoint')
if args.timeout:
function.timeout = '{}s'.format(args.timeout)
updated_fields.append('timeout')
if args.memory:
# For v1 convert args.memory from str to number of bytes in int
args.memory = flags.ParseMemoryStrToNumBytes(args.memory)
function.availableMemoryMb = utils.BytesToMb(args.memory)
updated_fields.append('availableMemoryMb')
if args.service_account:
function.serviceAccountEmail = args.service_account
updated_fields.append('serviceAccountEmail')
if args.IsSpecified('max_instances') or args.IsSpecified(
'clear_max_instances'
):
max_instances = 0 if args.clear_max_instances else args.max_instances
function.maxInstances = max_instances
updated_fields.append('maxInstances')
if args.IsSpecified('min_instances') or args.IsSpecified(
'clear_min_instances'
):
min_instances = 0 if args.clear_min_instances else args.min_instances
function.minInstances = min_instances
updated_fields.append('minInstances')
if args.IsSpecified('runtime'):
function.runtime = args.runtime
updated_fields.append('runtime')
elif is_new_function:
raise calliope_exceptions.RequiredArgumentException(
'runtime', 'Flag `--runtime` is required for new functions.'
)
if args.IsSpecified('runtime_update_policy'):
if args.runtime_update_policy == 'automatic':
function.automaticUpdatePolicy = messages.AutomaticUpdatePolicy()
function.onDeployUpdatePolicy = None
if args.runtime_update_policy == 'on-deploy':
function.onDeployUpdatePolicy = messages.OnDeployUpdatePolicy()
function.automaticUpdatePolicy = None
updated_fields.extend(['automaticUpdatePolicy', 'onDeployUpdatePolicy'])
warning = api_util.ValidateRuntimeOrRaise(
v2_client.FunctionsClient(base.ReleaseTrack.GA),
function.runtime,
function_ref.locationsId,
)
if warning:
log.warning(warning)
vpc_connector_ref = args.CONCEPTS.vpc_connector.Parse()
if args.clear_vpc_connector:
function.vpcConnector = ''
function.vpcConnectorEgressSettings = None
updated_fields.append('vpcConnector')
updated_fields.append('vpcConnectorEgressSettings')
if vpc_connector_ref:
function.vpcConnector = vpc_connector_ref.RelativeName()
updated_fields.append('vpcConnector')
if args.IsSpecified('egress_settings'):
will_have_vpc_connector = (
had_vpc_connector and not args.clear_vpc_connector
) or vpc_connector_ref
if not will_have_vpc_connector:
raise calliope_exceptions.RequiredArgumentException(
'vpc-connector',
'Flag `--vpc-connector` is required for setting `egress-settings`.',
)
egress_settings_enum = arg_utils.ChoiceEnumMapper(
arg_name='egress_settings',
message_enum=function.VpcConnectorEgressSettingsValueValuesEnum,
custom_mappings=flags.EGRESS_SETTINGS_MAPPING,
).GetEnumForChoice(args.egress_settings)
function.vpcConnectorEgressSettings = egress_settings_enum
updated_fields.append('vpcConnectorEgressSettings')
if args.IsSpecified('ingress_settings'):
ingress_settings_enum = arg_utils.ChoiceEnumMapper(
arg_name='ingress_settings',
message_enum=function.IngressSettingsValueValuesEnum,
custom_mappings=flags.INGRESS_SETTINGS_MAPPING,
).GetEnumForChoice(args.ingress_settings)
function.ingressSettings = ingress_settings_enum
updated_fields.append('ingressSettings')
if args.build_worker_pool or args.clear_build_worker_pool:
function.buildWorkerPool = (
'' if args.clear_build_worker_pool else args.build_worker_pool
)
updated_fields.append('buildWorkerPool')
# Populate trigger properties of function based on trigger args.
if args.trigger_http:
function.httpsTrigger = messages.HttpsTrigger()
function.eventTrigger = None
updated_fields.extend(['eventTrigger', 'httpsTrigger'])
if trigger_params:
function.eventTrigger = trigger_util.CreateEventTrigger(**trigger_params)
function.httpsTrigger = None
updated_fields.extend(['eventTrigger', 'httpsTrigger'])
if args.IsSpecified('retry'):
updated_fields.append('eventTrigger.failurePolicy')
if args.retry:
function.eventTrigger.failurePolicy = messages.FailurePolicy()
function.eventTrigger.failurePolicy.retry = messages.Retry()
else:
function.eventTrigger.failurePolicy = None
elif function.eventTrigger:
function.eventTrigger.failurePolicy = None
will_have_http_trigger = had_http_trigger or args.trigger_http
if args.IsSpecified('security_level') or (
will_have_http_trigger and is_new_function
):
if not will_have_http_trigger:
raise calliope_exceptions.RequiredArgumentException(
'trigger-http',
'Flag `--trigger-http` is required for setting `security-level`.',
)
# SecurityLevelValueValuesEnum('SECURE_ALWAYS' | 'SECURE_OPTIONAL')
security_level_enum = arg_utils.ChoiceEnumMapper(
arg_name='security_level',
message_enum=function.httpsTrigger.SecurityLevelValueValuesEnum,
custom_mappings=flags.SECURITY_LEVEL_MAPPING,
).GetEnumForChoice(args.security_level)
function.httpsTrigger.securityLevel = security_level_enum
updated_fields.append('httpsTrigger.securityLevel')
kms_key = _GetActiveKMSKey(function, args)
# Populate source properties of function based on source args.
# Only Add source to function if its explicitly provided, a new function,
# using a stage bucket or deploy of an existing function that previously
# used local source.
if (
args.source
or args.stage_bucket
or is_new_function
or function.sourceUploadUrl
):
updated_fields.extend(
source_util.SetFunctionSourceProps(
function,
function_ref,
args.source,
args.stage_bucket,
args.ignore_file,
kms_key,
)
)
# Apply label args to function
if labels_util.SetFunctionLabels(
function, args.update_labels, args.remove_labels, args.clear_labels
):
updated_fields.append('labels')
# Apply build environment variables args to function
updated_fields.extend(_ApplyBuildEnvVarsArgsToFunction(function, args))
# Apply environment variables args to function
updated_fields.extend(_ApplyEnvVarsArgsToFunction(function, args))
ensure_all_users_invoke = flags.ShouldEnsureAllUsersInvoke(args)
deny_all_users_invoke = flags.ShouldDenyAllUsersInvoke(args)
# Applies secrets args to function
updated_fields.extend(_ApplySecretsArgsToFunction(function, args))
# Applies CMEK args to function
updated_fields.extend(_ApplyCMEKArgsToFunction(function_ref, function, args))
# Applies remaining Artifact Registry args to the function. Note that one of
# them, docker_repository, was already added as part of CMEK
updated_fields.extend(_ApplyDockerRegistryArgsToFunction(function, args))
# Applies Buildpack stack args to the function.
updated_fields.extend(
_ApplyBuildpackStackArgsToFunction(function, args, track)
)
updated_fields.extend(_ApplyBuildServiceAccountToFunction(function, args))
# TODO(b/287538740): Can be cleaned up after a full transition to the AR.
# Expected to be set after all other fields.
updated_fields.extend(
_DefaultDockerRegistryIfUnspecified(function, updated_fields)
)
api_enablement.PromptToEnableApiIfDisabled('cloudbuild.googleapis.com')
_PromptToEnableArtifactRegistryIfRequired(args)
if is_new_function:
if (
function.httpsTrigger
and not ensure_all_users_invoke
and not deny_all_users_invoke
and api_util.CanAddFunctionIamPolicyBinding(_GetProject())
):
ensure_all_users_invoke = console_io.PromptContinue(
prompt_string=(
'Allow unauthenticated invocations of new function [{}]?'.format(
args.NAME
)
),
default=False,
)
service_account_util.ValidateDefaultBuildServiceAccountAndPromptWarning(
_GetProject(), function_ref.locationsId, function.buildServiceAccount
)
op = api_util.CreateFunction(function, function_ref.Parent().RelativeName())
if api_util.IsGcrRepository(function):
api_util.ValidateSecureImageRepositoryOrWarn(
function_ref.locationsId, _GetProject()
)
if (
function.httpsTrigger
and not ensure_all_users_invoke
and not deny_all_users_invoke
):
template = (
'Function created with limited-access IAM policy. '
'To enable unauthorized access consider `%s`'
)
log.warning(template % _CreateBindPolicyCommand(function_ref))
deny_all_users_invoke = True
elif updated_fields:
service_account_util.ValidateDefaultBuildServiceAccountAndPromptWarning(
_GetProject(), function_ref.locationsId, function.buildServiceAccount
)
op = api_util.PatchFunction(function, updated_fields)
if api_util.IsGcrRepository(function):
api_util.ValidateSecureImageRepositoryOrWarn(
function_ref.locationsId, _GetProject()
)
else:
op = None # Nothing to wait for
if not ensure_all_users_invoke and not deny_all_users_invoke:
log.status.Print('Nothing to update.')
return
stop_trying_perm_set = [False]
# The server asyncrhonously sets allUsers invoker permissions some time after
# we create the function. That means, to remove it, we need do so after the
# server adds it. We can remove this mess after the default changes.
# TODO(b/130604453): Remove the "remove" path, only bother adding. Remove the
# logic from the polling loop. Remove the ability to add logic like this to
# the polling loop.
# Because of the DRS policy restrictions, private-by-default behavior is not
# guaranteed for all projects and we need this hack until IAM deny is
# implemented and all projects have private-by-default.
def TryToSetInvokerPermission():
"""Try to make the invoker permission be what we said it should.
This is for executing in the polling loop, and will stop trying as soon as
it succeeds at making a change.
"""
if stop_trying_perm_set[0]:
return
try:
if ensure_all_users_invoke:
api_util.AddFunctionIamPolicyBinding(function.name)
stop_trying_perm_set[0] = True
elif deny_all_users_invoke:
stop_trying_perm_set[0] = (
api_util.RemoveFunctionIamPolicyBindingIfFound(function.name)
)
except calliope_exceptions.HttpException:
stop_trying_perm_set[0] = True
log.warning(
'Setting IAM policy failed, try `%s`'
% _CreateBindPolicyCommand(function_ref)
)
log_stackdriver_url = [True]
def TryToLogStackdriverURL(op):
"""Logs stackdriver URL.
This is for executing in the polling loop, and will stop trying as soon as
it succeeds at making a change.
Args:
op: the operation
"""
if log_stackdriver_url[0] and op.metadata:
metadata = encoding.PyValueToMessage(
messages.OperationMetadataV1, encoding.MessageToPyValue(op.metadata)
)
if metadata.buildName and _BUILD_NAME_REGEX.match(metadata.buildName):
log.status.Print(
'\nFor Cloud Build Logs, visit: %s'
% _CreateCloudBuildLogURL(metadata.buildName)
)
log_stackdriver_url[0] = False
elif metadata.buildId:
sd_info_template = '\nFor Cloud Build Stackdriver Logs, visit: %s'
log.status.Print(
sd_info_template
% _CreateStackdriverURLforBuildLogs(metadata.buildId, _GetProject())
)
log_stackdriver_url[0] = False
if op:
try_set_invoker = None
if function.httpsTrigger:
try_set_invoker = TryToSetInvokerPermission
api_util.WaitForFunctionUpdateOperation(
op,
try_set_invoker=try_set_invoker,
on_every_poll=[TryToLogStackdriverURL],
)
return api_util.GetFunction(function.name)

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 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.
"""Enum utilities for 'functions deploy...'."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis as core_apis
from googlecloudsdk.command_lib.functions import flags
from googlecloudsdk.command_lib.util.apis import arg_utils
def ParseDockerRegistry(docker_registry_str):
"""Converts string value of the docker_registry enum to its enum equivalent.
Args:
docker_registry_str: a string representing the enum value
Returns:
Corresponding DockerRegistryValueValuesEnum value or None for invalid values
"""
func_message = core_apis.GetMessagesModule('cloudfunctions', 'v1')
return arg_utils.ChoiceEnumMapper(
arg_name='docker_registry',
message_enum=func_message.CloudFunction.DockerRegistryValueValuesEnum,
custom_mappings=flags.DOCKER_REGISTRY_MAPPING,
).GetEnumForChoice(docker_registry_str)

View File

@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""'functions deploy' utilities for labels."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v1 import util as api_util
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.command_lib.util.args import labels_util as args_labels_util
NO_LABELS_STARTING_WITH_DEPLOY_MESSAGE = (
'Label keys starting with `deployment` are reserved for use by deployment '
'tools and cannot be specified manually.'
)
def CheckNoDeploymentLabels(flag_name, label_names):
"""Check for labels that start with `deployment`, which is not allowed.
Args:
flag_name: The name of the flag to include in case of an exception
label_names: A list of label names to check
Raises:
calliope_exceptions.InvalidArgumentException
"""
if not label_names:
return
for label_name in label_names:
if label_name.startswith('deployment'):
raise calliope_exceptions.InvalidArgumentException(
flag_name, NO_LABELS_STARTING_WITH_DEPLOY_MESSAGE
)
def SetFunctionLabels(function, update_labels, remove_labels, clear_labels):
"""Set the labels on a function based on args.
Args:
function: the function to set the labels on
update_labels: a dict of <label-name>-<label-value> pairs for the labels to
be updated, from --update-labels
remove_labels: a list of the labels to be removed, from --remove-labels
clear_labels: a bool representing whether or not to clear all labels, from
--clear-labels
Returns:
A bool indicating whether or not any labels were updated on the function.
"""
labels_to_update = update_labels or {}
labels_to_update['deployment-tool'] = 'cli-gcloud'
labels_diff = args_labels_util.Diff(
additions=labels_to_update, subtractions=remove_labels, clear=clear_labels
)
messages = api_util.GetApiMessagesModule()
labels_update = labels_diff.Apply(
messages.CloudFunction.LabelsValue, function.labels
)
if labels_update.needs_update:
function.labels = labels_update.labels
return True
return False

View File

@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""V1-specific utilities for function source code."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
from apitools.base.py import exceptions as http_exceptions
from googlecloudsdk.api_lib.functions import cmek_util
from googlecloudsdk.api_lib.functions.v1 import util as api_util
from googlecloudsdk.command_lib.functions import source_util
from googlecloudsdk.core import log
from googlecloudsdk.core.util import files as file_utils
def _AddDefaultBranch(source_archive_url):
cloud_repo_pattern = (
r'^https://source\.developers\.google\.com'
r'/projects/[^/]+'
r'/repos/[^/]+$'
)
if re.match(cloud_repo_pattern, source_archive_url):
return source_archive_url + '/moveable-aliases/master'
return source_archive_url
def _GetUploadUrl(messages, service, function_ref, kms_key=None):
"""Retrieves the upload url to upload source code."""
generate_upload_url_request = None
if kms_key:
generate_upload_url_request = messages.GenerateUploadUrlRequest(
kmsKeyName=kms_key
)
request = (
messages.CloudfunctionsProjectsLocationsFunctionsGenerateUploadUrlRequest
)(
parent='projects/{}/locations/{}'.format(
function_ref.projectsId, function_ref.locationsId
),
generateUploadUrlRequest=generate_upload_url_request,
)
try:
response = service.GenerateUploadUrl(request)
return response.uploadUrl
except http_exceptions.HttpError as e:
cmek_util.ProcessException(e, kms_key)
raise e
def SetFunctionSourceProps(
function,
function_ref,
source_arg,
stage_bucket,
ignore_file=None,
kms_key=None,
):
"""Add sources to function.
Args:
function: The function to add a source to.
function_ref: The reference to the function.
source_arg: Location of source code to deploy.
stage_bucket: The name of the Google Cloud Storage bucket where source code
will be stored.
ignore_file: custom ignore_file name. Override .gcloudignore file to
customize files to be skipped.
kms_key: KMS key configured for the function.
Returns:
A list of fields on the function that have been changed.
Raises:
FunctionsError: If the kms_key doesn't exist or GCF P4SA lacks permissions.
"""
function.sourceArchiveUrl = None
function.sourceRepository = None
function.sourceUploadUrl = None
messages = api_util.GetApiMessagesModule()
if source_arg is None:
source_arg = '.'
source_arg = source_arg or '.'
if source_arg.startswith('gs://'):
if not source_arg.endswith('.zip'):
# Users may have .zip archives with unusual names, and we don't want to
# prevent those from being deployed; the deployment should go through so
# just warn here.
log.warning(
'[{}] does not end with extension `.zip`. '
'The `--source` argument must designate the zipped source archive '
'when providing a Google Cloud Storage URI.'.format(source_arg)
)
function.sourceArchiveUrl = source_arg
return ['sourceArchiveUrl']
elif source_arg.startswith('https://'):
function.sourceRepository = messages.SourceRepository(
url=_AddDefaultBranch(source_arg)
)
return ['sourceRepository']
with file_utils.TemporaryDirectory() as tmp_dir:
zip_file = source_util.CreateSourcesZipFile(
tmp_dir,
source_arg,
ignore_file,
enforce_size_limit=True,
)
service = api_util.GetApiClientInstance().projects_locations_functions
if stage_bucket:
dest_object = source_util.UploadToStageBucket(
zip_file, function_ref, stage_bucket
)
function.sourceArchiveUrl = dest_object.ToUrl()
return ['sourceArchiveUrl']
upload_url = _GetUploadUrl(messages, service, function_ref, kms_key)
source_util.UploadToGeneratedUrl(
zip_file,
upload_url,
extra_headers={
# Magic header that needs to be specified per:
# https://cloud.google.com/functions/docs/reference/rest/v1/projects.locations.functions/generateUploadUrl
'x-goog-content-length-range': '0,104857600',
},
)
function.sourceUploadUrl = upload_url
return ['sourceUploadUrl']

View File

@@ -0,0 +1,300 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""'functions deploy' utilities for triggers."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v1 import exceptions
from googlecloudsdk.api_lib.functions.v1 import triggers
from googlecloudsdk.api_lib.functions.v1 import util as api_util
from googlecloudsdk.api_lib.storage import storage_util
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.core import exceptions as core_exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources
class TriggerCompatibilityError(core_exceptions.Error):
"""Raised when deploy trigger is incompatible with existing trigger."""
GCS_COMPATIBILITY_ERROR = (
'The `--trigger-bucket` flag corresponds to the '
'`google.storage.object.finalize` event on file creation. '
'You are trying to update a function that is using the legacy '
'`providers/cloud.storage/eventTypes/object.change` event type. To get the '
'legacy behavior, use the `--trigger-event` and `--trigger-resource` flags '
'e.g. `gcloud functions deploy --trigger-event '
'providers/cloud.storage/eventTypes/object.change '
'--trigger-resource [your_bucket_name]`.'
'Please see https://cloud.google.com/storage/docs/pubsub-notifications for '
'more information on storage event types.'
)
PUBSUB_COMPATIBILITY_ERROR = (
'The format of the Pub/Sub event source has changed. You are trying to '
'update a function that is using the legacy '
'`providers/cloud.pubsub/eventTypes/topic.publish` event type. To get the '
'legacy behavior, use the `--trigger-event` and `--trigger-resource` flags '
'e.g. `gcloud functions deploy --trigger-event '
'providers/cloud.pubsub/eventTypes/topic.publish '
'--trigger-resource [your_topic_name]`.'
)
# Old style trigger events as of 02/2018.
LEGACY_TRIGGER_EVENTS = {
'providers/cloud.storage/eventTypes/object.change': GCS_COMPATIBILITY_ERROR,
'providers/cloud.pubsub/eventTypes/topic.publish': (
PUBSUB_COMPATIBILITY_ERROR
),
}
def CheckTriggerSpecified(args):
if not (
args.IsSpecified('trigger_topic')
or args.IsSpecified('trigger_bucket')
or args.IsSpecified('trigger_http')
or args.IsSpecified('trigger_event')
):
raise calliope_exceptions.OneOfArgumentsRequiredException(
[
'--trigger-topic',
'--trigger-bucket',
'--trigger-http',
'--trigger-event',
],
'You must specify a trigger when deploying a new function.',
)
def ValidateTriggerArgs(
trigger_event, trigger_resource, retry_specified, trigger_http_specified
):
"""Check if args related function triggers are valid.
Args:
trigger_event: The trigger event
trigger_resource: The trigger resource
retry_specified: Whether or not `--retry` was specified
trigger_http_specified: Whether or not `--trigger-http` was specified
Raises:
FunctionsError.
"""
# Check that Event Type is valid
trigger_provider = triggers.TRIGGER_PROVIDER_REGISTRY.ProviderForEvent(
trigger_event
)
trigger_provider_label = trigger_provider.label
if trigger_provider_label != triggers.UNADVERTISED_PROVIDER_LABEL:
resource_type = triggers.TRIGGER_PROVIDER_REGISTRY.Event(
trigger_provider_label, trigger_event
).resource_type
if trigger_resource is None and resource_type != triggers.Resources.PROJECT:
raise exceptions.FunctionsError(
'You must provide --trigger-resource when using '
'--trigger-event={}'.format(trigger_event)
)
if retry_specified and trigger_http_specified:
raise calliope_exceptions.ConflictingArgumentsException(
'--trigger-http', '--retry'
)
def _GetBucketTriggerEventParams(trigger_bucket):
bucket_name = trigger_bucket[5:-1]
return {
'trigger_provider': 'cloud.storage',
'trigger_event': 'google.storage.object.finalize',
'trigger_resource': bucket_name,
}
def _GetTopicTriggerEventParams(trigger_topic):
return {
'trigger_provider': 'cloud.pubsub',
'trigger_event': 'google.pubsub.topic.publish',
'trigger_resource': trigger_topic,
}
def _GetEventTriggerEventParams(trigger_event, trigger_resource):
"""Get the args for creating an event trigger.
Args:
trigger_event: The trigger event
trigger_resource: The trigger resource
Returns:
A dictionary containing trigger_provider, trigger_event, and
trigger_resource.
"""
trigger_provider = triggers.TRIGGER_PROVIDER_REGISTRY.ProviderForEvent(
trigger_event
)
trigger_provider_label = trigger_provider.label
result = {
'trigger_provider': trigger_provider_label,
'trigger_event': trigger_event,
'trigger_resource': trigger_resource,
}
if trigger_provider_label == triggers.UNADVERTISED_PROVIDER_LABEL:
return result
resource_type = triggers.TRIGGER_PROVIDER_REGISTRY.Event(
trigger_provider_label, trigger_event
).resource_type
if resource_type == triggers.Resources.TOPIC:
trigger_resource = api_util.ValidatePubsubTopicNameOrRaise(trigger_resource)
elif resource_type == triggers.Resources.BUCKET:
trigger_resource = storage_util.BucketReference.FromUrl(
trigger_resource
).bucket
elif resource_type in [
triggers.Resources.FIREBASE_ANALYTICS_EVENT,
triggers.Resources.FIREBASE_DB,
triggers.Resources.FIRESTORE_DOC,
]:
pass
elif resource_type == triggers.Resources.PROJECT:
if trigger_resource:
properties.VALUES.core.project.Validate(trigger_resource)
else:
# Check if programmer allowed other methods in
# api_util.PROVIDER_EVENT_RESOURCE but forgot to update code here
raise core_exceptions.InternalError()
# checked if provided resource and path have correct format
result['trigger_resource'] = trigger_resource
return result
def GetTriggerEventParams(
trigger_http, trigger_bucket, trigger_topic, trigger_event, trigger_resource
):
"""Check --trigger-* arguments and deduce if possible.
0. if --trigger-http is return None.
1. if --trigger-bucket return bucket trigger args (_GetBucketTriggerArgs)
2. if --trigger-topic return pub-sub trigger args (_GetTopicTriggerArgs)
3. if --trigger-event, deduce provider and resource from registry and return
Args:
trigger_http: The trigger http
trigger_bucket: The trigger bucket
trigger_topic: The trigger topic
trigger_event: The trigger event
trigger_resource: The trigger resource
Returns:
None, when using HTTPS trigger. Otherwise a dictionary containing
trigger_provider, trigger_event, and trigger_resource.
"""
if trigger_http:
return None
if trigger_bucket:
return _GetBucketTriggerEventParams(trigger_bucket)
if trigger_topic:
return _GetTopicTriggerEventParams(trigger_topic)
if trigger_event:
return _GetEventTriggerEventParams(trigger_event, trigger_resource)
elif trigger_resource:
log.warning(
'Ignoring the flag --trigger-resource. The flag --trigger-resource is '
'provided but --trigger-event is not. If you intend to change '
'trigger-resource you need to provide trigger-event as well.'
)
def ConvertTriggerArgsToRelativeName(
trigger_provider, trigger_event, trigger_resource
):
"""Prepares resource field for Function EventTrigger to use in API call.
API uses relative resource name in EventTrigger message field. The
structure of that identifier depends on the resource type which depends on
combination of --trigger-provider and --trigger-event arguments' values.
This function chooses the appropriate form, fills it with required data and
returns as a string.
Args:
trigger_provider: The --trigger-provider flag value.
trigger_event: The --trigger-event flag value.
trigger_resource: The --trigger-resource flag value.
Returns:
Relative resource name to use in EventTrigger field.
"""
resource_type = triggers.TRIGGER_PROVIDER_REGISTRY.Event(
trigger_provider, trigger_event
).resource_type
params = {}
if resource_type.value.collection_id in {
'google.firebase.analytics.event',
'google.firebase.database.ref',
'google.firestore.document',
}:
return trigger_resource
elif resource_type.value.collection_id == 'cloudresourcemanager.projects':
params['projectId'] = properties.VALUES.core.project.GetOrFail
elif resource_type.value.collection_id == 'pubsub.projects.topics':
params['projectsId'] = properties.VALUES.core.project.GetOrFail
elif resource_type.value.collection_id == 'cloudfunctions.projects.buckets':
pass
ref = resources.REGISTRY.Parse(
trigger_resource,
params,
collection=resource_type.value.collection_id,
)
return ref.RelativeName()
def CreateEventTrigger(trigger_provider, trigger_event, trigger_resource):
"""Create event trigger message.
Args:
trigger_provider: str, trigger provider label.
trigger_event: str, trigger event label.
trigger_resource: str, trigger resource name.
Returns:
A EventTrigger protobuf message.
"""
messages = api_util.GetApiMessagesModule()
event_trigger = messages.EventTrigger()
event_trigger.eventType = trigger_event
if trigger_provider == triggers.UNADVERTISED_PROVIDER_LABEL:
event_trigger.resource = trigger_resource
else:
event_trigger.resource = ConvertTriggerArgsToRelativeName(
trigger_provider, trigger_event, trigger_resource
)
return event_trigger
def CheckLegacyTriggerUpdate(function_trigger, new_trigger_event):
if function_trigger:
function_event_type = function_trigger.eventType
if (
function_event_type in LEGACY_TRIGGER_EVENTS
and function_event_type != new_trigger_event
):
error = LEGACY_TRIGGER_EVENTS[function_event_type]
raise TriggerCompatibilityError(error)

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""List event types available to Google Cloud Functions v1."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v1 import triggers
def Run(args):
"""Lists GCF v1 available event_types.
Args:
args: an argparse namespace. All the arguments that were provided to this
command invocation.
Yields:
events: List[v1.TriggerEvent], The list of v1 supported event types.
"""
del args
for provider in triggers.TRIGGER_PROVIDER_REGISTRY.providers:
for event in provider.events:
yield event

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions get-iam-policy` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v1 import util
def Run(args):
"""Get the IAM policy for a Google Cloud Function."""
function_ref = args.CONCEPTS.name.Parse()
return util.GetFunctionIamPolicy(function_ref.RelativeName())

View File

@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions list` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.py import exceptions as api_exceptions
from apitools.base.py import list_pager
from googlecloudsdk.api_lib.functions.v1 import util
from googlecloudsdk.calliope import exceptions as base_exceptions
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources
def _GetFunctionsAndLogUnreachable(message, attribute):
"""Response callback to log unreachable while generating functions."""
if message.unreachable:
log.warning(
'The following regions were fully or partially unreachable '
'for query: %s'
'This could be due to permission setup. Additional information'
'can be found in: '
'https://cloud.google.com/functions/docs/troubleshooting'
% ', '.join(message.unreachable)
)
return getattr(message, attribute)
def YieldFromLocations(locations, project, limit, messages, client):
"""Yield the functions from the given locations."""
for location in locations:
location_ref = resources.REGISTRY.Parse(
location,
params={'projectsId': project},
collection='cloudfunctions.projects.locations',
)
for function in _YieldFromLocation(location_ref, limit, messages, client):
yield function
def _YieldFromLocation(location_ref, limit, messages, client):
"""Yield the functions from the given location."""
list_generator = list_pager.YieldFromList(
service=client.projects_locations_functions,
request=_BuildRequest(location_ref, messages),
limit=limit,
field='functions',
batch_size_attribute='pageSize',
get_field_func=_GetFunctionsAndLogUnreachable,
)
# Decorators (e.g. util.CatchHTTPErrorRaiseHTTPException) don't work
# for generators. We have to catch the exception above the iteration loop,
# but inside the function.
try:
for item in list_generator:
yield item
except api_exceptions.HttpError as error:
msg = util.GetHttpErrorMessage(error)
exceptions.reraise(base_exceptions.HttpException(msg))
def _BuildRequest(location_ref, messages):
return messages.CloudfunctionsProjectsLocationsFunctionsListRequest(
parent=location_ref.RelativeName()
)
def Run(args):
"""List Google Cloud Functions."""
client = util.GetApiClientInstance()
messages = util.GetApiMessagesModule()
project = properties.VALUES.core.project.GetOrFail()
limit = args.limit
return YieldFromLocations(args.regions, project, limit, messages, client)

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""List regions available to Google Cloud Functions."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v1 import util
def Run(args):
"""Lists regions available with the given args."""
del args # unused by list command
return util.ListRegions()

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions remove-iam-policy-binding` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v1 import util
from googlecloudsdk.command_lib.iam import iam_util
def Run(args):
"""Remove a binding from the IAM policy for a Google Cloud Function."""
client = util.GetApiClientInstance()
messages = client.MESSAGES_MODULE
function_ref = args.CONCEPTS.name.Parse()
policy = client.projects_locations_functions.GetIamPolicy(
messages.CloudfunctionsProjectsLocationsFunctionsGetIamPolicyRequest(
resource=function_ref.RelativeName()))
iam_util.RemoveBindingFromIamPolicy(policy, args.member, args.role)
return client.projects_locations_functions.SetIamPolicy(
messages.CloudfunctionsProjectsLocationsFunctionsSetIamPolicyRequest(
resource=function_ref.RelativeName(),
setIamPolicyRequest=messages.SetIamPolicyRequest(policy=policy)))

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions set-iam-policy` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v1 import util
from googlecloudsdk.command_lib.iam import iam_util
def Run(args):
"""Set the IAM policy for a Google Cloud Function."""
client = util.GetApiClientInstance()
messages = client.MESSAGES_MODULE
function_ref = args.CONCEPTS.name.Parse()
policy, update_mask = iam_util.ParseYamlOrJsonPolicyFile(
args.policy_file, messages.Policy)
result = client.projects_locations_functions.SetIamPolicy(
messages.CloudfunctionsProjectsLocationsFunctionsSetIamPolicyRequest(
resource=function_ref.RelativeName(),
setIamPolicyRequest=messages.SetIamPolicyRequest(
policy=policy, updateMask=update_mask)))
iam_util.LogSetIamPolicy(function_ref.Name(), 'function')
return result

View File

@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions add-iam-policy-binding` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v2 import util as api_util
from googlecloudsdk.command_lib.functions import run_util
from googlecloudsdk.command_lib.functions.v2.add_invoker_policy_binding import command as add_invoker_policy_binding_command
from googlecloudsdk.command_lib.iam import iam_util
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_io
def Run(args, release_track):
"""Adds a binding to the IAM policy for a Google Cloud Function.
Args:
args: an argparse namespace. All the arguments that were provided to this
command invocation.
release_track: The relevant value from the
googlecloudsdk.calliope.base.ReleaseTrack enum.
Returns:
The updated IAM policy.
"""
client = api_util.GetClientInstance(release_track=release_track)
messages = api_util.GetMessagesModule(release_track=release_track)
function_ref = args.CONCEPTS.name.Parse()
function_relative_name = function_ref.RelativeName()
if args.role == 'roles/run.invoker':
log.warning(
'The role [roles/run.invoker] cannot be bound to a Cloud Function IAM'
' policy as it is a Cloud Run role. For 2nd gen functions, this role'
' must be granted on the underlying Cloud Run service. This'
' can be done by running the `gcloud functions'
' add-invoker-policy-binding` comand.\n'
)
if console_io.CanPrompt() and console_io.PromptContinue(
prompt_string=(
'Would you like to run this command instead and grant [{}]'
' permission to invoke function [{}]'.format(
args.member, function_ref.Name()
)
)
):
return add_invoker_policy_binding_command.Run(args, release_track)
policy = client.projects_locations_functions.GetIamPolicy(
messages.CloudfunctionsProjectsLocationsFunctionsGetIamPolicyRequest(
resource=function_relative_name))
iam_util.AddBindingToIamPolicy(
messages.Binding, policy, args.member, args.role
)
policy = client.projects_locations_functions.SetIamPolicy(
messages.CloudfunctionsProjectsLocationsFunctionsSetIamPolicyRequest(
resource=function_relative_name,
setIamPolicyRequest=messages.SetIamPolicyRequest(policy=policy),
)
)
if args.role in [
'roles/cloudfunctions.admin',
'roles/cloudfunctions.developer',
'roles/cloudfunctions.invoker',
]:
log.warning(
'The role [{role}] was successfully bound to member [{member}] but this'
' does not grant the member permission to invoke 2nd gen function'
' [{name}]. Instead, the role [roles/run.invoker] must be granted on'
' the underlying Cloud Run service. This can be done by running the'
' `gcloud functions add-invoker-policy-binding` command.\n'.format(
role=args.role, member=args.member, name=function_ref.Name()
)
)
if console_io.CanPrompt() and console_io.PromptContinue(
prompt_string=(
'Would you like to run this command and additionally grant [{}]'
' permission to invoke function [{}]'
).format(args.member, function_ref.Name()),
):
function = client.projects_locations_functions.Get(
messages.CloudfunctionsProjectsLocationsFunctionsGetRequest(
name=function_ref.RelativeName()
)
)
run_util.AddOrRemoveInvokerBinding(
function, args.member, add_binding=True
)
log.status.Print(
'The role [roles/run.invoker] was successfully bound to the'
' underlying Cloud Run service. You can view its IAM policy by'
' running:\n'
'gcloud run services get-iam-policy {}\n'.format(
function.serviceConfig.service
)
)
return policy
log.status.Print(
'Additional information on authenticating function calls can be found'
' at:\n'
'https://cloud.google.com/functions/docs/securing/authenticating#authenticating_function_to_function_calls'
)
return policy

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions add-invoker-policy-binding` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v2 import util as api_util
from googlecloudsdk.command_lib.functions import run_util
CLOUD_RUN_SERVICE_COLLECTION_K8S = 'run.namespaces.services'
CLOUD_RUN_SERVICE_COLLECTION_ONE_PLATFORM = 'run.projects.locations.services'
def Run(args, release_track):
"""Add an invoker binding to the IAM policy of a Google Cloud Function."""
client = api_util.GetClientInstance(release_track=release_track)
messages = api_util.GetMessagesModule(release_track=release_track)
function_ref = args.CONCEPTS.name.Parse()
function = client.projects_locations_functions.Get(
messages.CloudfunctionsProjectsLocationsFunctionsGetRequest(
name=function_ref.RelativeName()))
return run_util.AddOrRemoveInvokerBinding(
function, args.member, add_binding=True
)

View File

@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""Calls cloud run service of a Google Cloud Function."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v2 import util as v2_api_util
from googlecloudsdk.command_lib.config import config_helper
from googlecloudsdk.command_lib.functions import call_util
from googlecloudsdk.core.credentials import store
def GenerateIdToken(impersonate_service_account: bool = False):
"""Generate an expiring Google-signed OAuth2 identity token.
Args:
impersonate_service_account: bool, whether to enable a service account
impersonationwhen generating the token.
Returns:
token: str, expiring Google-signed OAuth2 identity token
"""
# str | None, account is either a user account or google service account.
account = None
# oauth2client.client.OAuth2Credentials |
# core.credentials.google_auth_credentials.Credentials
cred = store.Load(
# if account is None, implicitly retrieves properties.VALUES.core.account
account,
allow_account_impersonation=True,
use_google_auth=True)
# sets token on property of either
# credentials.token_response['id_token'] or
# credentials.id_tokenb64
store.Refresh(cred, is_impersonated_credential=impersonate_service_account)
credential = config_helper.Credential(cred)
# str, Expiring Google-signed OAuth2 identity token
token = credential.id_token
return token
def Run(args, release_track):
"""Call a v2 Google Cloud Function."""
v2_client = v2_api_util.GetClientInstance(release_track=release_track)
v2_messages = v2_client.MESSAGES_MODULE
function_ref = args.CONCEPTS.name.Parse()
# cloudfunctions_v2alpha_messages.Function
function = v2_client.projects_locations_functions.Get(
v2_messages.CloudfunctionsProjectsLocationsFunctionsGetRequest(
name=function_ref.RelativeName()))
call_util.UpdateHttpTimeout(args, function, 'v2', release_track)
cloud_run_uri = function.serviceConfig.uri
token = GenerateIdToken(args.IsSpecified('impersonate_service_account'))
auth_header = {'Authorization': 'Bearer {}'.format(token)}
return call_util.MakePostRequest(
cloud_run_uri, args, extra_headers=auth_header)

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions delete` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v2 import exceptions
from googlecloudsdk.api_lib.functions.v2 import util as api_util
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_io
def Run(args, release_track):
"""Delete a Google Cloud Function."""
client = api_util.GetClientInstance(release_track=release_track)
messages = api_util.GetMessagesModule(release_track=release_track)
function_ref = args.CONCEPTS.name.Parse()
function_relative_name = function_ref.RelativeName()
prompt_message = '2nd gen function [{0}] will be deleted.'.format(
function_relative_name
)
if not console_io.PromptContinue(message=prompt_message):
raise exceptions.FunctionsError('Deletion aborted by user.')
operation = client.projects_locations_functions.Delete(
messages.CloudfunctionsProjectsLocationsFunctionsDeleteRequest(
name=function_relative_name))
api_util.WaitForOperation(client, messages, operation, 'Deleting function')
log.DeletedResource(function_relative_name)

View File

@@ -0,0 +1,120 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""'functions deploy' utilities for environment variables."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import argparse
from googlecloudsdk.command_lib.util.args import map_util
import six
def EnvVarKeyType(key):
"""Validator for environment variable keys.
Args:
key: The environment variable key.
Returns:
The environment variable key.
Raises:
ArgumentTypeError: If the key is not a valid environment variable key.
"""
if not key:
raise argparse.ArgumentTypeError(
'Environment variable keys cannot be empty.'
)
if key.startswith('X_GOOGLE_'):
raise argparse.ArgumentTypeError(
'Environment variable keys that start with `X_GOOGLE_` are reserved '
'for use by deployment tools and cannot be specified manually.'
)
if '=' in key:
raise argparse.ArgumentTypeError(
'Environment variable keys cannot contain `=`.'
)
return key
def EnvVarValueType(value):
if not isinstance(value, six.text_type):
raise argparse.ArgumentTypeError(
'Environment variable values must be strings. Found {} (type {})'
.format(value, type(value))
)
return value
def AddUpdateEnvVarsFlags(parser):
"""Add flags for setting and removing environment variables.
Args:
parser: The argument parser.
"""
map_util.AddUpdateMapFlags(
parser,
'env-vars',
long_name='environment variables',
key_type=EnvVarKeyType,
value_type=EnvVarValueType,
)
def BuildEnvVarKeyType(key):
"""Validator for build environment variable keys.
All existing validations for environment variables are also applicable for
build environment variables.
Args:
key: The build environment variable key.
Returns:
The build environment variable key type.
Raises:
ArgumentTypeError: If the key is not valid.
"""
if key in [
'GOOGLE_ENTRYPOINT',
'GOOGLE_FUNCTION_TARGET',
'GOOGLE_RUNTIME',
'GOOGLE_RUNTIME_VERSION',
]:
raise argparse.ArgumentTypeError(
'{} is reserved for internal use by GCF deployments and cannot be used.'
.format(key)
)
return EnvVarKeyType(key)
def BuildEnvVarValueType(value):
return value
def AddBuildEnvVarsFlags(parser):
"""Add flags for managing build environment variables.
Args:
parser: The argument parser.
"""
map_util.AddUpdateMapFlags(
parser,
'build-env-vars',
long_name='build environment variables',
key_type=BuildEnvVarKeyType,
value_type=BuildEnvVarValueType,
)

View File

@@ -0,0 +1,63 @@
# -*- 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.
"""Utility functions for Functions specific to deploying Gen2 functions."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v2 import util as api_util
from googlecloudsdk.command_lib.projects import util as projects_util
def ensure_pubsub_sa_has_token_creator_role():
"""Ensures the project's Pub/Sub service account has permission to create tokens.
If the permission is missing, prompts the user to grant it. If the console
cannot prompt, prints a warning instead.
"""
pubsub_sa = 'service-{}@gcp-sa-pubsub.iam.gserviceaccount.com'.format(
projects_util.GetProjectNumber(api_util.GetProject())
)
api_util.PromptToBindRoleIfMissing(
pubsub_sa,
'roles/iam.serviceAccountTokenCreator',
alt_roles=['roles/pubsub.serviceAgent'],
reason=(
'Pub/Sub needs this role to create identity tokens. '
'For more details, please see '
'https://cloud.google.com/pubsub/docs/push#authentication'
),
)
def ensure_data_access_logs_are_enabled(trigger_event_filters):
# type: (list[cloudfunctions_v2_messages.EventFilter]) -> None
"""Ensures appropriate Data Access Audit Logs are enabled for the given event filters.
If they're not, the user will be prompted to enable them or warned if the
console cannot prompt.
Args:
trigger_event_filters: the CAL trigger's event filters.
"""
service_filter = [
f for f in trigger_event_filters if f.attribute == 'serviceName'
]
if service_filter:
api_util.PromptToEnableDataAccessAuditLogs(service_filter[0].value)

View File

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""List event types available to Google Cloud Functions v2."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.eventarc import providers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.eventarc.types import EventType
from googlecloudsdk.core import properties
def Run(args, release_track):
"""Lists GCF v2 available event_types.
Args:
args: an argparse namespace. All the arguments that were provided to this
command invocation.
release_track: base.ReleaseTrack, The release track (ga, beta, alpha)
Returns:
event_types: List[EventType], The list of supported event types.
"""
del release_track
client = providers.ProvidersClient(base.ReleaseTrack.GA)
project = args.project or properties.VALUES.core.project.GetOrFail()
provider_list = client.List(
'projects/{}/locations/-'.format(project), limit=None, page_size=None)
event_types = {}
for p in provider_list:
for t in p.eventTypes:
name = t.type
description = '{}: {}'.format(p.displayName, t.description)
attributes = ','.join(fa.attribute for fa in t.filteringAttributes)
if name not in event_types:
event_types[name] = EventType(name, description, attributes)
return [v for k, v in event_types.items()]

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions get-iam-policy` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v2 import util as api_util
from googlecloudsdk.core import log
def Run(args, release_track):
"""Get the IAM policy for a Google Cloud Function."""
client = api_util.GetClientInstance(release_track=release_track)
messages = api_util.GetMessagesModule(release_track=release_track)
function_ref = args.CONCEPTS.name.Parse()
function_relative_name = function_ref.RelativeName()
function = client.projects_locations_functions.Get(
messages.CloudfunctionsProjectsLocationsFunctionsGetRequest(
name=function_ref.RelativeName()
)
)
log.warning(
'To view more details about the invoker policy in the underlying Cloud'
' Run Service, please run:\n\n gcloud run services get-iam-policy {}\n'
.format(function.serviceConfig.service)
)
return client.projects_locations_functions.GetIamPolicy(
messages.CloudfunctionsProjectsLocationsFunctionsGetIamPolicyRequest(
resource=function_relative_name
)
)

View File

@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions list` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.py import list_pager
from googlecloudsdk.api_lib.functions.v2 import util as api_util
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources
def _YieldFromLocations(
locations, project, limit, messages, client, filter_exp
):
"""Yield the functions from the given locations.
Args:
locations: List[str], list of gcp regions.
project: str, Name of the API to modify. E.g. "cloudfunctions"
limit: int, List messages limit.
messages: module, Generated messages module.
client: base_api.BaseApiClient, cloud functions client library.
filter_exp: Filter expression in list functions request.
Yields:
protorpc.message.Message, The resources listed by the service.
"""
def _ReadAttrAndLogUnreachable(message, attribute):
if message.unreachable:
log.warning(
(
'The following regions were fully or partially unreachable '
'for query: %s\n'
'This could be due to permission setup. Additional information'
'can be found in: '
'https://cloud.google.com/functions/docs/troubleshooting'
),
', '.join(message.unreachable),
)
return getattr(message, attribute)
for location in locations:
location_ref = resources.REGISTRY.Parse(
location,
params={'projectsId': project},
collection='cloudfunctions.projects.locations',
)
for function in list_pager.YieldFromList(
service=client.projects_locations_functions,
request=messages.CloudfunctionsProjectsLocationsFunctionsListRequest(
parent=location_ref.RelativeName(), filter=filter_exp
),
limit=limit,
field='functions',
batch_size_attribute='pageSize',
get_field_func=_ReadAttrAndLogUnreachable,
):
yield function
def Run(args, release_track, filter_exp=None):
"""List Google Cloud Functions."""
client = api_util.GetClientInstance(release_track=release_track)
messages = api_util.GetMessagesModule(release_track=release_track)
project = properties.VALUES.core.project.GetOrFail()
limit = args.limit
return _YieldFromLocations(
args.regions, project, limit, messages, client, filter_exp
)

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""List regions available to Google Cloud Functions."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v2 import client
def Run(args, release_track):
"""Lists GCF gen2 regions available with the given args.
Args:
args: argparse.Namespace, All the arguments that were provided to this
command invocation.
release_track: base.ReleaseTrack, The release track (ga, beta, alpha)
Returns:
Iterable[cloudfunctions_v2alpha.Location], Generator of available GCF gen2
regions.
"""
del args # unused by list command
return client.FunctionsClient(release_track).ListRegions()

View File

@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions remove-iam-policy-binding` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v2 import util as api_util
from googlecloudsdk.command_lib.functions import run_util
from googlecloudsdk.command_lib.iam import iam_util
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_io
def Run(args, release_track):
"""Removes a binding from the IAM policy for a Google Cloud Function."""
client = api_util.GetClientInstance(release_track=release_track)
messages = api_util.GetMessagesModule(release_track=release_track)
function_ref = args.CONCEPTS.name.Parse()
function_relative_name = function_ref.RelativeName()
policy = client.projects_locations_functions.GetIamPolicy(
messages.CloudfunctionsProjectsLocationsFunctionsGetIamPolicyRequest(
resource=function_relative_name
)
)
iam_util.RemoveBindingFromIamPolicy(policy, args.member, args.role)
policy = client.projects_locations_functions.SetIamPolicy(
messages.CloudfunctionsProjectsLocationsFunctionsSetIamPolicyRequest(
resource=function_relative_name,
setIamPolicyRequest=messages.SetIamPolicyRequest(policy=policy),
)
)
if args.role in [
'roles/cloudfunctions.admin',
'roles/cloudfunctions.developer',
'roles/cloudfunctions.invoker',
]:
log.warning(
'The binding between member {member} and role {role} has been'
' successfully removed. However, to make sure the member {member}'
" doesn't have the permission to invoke the 2nd gen function, you need"
' to remove the invoker binding in the underlying Cloud Run service.'
' This can be done by running the following command:\n '
' gcloud functions remove-invoker-policy-binding {function_ref}'
' --member={member} \n'.format(
member=args.member, role=args.role, function_ref=function_ref.Name()
)
)
if console_io.CanPrompt() and console_io.PromptContinue(
prompt_string=(
'Would you like to run this command and additionally deny [{}]'
' permission to invoke function [{}]'
).format(args.member, function_ref.Name()),
):
function = client.projects_locations_functions.Get(
messages.CloudfunctionsProjectsLocationsFunctionsGetRequest(
name=function_ref.RelativeName()
)
)
run_util.AddOrRemoveInvokerBinding(
function, args.member, add_binding=False
)
log.status.Print(
'The role [roles/run.invoker] was successfully removed for member '
'{member} in the underlying Cloud Run service. You can view '
'its IAM policy by running:\n'
'gcloud run services get-iam-policy {service}\n'.format(
service=function.serviceConfig.service, member=args.member
)
)
return policy

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions remove-invoker-policy-binding` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v2 import util as api_util
from googlecloudsdk.api_lib.run import global_methods
from googlecloudsdk.command_lib.run import connection_context
from googlecloudsdk.command_lib.run import serverless_operations
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources
CLOUD_RUN_SERVICE_COLLECTION_K8S = 'run.namespaces.services'
CLOUD_RUN_SERVICE_COLLECTION_ONE_PLATFORM = 'run.projects.locations.services'
def Run(args, release_track):
"""Remove an invoker binding from the IAM policy of a Google Cloud Function."""
client = api_util.GetClientInstance(release_track=release_track)
messages = api_util.GetMessagesModule(release_track=release_track)
function_ref = args.CONCEPTS.name.Parse()
function = client.projects_locations_functions.Get(
messages.CloudfunctionsProjectsLocationsFunctionsGetRequest(
name=function_ref.RelativeName()))
service_ref_one_platform = resources.REGISTRY.ParseRelativeName(
function.serviceConfig.service, CLOUD_RUN_SERVICE_COLLECTION_ONE_PLATFORM)
run_connection_context = connection_context.RegionalConnectionContext(
service_ref_one_platform.locationsId, global_methods.SERVERLESS_API_NAME,
global_methods.SERVERLESS_API_VERSION)
with serverless_operations.Connect(run_connection_context) as operations:
service_ref_k8s = resources.REGISTRY.ParseRelativeName(
'namespaces/{}/services/{}'.format(
properties.VALUES.core.project.GetOrFail(),
service_ref_one_platform.Name()), CLOUD_RUN_SERVICE_COLLECTION_K8S)
return operations.AddOrRemoveIamPolicyBinding(
service_ref_k8s,
False, # Remove the binding
member=args.member,
role=serverless_operations.ALLOW_UNAUTH_POLICY_BINDING_ROLE)

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""List runtimes available to Google Cloud Functions."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
from googlecloudsdk.api_lib.functions.v2 import client
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
def Run(args, release_track):
"""Lists GCF runtimes available with the given args from the v2 API.
Args:
args: an argparse namespace. All the arguments that were provided to this
command invocation.
release_track: base.ReleaseTrack, The release track (ga, beta, alpha)
Returns:
List[Runtime], List of available GCF runtimes
"""
del args
if not properties.VALUES.functions.region.IsExplicitlySet():
log.status.Print('Suggest using `--region us-west1`')
region = properties.VALUES.functions.region.Get()
gcf_client = client.FunctionsClient(release_track=release_track)
# ListRuntimesResponse
response = gcf_client.ListRuntimes(region)
if response:
runtime_mapping = collections.OrderedDict()
for runtime in response.runtimes:
runtime_mapping.setdefault(runtime.name, []).append(runtime)
return [Runtime(value) for value in runtime_mapping.values()]
else:
return []
class Runtime:
"""Runtimes wrapper for ListRuntimesResponse#Runtimes.
The runtimes response from GCFv2 duplicates runtimes for each environment. To
make formatting easier, this includes all environments under a single object.
Attributes:
name: A string name of the runtime.
stage: An enum of the release state of the runtime, e.g., GA, BETA, etc.
environments: A list of supported runtimes, [GEN_1, GEN_2]
"""
def __init__(self, runtimes):
for runtime in runtimes:
if runtime.name != runtimes[0].name:
raise ValueError('Only runtimes with the same name should be included')
self.name = runtimes[0].name if runtimes else ''
self.stage = runtimes[0].stage if runtimes else ''
self.environments = [runtime.environment for runtime in runtimes]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""This file provides the implementation of the `functions set-iam-policy` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.functions.v2 import util as api_util
from googlecloudsdk.command_lib.iam import iam_util
def Run(args, release_track):
"""Set the IAM policy for a Google Cloud Function."""
client = api_util.GetClientInstance(release_track=release_track)
messages = api_util.GetMessagesModule(release_track=release_track)
function_ref = args.CONCEPTS.name.Parse()
function_relative_name = function_ref.RelativeName()
policy, update_mask = iam_util.ParseYamlOrJsonPolicyFile(
args.policy_file, messages.Policy)
return client.projects_locations_functions.SetIamPolicy(
messages.CloudfunctionsProjectsLocationsFunctionsSetIamPolicyRequest(
resource=function_relative_name,
setIamPolicyRequest=messages.SetIamPolicyRequest(
policy=policy, updateMask=update_mask)))