diff --git a/google/cloud/functions/__init__.py b/google/cloud/functions/__init__.py index beaf702..8118b84 100644 --- a/google/cloud/functions/__init__.py +++ b/google/cloud/functions/__init__.py @@ -36,6 +36,8 @@ from google.cloud.functions_v1.types.functions import HttpsTrigger from google.cloud.functions_v1.types.functions import ListFunctionsRequest from google.cloud.functions_v1.types.functions import ListFunctionsResponse +from google.cloud.functions_v1.types.functions import SecretEnvVar +from google.cloud.functions_v1.types.functions import SecretVolume from google.cloud.functions_v1.types.functions import SourceRepository from google.cloud.functions_v1.types.functions import UpdateFunctionRequest from google.cloud.functions_v1.types.functions import CloudFunctionStatus @@ -60,6 +62,8 @@ "HttpsTrigger", "ListFunctionsRequest", "ListFunctionsResponse", + "SecretEnvVar", + "SecretVolume", "SourceRepository", "UpdateFunctionRequest", "CloudFunctionStatus", diff --git a/google/cloud/functions_v1/__init__.py b/google/cloud/functions_v1/__init__.py index dfa8c79..378daca 100644 --- a/google/cloud/functions_v1/__init__.py +++ b/google/cloud/functions_v1/__init__.py @@ -32,6 +32,8 @@ from .types.functions import HttpsTrigger from .types.functions import ListFunctionsRequest from .types.functions import ListFunctionsResponse +from .types.functions import SecretEnvVar +from .types.functions import SecretVolume from .types.functions import SourceRepository from .types.functions import UpdateFunctionRequest from .types.functions import CloudFunctionStatus @@ -59,6 +61,8 @@ "ListFunctionsResponse", "OperationMetadataV1", "OperationType", + "SecretEnvVar", + "SecretVolume", "SourceRepository", "UpdateFunctionRequest", ) diff --git a/google/cloud/functions_v1/services/cloud_functions_service/async_client.py b/google/cloud/functions_v1/services/cloud_functions_service/async_client.py index ca6f0a0..e7da3fb 100644 --- a/google/cloud/functions_v1/services/cloud_functions_service/async_client.py +++ b/google/cloud/functions_v1/services/cloud_functions_service/async_client.py @@ -57,6 +57,14 @@ class CloudFunctionsServiceAsyncClient: parse_cloud_function_path = staticmethod( CloudFunctionsServiceClient.parse_cloud_function_path ) + crypto_key_path = staticmethod(CloudFunctionsServiceClient.crypto_key_path) + parse_crypto_key_path = staticmethod( + CloudFunctionsServiceClient.parse_crypto_key_path + ) + repository_path = staticmethod(CloudFunctionsServiceClient.repository_path) + parse_repository_path = staticmethod( + CloudFunctionsServiceClient.parse_repository_path + ) common_billing_account_path = staticmethod( CloudFunctionsServiceClient.common_billing_account_path ) @@ -281,6 +289,7 @@ async def get_function( contains user computation executed in response to an event. It encapsulate function and triggers configurations. + Next tag: 36 """ # Create or coerce a protobuf request object. @@ -373,7 +382,7 @@ async def create_function( The result type for the operation will be :class:`google.cloud.functions_v1.types.CloudFunction` Describes a Cloud Function that contains user computation executed in response to an event. It encapsulate function and - triggers configurations. + triggers configurations. Next tag: 36 """ # Create or coerce a protobuf request object. @@ -457,7 +466,7 @@ async def update_function( The result type for the operation will be :class:`google.cloud.functions_v1.types.CloudFunction` Describes a Cloud Function that contains user computation executed in response to an event. It encapsulate function and - triggers configurations. + triggers configurations. Next tag: 36 """ # Create or coerce a protobuf request object. diff --git a/google/cloud/functions_v1/services/cloud_functions_service/client.py b/google/cloud/functions_v1/services/cloud_functions_service/client.py index c47deb7..ca6114b 100644 --- a/google/cloud/functions_v1/services/cloud_functions_service/client.py +++ b/google/cloud/functions_v1/services/cloud_functions_service/client.py @@ -185,6 +185,43 @@ def parse_cloud_function_path(path: str) -> Dict[str, str]: ) return m.groupdict() if m else {} + @staticmethod + def crypto_key_path( + project: str, location: str, key_ring: str, crypto_key: str, + ) -> str: + """Returns a fully-qualified crypto_key string.""" + return "projects/{project}/locations/{location}/keyRings/{key_ring}/cryptoKeys/{crypto_key}".format( + project=project, + location=location, + key_ring=key_ring, + crypto_key=crypto_key, + ) + + @staticmethod + def parse_crypto_key_path(path: str) -> Dict[str, str]: + """Parses a crypto_key path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/keyRings/(?P.+?)/cryptoKeys/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + + @staticmethod + def repository_path(project: str, location: str, repository: str,) -> str: + """Returns a fully-qualified repository string.""" + return "projects/{project}/locations/{location}/repositories/{repository}".format( + project=project, location=location, repository=repository, + ) + + @staticmethod + def parse_repository_path(path: str) -> Dict[str, str]: + """Parses a repository path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/repositories/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + @staticmethod def common_billing_account_path(billing_account: str,) -> str: """Returns a fully-qualified billing_account string.""" @@ -455,6 +492,7 @@ def get_function( contains user computation executed in response to an event. It encapsulate function and triggers configurations. + Next tag: 36 """ # Create or coerce a protobuf request object. @@ -537,7 +575,7 @@ def create_function( The result type for the operation will be :class:`google.cloud.functions_v1.types.CloudFunction` Describes a Cloud Function that contains user computation executed in response to an event. It encapsulate function and - triggers configurations. + triggers configurations. Next tag: 36 """ # Create or coerce a protobuf request object. @@ -621,7 +659,7 @@ def update_function( The result type for the operation will be :class:`google.cloud.functions_v1.types.CloudFunction` Describes a Cloud Function that contains user computation executed in response to an event. It encapsulate function and - triggers configurations. + triggers configurations. Next tag: 36 """ # Create or coerce a protobuf request object. diff --git a/google/cloud/functions_v1/types/__init__.py b/google/cloud/functions_v1/types/__init__.py index 057540b..0d3ab6a 100644 --- a/google/cloud/functions_v1/types/__init__.py +++ b/google/cloud/functions_v1/types/__init__.py @@ -29,6 +29,8 @@ HttpsTrigger, ListFunctionsRequest, ListFunctionsResponse, + SecretEnvVar, + SecretVolume, SourceRepository, UpdateFunctionRequest, CloudFunctionStatus, @@ -54,6 +56,8 @@ "HttpsTrigger", "ListFunctionsRequest", "ListFunctionsResponse", + "SecretEnvVar", + "SecretVolume", "SourceRepository", "UpdateFunctionRequest", "CloudFunctionStatus", diff --git a/google/cloud/functions_v1/types/functions.py b/google/cloud/functions_v1/types/functions.py index e4a5441..40e18d2 100644 --- a/google/cloud/functions_v1/types/functions.py +++ b/google/cloud/functions_v1/types/functions.py @@ -29,6 +29,8 @@ "HttpsTrigger", "EventTrigger", "FailurePolicy", + "SecretEnvVar", + "SecretVolume", "CreateFunctionRequest", "UpdateFunctionRequest", "GetFunctionRequest", @@ -58,7 +60,7 @@ class CloudFunctionStatus(proto.Enum): class CloudFunction(proto.Message): r"""Describes a Cloud Function that contains user computation executed in response to an event. It encapsulate function and - triggers configurations. + triggers configurations. Next tag: 36 This message has `oneof`_ fields (mutually exclusive fields). For each oneof, at most one member field can be set at the same time. @@ -140,6 +142,9 @@ class CloudFunction(proto.Message): environment_variables (Sequence[google.cloud.functions_v1.types.CloudFunction.EnvironmentVariablesEntry]): Environment variables that shall be available during function execution. + build_environment_variables (Sequence[google.cloud.functions_v1.types.CloudFunction.BuildEnvironmentVariablesEntry]): + Build environment variables that shall be + available during build time. network (str): The VPC Network that this cloud function can connect to. It can be either the fully-qualified URI, or the short name of @@ -159,7 +164,21 @@ class CloudFunction(proto.Message): documentation `__ for more information on connecting Cloud projects. max_instances (int): - The limit on the maximum number of function + The limit on the maximum number of function instances that + may coexist at a given time. + + In some cases, such as rapid traffic surges, Cloud Functions + may, for a short period of time, create more instances than + the specified max instances limit. If your function cannot + tolerate this temporary behavior, you may want to factor in + a safety margin and set a lower max instances value than + your function can tolerate. + + See the `Max + Instances `__ + Guide for more details. + min_instances (int): + A lower bound for the number function instances that may coexist at a given time. vpc_connector (str): The VPC Network Connector that this cloud function can @@ -179,9 +198,80 @@ class CloudFunction(proto.Message): ingress_settings (google.cloud.functions_v1.types.CloudFunction.IngressSettings): The ingress settings for the function, controlling what traffic can reach it. + kms_key_name (str): + Resource name of a KMS crypto key (managed by the user) used + to encrypt/decrypt function resources. + + It must match the pattern + ``projects/{project}/locations/{location}/keyRings/{key_ring}/cryptoKeys/{crypto_key}``. + + If specified, you must also provide an artifact registry + repository using the ``docker_repository`` field that was + created with the same KMS crypto key. + + The following service accounts need to be granted Cloud KMS + crypto key encrypter/decrypter roles on the key. + + 1. Google Cloud Functions service account + (service-{project_number}@gcf-admin-robot.iam.gserviceaccount.com) + - Required to protect the function's image. + 2. Google Storage service account + (service-{project_number}@gs-project-accounts.iam.gserviceaccount.com) + - Required to protect the function's source code. If this + service account does not exist, deploying a function + without a KMS key or retrieving the service agent name + provisions it. For more information, see + https://cloud.google.com/storage/docs/projects#service-agents + and + https://cloud.google.com/storage/docs/getting-service-agent#gsutil. + + Google Cloud Functions delegates access to service agents to + protect function resources in internal projects that are not + accessible by the end user. + build_worker_pool (str): + Name of the Cloud Build Custom Worker Pool that should be + used to build the function. The format of this field is + ``projects/{project}/locations/{region}/workerPools/{workerPool}`` + where ``{project}`` and ``{region}`` are the project id and + region respectively where the worker pool is defined and + ``{workerPool}`` is the short name of the worker pool. + + If the project id is not the same as the function, then the + Cloud Functions Service Agent + (``service-@gcf-admin-robot.iam.gserviceaccount.com``) + must be granted the role Cloud Build Custom Workers Builder + (``roles/cloudbuild.customworkers.builder``) in the project. build_id (str): Output only. The Cloud Build ID of the latest successful deployment of the function. + build_name (str): + Output only. The Cloud Build Name of the function + deployment. + ``projects//locations//builds/``. + secret_environment_variables (Sequence[google.cloud.functions_v1.types.SecretEnvVar]): + Secret environment variables configuration. + secret_volumes (Sequence[google.cloud.functions_v1.types.SecretVolume]): + Secret volumes configuration. + source_token (str): + Input only. An identifier for Firebase + function sources. Disclaimer: This field is only + supported for Firebase function deployments. + docker_repository (str): + User managed repository created in Artifact Registry + optionally with a customer managed encryption key. If + specified, deployments will use Artifact Registry. If + unspecified and the deployment is eligible to use Artifact + Registry, GCF will create and use a repository named + 'gcf-artifacts' for every deployed region. This is the + repository to which the function docker image will be pushed + after it is built by Cloud Build. + + It must match the pattern + ``projects/{project}/locations/{location}/repositories/{repository}``. + + Cross-project repositories are not supported. Cross-location + repositories are not supported. Repository format must be + 'DOCKER'. """ class VpcConnectorEgressSettings(proto.Enum): @@ -231,14 +321,27 @@ class IngressSettings(proto.Enum): version_id = proto.Field(proto.INT64, number=14,) labels = proto.MapField(proto.STRING, proto.STRING, number=15,) environment_variables = proto.MapField(proto.STRING, proto.STRING, number=17,) + build_environment_variables = proto.MapField(proto.STRING, proto.STRING, number=28,) network = proto.Field(proto.STRING, number=18,) max_instances = proto.Field(proto.INT32, number=20,) + min_instances = proto.Field(proto.INT32, number=32,) vpc_connector = proto.Field(proto.STRING, number=22,) vpc_connector_egress_settings = proto.Field( proto.ENUM, number=23, enum=VpcConnectorEgressSettings, ) ingress_settings = proto.Field(proto.ENUM, number=24, enum=IngressSettings,) + kms_key_name = proto.Field(proto.STRING, number=25,) + build_worker_pool = proto.Field(proto.STRING, number=26,) build_id = proto.Field(proto.STRING, number=27,) + build_name = proto.Field(proto.STRING, number=33,) + secret_environment_variables = proto.RepeatedField( + proto.MESSAGE, number=29, message="SecretEnvVar", + ) + secret_volumes = proto.RepeatedField( + proto.MESSAGE, number=30, message="SecretVolume", + ) + source_token = proto.Field(proto.STRING, number=31,) + docker_repository = proto.Field(proto.STRING, number=34,) class SourceRepository(proto.Message): @@ -385,6 +488,101 @@ class Retry(proto.Message): retry = proto.Field(proto.MESSAGE, number=1, oneof="action", message=Retry,) +class SecretEnvVar(proto.Message): + r"""Configuration for a secret environment variable. It has the + information necessary to fetch the secret value from secret + manager and expose it as an environment variable. Secret value + is not a part of the configuration. Secret values are only + fetched when a new clone starts. + + Attributes: + key (str): + Name of the environment variable. + project_id (str): + Project identifier (preferrably project + number but can also be the project ID) of the + project that contains the secret. If not set, it + will be populated with the function's project + assuming that the secret exists in the same + project as of the function. + secret (str): + Name of the secret in secret manager (not the + full resource name). + version (str): + Version of the secret (version number or the + string 'latest'). It is recommended to use a + numeric version for secret environment variables + as any updates to the secret value is not + reflected until new clones start. + """ + + key = proto.Field(proto.STRING, number=1,) + project_id = proto.Field(proto.STRING, number=2,) + secret = proto.Field(proto.STRING, number=3,) + version = proto.Field(proto.STRING, number=4,) + + +class SecretVolume(proto.Message): + r"""Configuration for a secret volume. It has the information + necessary to fetch the secret value from secret manager and make + it available as files mounted at the requested paths within the + application container. Secret value is not a part of the + configuration. Every filesystem read operation performs a lookup + in secret manager to retrieve the secret value. + + Attributes: + mount_path (str): + The path within the container to mount the secret volume. + For example, setting the mount_path as ``/etc/secrets`` + would mount the secret value files under the + ``/etc/secrets`` directory. This directory will also be + completely shadowed and unavailable to mount any other + secrets. + + Recommended mount paths: /etc/secrets Restricted mount + paths: /cloudsql, /dev/log, /pod, /proc, /var/log + project_id (str): + Project identifier (preferrably project + number but can also be the project ID) of the + project that contains the secret. If not set, it + will be populated with the function's project + assuming that the secret exists in the same + project as of the function. + secret (str): + Name of the secret in secret manager (not the + full resource name). + versions (Sequence[google.cloud.functions_v1.types.SecretVolume.SecretVersion]): + List of secret versions to mount for this secret. If empty, + the ``latest`` version of the secret will be made available + in a file named after the secret under the mount point. + """ + + class SecretVersion(proto.Message): + r"""Configuration for a single version. + + Attributes: + version (str): + Version of the secret (version number or the string + 'latest'). It is preferrable to use ``latest`` version with + secret volumes as secret value changes are reflected + immediately. + path (str): + Relative path of the file under the mount path where the + secret value for this version will be fetched and made + available. For example, setting the mount_path as + '/etc/secrets' and path as ``/secret_foo`` would mount the + secret value file at ``/etc/secrets/secret_foo``. + """ + + version = proto.Field(proto.STRING, number=1,) + path = proto.Field(proto.STRING, number=2,) + + mount_path = proto.Field(proto.STRING, number=1,) + project_id = proto.Field(proto.STRING, number=2,) + secret = proto.Field(proto.STRING, number=3,) + versions = proto.RepeatedField(proto.MESSAGE, number=4, message=SecretVersion,) + + class CreateFunctionRequest(proto.Message): r"""Request for the ``CreateFunction`` method. diff --git a/google/cloud/functions_v1/types/operations.py b/google/cloud/functions_v1/types/operations.py index e58f39b..da5a636 100644 --- a/google/cloud/functions_v1/types/operations.py +++ b/google/cloud/functions_v1/types/operations.py @@ -55,6 +55,14 @@ class OperationMetadataV1(proto.Message): The Cloud Build ID of the function created or updated by an API call. This field is only populated for Create and Update operations. + source_token (str): + An identifier for Firebase function sources. + Disclaimer: This field is only supported for + Firebase function deployments. + build_name (str): + The Cloud Build Name of the function deployment. This field + is only populated for Create and Update operations. + ``projects//locations//builds/``. """ target = proto.Field(proto.STRING, number=1,) @@ -63,6 +71,8 @@ class OperationMetadataV1(proto.Message): version_id = proto.Field(proto.INT64, number=4,) update_time = proto.Field(proto.MESSAGE, number=5, message=timestamp_pb2.Timestamp,) build_id = proto.Field(proto.STRING, number=6,) + source_token = proto.Field(proto.STRING, number=7,) + build_name = proto.Field(proto.STRING, number=8,) __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/tests/unit/gapic/functions_v1/test_cloud_functions_service.py b/tests/unit/gapic/functions_v1/test_cloud_functions_service.py index 100d9a9..a1c50d4 100644 --- a/tests/unit/gapic/functions_v1/test_cloud_functions_service.py +++ b/tests/unit/gapic/functions_v1/test_cloud_functions_service.py @@ -830,10 +830,16 @@ def test_get_function( version_id=1074, network="network_value", max_instances=1389, + min_instances=1387, vpc_connector="vpc_connector_value", vpc_connector_egress_settings=functions.CloudFunction.VpcConnectorEgressSettings.PRIVATE_RANGES_ONLY, ingress_settings=functions.CloudFunction.IngressSettings.ALLOW_ALL, + kms_key_name="kms_key_name_value", + build_worker_pool="build_worker_pool_value", build_id="build_id_value", + build_name="build_name_value", + source_token="source_token_value", + docker_repository="docker_repository_value", source_archive_url="source_archive_url_value", https_trigger=functions.HttpsTrigger(url="url_value"), ) @@ -856,6 +862,7 @@ def test_get_function( assert response.version_id == 1074 assert response.network == "network_value" assert response.max_instances == 1389 + assert response.min_instances == 1387 assert response.vpc_connector == "vpc_connector_value" assert ( response.vpc_connector_egress_settings @@ -864,7 +871,12 @@ def test_get_function( assert ( response.ingress_settings == functions.CloudFunction.IngressSettings.ALLOW_ALL ) + assert response.kms_key_name == "kms_key_name_value" + assert response.build_worker_pool == "build_worker_pool_value" assert response.build_id == "build_id_value" + assert response.build_name == "build_name_value" + assert response.source_token == "source_token_value" + assert response.docker_repository == "docker_repository_value" def test_get_function_from_dict(): @@ -913,10 +925,16 @@ async def test_get_function_async( version_id=1074, network="network_value", max_instances=1389, + min_instances=1387, vpc_connector="vpc_connector_value", vpc_connector_egress_settings=functions.CloudFunction.VpcConnectorEgressSettings.PRIVATE_RANGES_ONLY, ingress_settings=functions.CloudFunction.IngressSettings.ALLOW_ALL, + kms_key_name="kms_key_name_value", + build_worker_pool="build_worker_pool_value", build_id="build_id_value", + build_name="build_name_value", + source_token="source_token_value", + docker_repository="docker_repository_value", ) ) response = await client.get_function(request) @@ -938,6 +956,7 @@ async def test_get_function_async( assert response.version_id == 1074 assert response.network == "network_value" assert response.max_instances == 1389 + assert response.min_instances == 1387 assert response.vpc_connector == "vpc_connector_value" assert ( response.vpc_connector_egress_settings @@ -946,7 +965,12 @@ async def test_get_function_async( assert ( response.ingress_settings == functions.CloudFunction.IngressSettings.ALLOW_ALL ) + assert response.kms_key_name == "kms_key_name_value" + assert response.build_worker_pool == "build_worker_pool_value" assert response.build_id == "build_id_value" + assert response.build_name == "build_name_value" + assert response.source_token == "source_token_value" + assert response.docker_repository == "docker_repository_value" @pytest.mark.asyncio @@ -3182,8 +3206,60 @@ def test_parse_cloud_function_path(): assert expected == actual +def test_crypto_key_path(): + project = "cuttlefish" + location = "mussel" + key_ring = "winkle" + crypto_key = "nautilus" + expected = "projects/{project}/locations/{location}/keyRings/{key_ring}/cryptoKeys/{crypto_key}".format( + project=project, location=location, key_ring=key_ring, crypto_key=crypto_key, + ) + actual = CloudFunctionsServiceClient.crypto_key_path( + project, location, key_ring, crypto_key + ) + assert expected == actual + + +def test_parse_crypto_key_path(): + expected = { + "project": "scallop", + "location": "abalone", + "key_ring": "squid", + "crypto_key": "clam", + } + path = CloudFunctionsServiceClient.crypto_key_path(**expected) + + # Check that the path construction is reversible. + actual = CloudFunctionsServiceClient.parse_crypto_key_path(path) + assert expected == actual + + +def test_repository_path(): + project = "whelk" + location = "octopus" + repository = "oyster" + expected = "projects/{project}/locations/{location}/repositories/{repository}".format( + project=project, location=location, repository=repository, + ) + actual = CloudFunctionsServiceClient.repository_path(project, location, repository) + assert expected == actual + + +def test_parse_repository_path(): + expected = { + "project": "nudibranch", + "location": "cuttlefish", + "repository": "mussel", + } + path = CloudFunctionsServiceClient.repository_path(**expected) + + # Check that the path construction is reversible. + actual = CloudFunctionsServiceClient.parse_repository_path(path) + assert expected == actual + + def test_common_billing_account_path(): - billing_account = "cuttlefish" + billing_account = "winkle" expected = "billingAccounts/{billing_account}".format( billing_account=billing_account, ) @@ -3193,7 +3269,7 @@ def test_common_billing_account_path(): def test_parse_common_billing_account_path(): expected = { - "billing_account": "mussel", + "billing_account": "nautilus", } path = CloudFunctionsServiceClient.common_billing_account_path(**expected) @@ -3203,7 +3279,7 @@ def test_parse_common_billing_account_path(): def test_common_folder_path(): - folder = "winkle" + folder = "scallop" expected = "folders/{folder}".format(folder=folder,) actual = CloudFunctionsServiceClient.common_folder_path(folder) assert expected == actual @@ -3211,7 +3287,7 @@ def test_common_folder_path(): def test_parse_common_folder_path(): expected = { - "folder": "nautilus", + "folder": "abalone", } path = CloudFunctionsServiceClient.common_folder_path(**expected) @@ -3221,7 +3297,7 @@ def test_parse_common_folder_path(): def test_common_organization_path(): - organization = "scallop" + organization = "squid" expected = "organizations/{organization}".format(organization=organization,) actual = CloudFunctionsServiceClient.common_organization_path(organization) assert expected == actual @@ -3229,7 +3305,7 @@ def test_common_organization_path(): def test_parse_common_organization_path(): expected = { - "organization": "abalone", + "organization": "clam", } path = CloudFunctionsServiceClient.common_organization_path(**expected) @@ -3239,7 +3315,7 @@ def test_parse_common_organization_path(): def test_common_project_path(): - project = "squid" + project = "whelk" expected = "projects/{project}".format(project=project,) actual = CloudFunctionsServiceClient.common_project_path(project) assert expected == actual @@ -3247,7 +3323,7 @@ def test_common_project_path(): def test_parse_common_project_path(): expected = { - "project": "clam", + "project": "octopus", } path = CloudFunctionsServiceClient.common_project_path(**expected) @@ -3257,8 +3333,8 @@ def test_parse_common_project_path(): def test_common_location_path(): - project = "whelk" - location = "octopus" + project = "oyster" + location = "nudibranch" expected = "projects/{project}/locations/{location}".format( project=project, location=location, ) @@ -3268,8 +3344,8 @@ def test_common_location_path(): def test_parse_common_location_path(): expected = { - "project": "oyster", - "location": "nudibranch", + "project": "cuttlefish", + "location": "mussel", } path = CloudFunctionsServiceClient.common_location_path(**expected)