diff --git a/packages/google-cloud-billing/.flake8 b/packages/google-cloud-billing/.flake8 index 9f96f02709c1..49e826987d8b 100644 --- a/packages/google-cloud-billing/.flake8 +++ b/packages/google-cloud-billing/.flake8 @@ -1,6 +1,22 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2020 Google LLC +# +# 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 +# +# https://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. + # Generated by synthtool. DO NOT EDIT! [flake8] -ignore = E203, E266, E501, W503, F401, F841, E712 +ignore = E203, E266, E501, W503, F401, F841 exclude = # Exclude generated code. **/proto/** diff --git a/packages/google-cloud-billing/.github/ISSUE_TEMPLATE/bug_report.md b/packages/google-cloud-billing/.github/ISSUE_TEMPLATE/bug_report.md index aa53d7119b46..860f0b25a04f 100644 --- a/packages/google-cloud-billing/.github/ISSUE_TEMPLATE/bug_report.md +++ b/packages/google-cloud-billing/.github/ISSUE_TEMPLATE/bug_report.md @@ -11,8 +11,7 @@ Thanks for stopping by to let us know something could be better! Please run down the following list and make sure you've tried the usual "quick fixes": - Search the issues already opened: https://github.com/googleapis/python-billing/issues - - Search the issues on our "catch-all" repository: https://github.com/googleapis/google-cloud-python - - Search StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform+python + - Search StackOverflow: https://stackoverflow.com/questions/tagged/google-cloud-platform+python If you are still having issues, please be sure to include as much information as possible: diff --git a/packages/google-cloud-billing/.kokoro/publish-docs.sh b/packages/google-cloud-billing/.kokoro/publish-docs.sh index 2637470906ee..9801da8fe968 100755 --- a/packages/google-cloud-billing/.kokoro/publish-docs.sh +++ b/packages/google-cloud-billing/.kokoro/publish-docs.sh @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -#!/bin/bash - set -eo pipefail # Disable buffering, so that the logs stream through. diff --git a/packages/google-cloud-billing/.kokoro/release.sh b/packages/google-cloud-billing/.kokoro/release.sh index 03bcc7fb0236..2f832a0d5c54 100755 --- a/packages/google-cloud-billing/.kokoro/release.sh +++ b/packages/google-cloud-billing/.kokoro/release.sh @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -#!/bin/bash - set -eo pipefail # Start the releasetool reporter diff --git a/packages/google-cloud-billing/CONTRIBUTING.rst b/packages/google-cloud-billing/CONTRIBUTING.rst index 17e64b23dd4f..e6c10ae0b259 100644 --- a/packages/google-cloud-billing/CONTRIBUTING.rst +++ b/packages/google-cloud-billing/CONTRIBUTING.rst @@ -22,7 +22,7 @@ In order to add a feature: documentation. - The feature must work fully on the following CPython versions: 2.7, - 3.5, 3.6, and 3.7 on both UNIX and Windows. + 3.5, 3.6, 3.7 and 3.8 on both UNIX and Windows. - The feature must not add unnecessary dependencies (where "unnecessary" is of course subjective, but new dependencies should @@ -214,26 +214,18 @@ We support: - `Python 3.5`_ - `Python 3.6`_ - `Python 3.7`_ +- `Python 3.8`_ .. _Python 3.5: https://docs.python.org/3.5/ .. _Python 3.6: https://docs.python.org/3.6/ .. _Python 3.7: https://docs.python.org/3.7/ +.. _Python 3.8: https://docs.python.org/3.8/ Supported versions can be found in our ``noxfile.py`` `config`_. .. _config: https://github.com/googleapis/python-billing/blob/master/noxfile.py -We explicitly decided not to support `Python 2.5`_ due to `decreased usage`_ -and lack of continuous integration `support`_. - -.. _Python 2.5: https://docs.python.org/2.5/ -.. _decreased usage: https://caremad.io/2013/10/a-look-at-pypi-downloads/ -.. _support: https://blog.travis-ci.com/2013-11-18-upcoming-build-environment-updates/ - -We have `dropped 2.6`_ as a supported version as well since Python 2.6 is no -longer supported by the core development team. - Python 2.7 support is deprecated. All code changes should maintain Python 2.7 compatibility until January 1, 2020. We also explicitly decided to support Python 3 beginning with version @@ -247,7 +239,6 @@ We also explicitly decided to support Python 3 beginning with version .. _prominent: https://docs.djangoproject.com/en/1.9/faq/install/#what-python-version-can-i-use-with-django .. _projects: http://flask.pocoo.org/docs/0.10/python3/ .. _Unicode literal support: https://www.python.org/dev/peps/pep-0414/ -.. _dropped 2.6: https://github.com/googleapis/google-cloud-python/issues/995 ********** Versioning diff --git a/packages/google-cloud-billing/MANIFEST.in b/packages/google-cloud-billing/MANIFEST.in index cd011be27a0e..68855abc3f02 100644 --- a/packages/google-cloud-billing/MANIFEST.in +++ b/packages/google-cloud-billing/MANIFEST.in @@ -1,3 +1,19 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2020 Google LLC +# +# 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 +# +# https://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. + # Generated by synthtool. DO NOT EDIT! include README.rst LICENSE recursive-include google *.json *.proto diff --git a/packages/google-cloud-billing/docs/index.rst b/packages/google-cloud-billing/docs/index.rst index f99706abe060..eddee5fe7983 100644 --- a/packages/google-cloud-billing/docs/index.rst +++ b/packages/google-cloud-billing/docs/index.rst @@ -1,5 +1,7 @@ .. include:: README.rst +.. include:: multiprocessing.rst + API Reference ------------- diff --git a/packages/google-cloud-billing/docs/multiprocessing.rst b/packages/google-cloud-billing/docs/multiprocessing.rst new file mode 100644 index 000000000000..1cb29d4ca967 --- /dev/null +++ b/packages/google-cloud-billing/docs/multiprocessing.rst @@ -0,0 +1,7 @@ +.. note:: + + Because this client uses :mod:`grpcio` library, it is safe to + share instances across threads. In multiprocessing scenarios, the best + practice is to create client instances *after* the invocation of + :func:`os.fork` by :class:`multiprocessing.Pool` or + :class:`multiprocessing.Process`. diff --git a/packages/google-cloud-billing/google/cloud/billing/__init__.py b/packages/google-cloud-billing/google/cloud/billing/__init__.py index c97dac9a7e76..73508b8e0409 100644 --- a/packages/google-cloud-billing/google/cloud/billing/__init__.py +++ b/packages/google-cloud-billing/google/cloud/billing/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/__init__.py b/packages/google-cloud-billing/google/cloud/billing_v1/__init__.py index b091b20c51a0..02776c62f8fa 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/__init__.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/__init__.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/__init__.py index 2c56c5372223..42ffdf2bc43d 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/__init__.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/__init__.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/__init__.py index fb7e4ed0b73c..3ff9f58cd2ad 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/__init__.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/client.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/client.py index 72e59ac35df3..cba969dd66f6 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/client.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/client.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ # from collections import OrderedDict -from typing import Dict, Iterable, Iterator, Sequence, Tuple, Type, Union +import re +from typing import Callable, Dict, Sequence, Tuple, Type, Union import pkg_resources import google.api_core.client_options as ClientOptions # type: ignore @@ -70,8 +71,38 @@ class CloudBillingClient(metaclass=CloudBillingClientMeta): with projects. """ - DEFAULT_OPTIONS = ClientOptions.ClientOptions( - api_endpoint="cloudbilling.googleapis.com" + @staticmethod + def _get_default_mtls_endpoint(api_endpoint): + """Convert api endpoint to mTLS endpoint. + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + str: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + DEFAULT_ENDPOINT = "cloudbilling.googleapis.com" + DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_ENDPOINT ) @classmethod @@ -99,7 +130,7 @@ def __init__( *, credentials: credentials.Credentials = None, transport: Union[str, CloudBillingTransport] = None, - client_options: ClientOptions = DEFAULT_OPTIONS, + client_options: ClientOptions = None, ) -> None: """Instantiate the cloud billing client. @@ -113,6 +144,17 @@ def __init__( transport to use. If set to None, a transport is chosen automatically. client_options (ClientOptions): Custom options for the client. + (1) The ``api_endpoint`` property can be used to override the + default endpoint provided by the client. + (2) If ``transport`` argument is None, ``client_options`` can be + used to create a mutual TLS transport. If ``client_cert_source`` + is provided, mutual TLS transport will be created with the given + ``api_endpoint`` or the default mTLS endpoint, and the client + SSL credentials obtained from ``client_cert_source``. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. """ if isinstance(client_options, dict): client_options = ClientOptions.from_dict(client_options) @@ -121,17 +163,46 @@ def __init__( # Ordinarily, we provide the transport, but allowing a custom transport # instance provides an extensibility point for unusual situations. if isinstance(transport, CloudBillingTransport): + # transport is a CloudBillingTransport instance. if credentials: raise ValueError( "When providing a transport instance, " "provide its credentials directly." ) self._transport = transport - else: + elif client_options is None or ( + client_options.api_endpoint is None + and client_options.client_cert_source is None + ): + # Don't trigger mTLS if we get an empty ClientOptions. Transport = type(self).get_transport_class(transport) self._transport = Transport( + credentials=credentials, host=self.DEFAULT_ENDPOINT + ) + else: + # We have a non-empty ClientOptions. If client_cert_source is + # provided, trigger mTLS with user provided endpoint or the default + # mTLS endpoint. + if client_options.client_cert_source: + api_mtls_endpoint = ( + client_options.api_endpoint + if client_options.api_endpoint + else self.DEFAULT_MTLS_ENDPOINT + ) + else: + api_mtls_endpoint = None + + api_endpoint = ( + client_options.api_endpoint + if client_options.api_endpoint + else self.DEFAULT_ENDPOINT + ) + + self._transport = CloudBillingGrpcTransport( credentials=credentials, - host=client_options.api_endpoint or "cloudbilling.googleapis.com", + host=api_endpoint, + api_mtls_endpoint=api_mtls_endpoint, + client_cert_source=client_options.client_cert_source, ) def get_billing_account( @@ -185,6 +256,7 @@ def get_billing_account( # If we have keyword arguments corresponding to fields on the # request, apply these. + if name is not None: request.name = name @@ -243,9 +315,6 @@ def list_billing_accounts( request = cloud_billing.ListBillingAccountsRequest(request) - # If we have keyword arguments corresponding to fields on the - # request, apply these. - # Wrap the RPC method; this adds retry and timeout information, # and friendly error handling. rpc = gapic_v1.method.wrap_method( @@ -327,6 +396,7 @@ def update_billing_account( # If we have keyword arguments corresponding to fields on the # request, apply these. + if name is not None: request.name = name if account is not None: @@ -407,6 +477,7 @@ def create_billing_account( # If we have keyword arguments corresponding to fields on the # request, apply these. + if billing_account is not None: request.billing_account = billing_account @@ -478,6 +549,7 @@ def list_project_billing_info( # If we have keyword arguments corresponding to fields on the # request, apply these. + if name is not None: request.name = name @@ -560,6 +632,7 @@ def get_project_billing_info( # If we have keyword arguments corresponding to fields on the # request, apply these. + if name is not None: request.name = name @@ -677,6 +750,7 @@ def update_project_billing_info( # If we have keyword arguments corresponding to fields on the # request, apply these. + if name is not None: request.name = name if project_billing_info is not None: @@ -715,10 +789,10 @@ def get_iam_policy( The request object. Request message for `GetIamPolicy` method. resource (:class:`str`): - REQUIRED: The resource for which the policy is being - requested. ``resource`` is usually specified as a path. - For example, a Project resource is specified as - ``projects/{project}``. + REQUIRED: The resource for which the + policy is being requested. See the + operation documentation for the + appropriate value for this field. This corresponds to the ``resource`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -735,36 +809,67 @@ def get_iam_policy( It is used to specify access control policies for Cloud Platform resources. - A ``Policy`` consists of a list of ``bindings``. A - ``Binding`` binds a list of ``members`` to a ``role``, - where the members can be user accounts, Google groups, - Google domains, and service accounts. A ``role`` is a - named list of permissions defined by IAM. + A ``Policy`` is a collection of ``bindings``. A + ``binding`` binds one or more ``members`` to a single + ``role``. Members can be user accounts, service + accounts, Google groups, and domains (such as G Suite). + A ``role`` is a named list of permissions (defined by + IAM or configured by users). A ``binding`` can + optionally specify a ``condition``, which is a logic + expression that further constrains the role binding + based on attributes about the request and/or target + resource. - **Example** + **JSON Example** :: { "bindings": [ { - "role": "roles/owner", + "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", - "serviceAccount:my-other-app@appspot.gserviceaccount.com", + "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { - "role": "roles/viewer", - "members": ["user:sean@example.com"] + "role": "roles/resourcemanager.organizationViewer", + "members": ["user:eve@example.com"], + "condition": { + "title": "expirable access", + "description": "Does not grant access after Sep 2020", + "expression": "request.time < + timestamp('2020-10-01T00:00:00.000Z')", + } } ] } + **YAML Example** + + :: + + bindings: + - members: + - user:mike@example.com + - group:admins@example.com + - domain:google.com + - serviceAccount:my-project-id@appspot.gserviceaccount.com + role: roles/resourcemanager.organizationAdmin + - members: + - user:eve@example.com + role: roles/resourcemanager.organizationViewer + condition: + title: expirable access + description: Does not grant access after Sep 2020 + expression: request.time < timestamp('2020-10-01T00:00:00.000Z') + For a description of IAM and its features, see the `IAM - developer's guide `__. + developer's + guide `__. """ # Create or coerce a protobuf request object. @@ -780,11 +885,13 @@ def get_iam_policy( # so it must be constructed via keyword expansion. if isinstance(request, dict): request = iam_policy.GetIamPolicyRequest(**request) + elif not request: request = iam_policy.GetIamPolicyRequest() # If we have keyword arguments corresponding to fields on the # request, apply these. + if resource is not None: request.resource = resource @@ -828,10 +935,10 @@ def set_iam_policy( The request object. Request message for `SetIamPolicy` method. resource (:class:`str`): - REQUIRED: The resource for which the policy is being - specified. ``resource`` is usually specified as a path. - For example, a Project resource is specified as - ``projects/{project}``. + REQUIRED: The resource for which the + policy is being specified. See the + operation documentation for the + appropriate value for this field. This corresponds to the ``resource`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -848,36 +955,67 @@ def set_iam_policy( It is used to specify access control policies for Cloud Platform resources. - A ``Policy`` consists of a list of ``bindings``. A - ``Binding`` binds a list of ``members`` to a ``role``, - where the members can be user accounts, Google groups, - Google domains, and service accounts. A ``role`` is a - named list of permissions defined by IAM. + A ``Policy`` is a collection of ``bindings``. A + ``binding`` binds one or more ``members`` to a single + ``role``. Members can be user accounts, service + accounts, Google groups, and domains (such as G Suite). + A ``role`` is a named list of permissions (defined by + IAM or configured by users). A ``binding`` can + optionally specify a ``condition``, which is a logic + expression that further constrains the role binding + based on attributes about the request and/or target + resource. - **Example** + **JSON Example** :: { "bindings": [ { - "role": "roles/owner", + "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", - "serviceAccount:my-other-app@appspot.gserviceaccount.com", + "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { - "role": "roles/viewer", - "members": ["user:sean@example.com"] + "role": "roles/resourcemanager.organizationViewer", + "members": ["user:eve@example.com"], + "condition": { + "title": "expirable access", + "description": "Does not grant access after Sep 2020", + "expression": "request.time < + timestamp('2020-10-01T00:00:00.000Z')", + } } ] } + **YAML Example** + + :: + + bindings: + - members: + - user:mike@example.com + - group:admins@example.com + - domain:google.com + - serviceAccount:my-project-id@appspot.gserviceaccount.com + role: roles/resourcemanager.organizationAdmin + - members: + - user:eve@example.com + role: roles/resourcemanager.organizationViewer + condition: + title: expirable access + description: Does not grant access after Sep 2020 + expression: request.time < timestamp('2020-10-01T00:00:00.000Z') + For a description of IAM and its features, see the `IAM - developer's guide `__. + developer's + guide `__. """ # Create or coerce a protobuf request object. @@ -893,11 +1031,13 @@ def set_iam_policy( # so it must be constructed via keyword expansion. if isinstance(request, dict): request = iam_policy.SetIamPolicyRequest(**request) + elif not request: request = iam_policy.SetIamPolicyRequest() # If we have keyword arguments corresponding to fields on the # request, apply these. + if resource is not None: request.resource = resource @@ -936,10 +1076,10 @@ def test_iam_permissions( The request object. Request message for `TestIamPermissions` method. resource (:class:`str`): - REQUIRED: The resource for which the policy detail is - being requested. ``resource`` is usually specified as a - path. For example, a Project resource is specified as - ``projects/{project}``. + REQUIRED: The resource for which the + policy detail is being requested. See + the operation documentation for the + appropriate value for this field. This corresponds to the ``resource`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -975,11 +1115,13 @@ def test_iam_permissions( # so it must be constructed via keyword expansion. if isinstance(request, dict): request = iam_policy.TestIamPermissionsRequest(**request) + elif not request: request = iam_policy.TestIamPermissionsRequest() # If we have keyword arguments corresponding to fields on the # request, apply these. + if resource is not None: request.resource = resource diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/pagers.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/pagers.py index 5d9d2588aae0..b335a6f06afd 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/pagers.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/pagers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/transports/__init__.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/transports/__init__.py index 54fb186de5f7..e8cfecae594d 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/transports/__init__.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/transports/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/transports/base.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/transports/base.py index 76f4caaafb0a..a11bcc4f722d 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/transports/base.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/transports/base.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/transports/grpc.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/transports/grpc.py index b25dd44af90e..0edc1bf4097d 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/transports/grpc.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_billing/transports/grpc.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,10 +15,12 @@ # limitations under the License. # -from typing import Callable, Dict +from typing import Callable, Dict, Tuple from google.api_core import grpc_helpers # type: ignore from google.auth import credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore + import grpc # type: ignore @@ -48,7 +50,9 @@ def __init__( *, host: str = "cloudbilling.googleapis.com", credentials: credentials.Credentials = None, - channel: grpc.Channel = None + channel: grpc.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None ) -> None: """Instantiate the transport. @@ -62,20 +66,55 @@ def __init__( This argument is ignored if ``channel`` is provided. channel (Optional[grpc.Channel]): A ``Channel`` instance through which to make calls. + api_mtls_endpoint (Optional[str]): The mutual TLS endpoint. If + provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or applicatin default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A + callback to provide client SSL certificate bytes and private key + bytes, both in PEM format. It is ignored if ``api_mtls_endpoint`` + is None. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. """ - # Sanity check: Ensure that channel and credentials are not both - # provided. if channel: + # Sanity check: Ensure that channel and credentials are not both + # provided. credentials = False + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + elif api_mtls_endpoint: + host = ( + api_mtls_endpoint + if ":" in api_mtls_endpoint + else api_mtls_endpoint + ":443" + ) + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + ssl_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + ssl_credentials = SslCredentials().ssl_credentials + + # create a new channel. The provided one is ignored. + self._grpc_channel = grpc_helpers.create_channel( + host, + credentials=credentials, + ssl_credentials=ssl_credentials, + scopes=self.AUTH_SCOPES, + ) + # Run the base constructor. super().__init__(host=host, credentials=credentials) self._stubs = {} # type: Dict[str, Callable] - # If a channel was explicitly provided, set it. - if channel: - self._grpc_channel = channel - @classmethod def create_channel( cls, diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/__init__.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/__init__.py index a1466ee8a9c8..2bce2655b279 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/__init__.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/client.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/client.py index d15bed7e6be8..2af0753e285f 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/client.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/client.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ # from collections import OrderedDict -from typing import Dict, Iterable, Iterator, Sequence, Tuple, Type, Union +import re +from typing import Callable, Dict, Sequence, Tuple, Type, Union import pkg_resources import google.api_core.client_options as ClientOptions # type: ignore @@ -69,8 +70,38 @@ class CloudCatalogClient(metaclass=CloudCatalogClientMeta): Platform services and SKUs. """ - DEFAULT_OPTIONS = ClientOptions.ClientOptions( - api_endpoint="cloudbilling.googleapis.com" + @staticmethod + def _get_default_mtls_endpoint(api_endpoint): + """Convert api endpoint to mTLS endpoint. + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + str: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + DEFAULT_ENDPOINT = "cloudbilling.googleapis.com" + DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_ENDPOINT ) @classmethod @@ -98,7 +129,7 @@ def __init__( *, credentials: credentials.Credentials = None, transport: Union[str, CloudCatalogTransport] = None, - client_options: ClientOptions = DEFAULT_OPTIONS, + client_options: ClientOptions = None, ) -> None: """Instantiate the cloud catalog client. @@ -112,6 +143,17 @@ def __init__( transport to use. If set to None, a transport is chosen automatically. client_options (ClientOptions): Custom options for the client. + (1) The ``api_endpoint`` property can be used to override the + default endpoint provided by the client. + (2) If ``transport`` argument is None, ``client_options`` can be + used to create a mutual TLS transport. If ``client_cert_source`` + is provided, mutual TLS transport will be created with the given + ``api_endpoint`` or the default mTLS endpoint, and the client + SSL credentials obtained from ``client_cert_source``. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. """ if isinstance(client_options, dict): client_options = ClientOptions.from_dict(client_options) @@ -120,17 +162,46 @@ def __init__( # Ordinarily, we provide the transport, but allowing a custom transport # instance provides an extensibility point for unusual situations. if isinstance(transport, CloudCatalogTransport): + # transport is a CloudCatalogTransport instance. if credentials: raise ValueError( "When providing a transport instance, " "provide its credentials directly." ) self._transport = transport - else: + elif client_options is None or ( + client_options.api_endpoint is None + and client_options.client_cert_source is None + ): + # Don't trigger mTLS if we get an empty ClientOptions. Transport = type(self).get_transport_class(transport) self._transport = Transport( + credentials=credentials, host=self.DEFAULT_ENDPOINT + ) + else: + # We have a non-empty ClientOptions. If client_cert_source is + # provided, trigger mTLS with user provided endpoint or the default + # mTLS endpoint. + if client_options.client_cert_source: + api_mtls_endpoint = ( + client_options.api_endpoint + if client_options.api_endpoint + else self.DEFAULT_MTLS_ENDPOINT + ) + else: + api_mtls_endpoint = None + + api_endpoint = ( + client_options.api_endpoint + if client_options.api_endpoint + else self.DEFAULT_ENDPOINT + ) + + self._transport = CloudCatalogGrpcTransport( credentials=credentials, - host=client_options.api_endpoint or "cloudbilling.googleapis.com", + host=api_endpoint, + api_mtls_endpoint=api_mtls_endpoint, + client_cert_source=client_options.client_cert_source, ) def list_services( @@ -165,9 +236,6 @@ def list_services( request = cloud_catalog.ListServicesRequest(request) - # If we have keyword arguments corresponding to fields on the - # request, apply these. - # Wrap the RPC method; this adds retry and timeout information, # and friendly error handling. rpc = gapic_v1.method.wrap_method( @@ -237,6 +305,7 @@ def list_skus( # If we have keyword arguments corresponding to fields on the # request, apply these. + if parent is not None: request.parent = parent diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/pagers.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/pagers.py index 05ded0675371..3e74a0a36a1a 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/pagers.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/pagers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/transports/__init__.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/transports/__init__.py index 1192215ccb20..a75502743787 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/transports/__init__.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/transports/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/transports/base.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/transports/base.py index aae64d5b09ed..8881cf6d3cdf 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/transports/base.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/transports/base.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/transports/grpc.py b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/transports/grpc.py index 16fa7f8e8597..0a57347fcf72 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/transports/grpc.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/services/cloud_catalog/transports/grpc.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,10 +15,12 @@ # limitations under the License. # -from typing import Callable, Dict +from typing import Callable, Dict, Tuple from google.api_core import grpc_helpers # type: ignore from google.auth import credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore + import grpc # type: ignore @@ -47,7 +49,9 @@ def __init__( *, host: str = "cloudbilling.googleapis.com", credentials: credentials.Credentials = None, - channel: grpc.Channel = None + channel: grpc.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None ) -> None: """Instantiate the transport. @@ -61,20 +65,55 @@ def __init__( This argument is ignored if ``channel`` is provided. channel (Optional[grpc.Channel]): A ``Channel`` instance through which to make calls. + api_mtls_endpoint (Optional[str]): The mutual TLS endpoint. If + provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or applicatin default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A + callback to provide client SSL certificate bytes and private key + bytes, both in PEM format. It is ignored if ``api_mtls_endpoint`` + is None. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. """ - # Sanity check: Ensure that channel and credentials are not both - # provided. if channel: + # Sanity check: Ensure that channel and credentials are not both + # provided. credentials = False + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + elif api_mtls_endpoint: + host = ( + api_mtls_endpoint + if ":" in api_mtls_endpoint + else api_mtls_endpoint + ":443" + ) + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + ssl_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + ssl_credentials = SslCredentials().ssl_credentials + + # create a new channel. The provided one is ignored. + self._grpc_channel = grpc_helpers.create_channel( + host, + credentials=credentials, + ssl_credentials=ssl_credentials, + scopes=self.AUTH_SCOPES, + ) + # Run the base constructor. super().__init__(host=host, credentials=credentials) self._stubs = {} # type: Dict[str, Callable] - # If a channel was explicitly provided, set it. - if channel: - self._grpc_channel = channel - @classmethod def create_channel( cls, diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/types/__init__.py b/packages/google-cloud-billing/google/cloud/billing_v1/types/__init__.py index c310786d19f3..2521cc934466 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/types/__init__.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/types/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/types/cloud_billing.py b/packages/google-cloud-billing/google/cloud/billing_v1/types/cloud_billing.py index 078f23a5055b..8c9bf76387b9 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/types/cloud_billing.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/types/cloud_billing.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/google/cloud/billing_v1/types/cloud_catalog.py b/packages/google-cloud-billing/google/cloud/billing_v1/types/cloud_catalog.py index 64afdb9a7c38..206b93a6d409 100644 --- a/packages/google-cloud-billing/google/cloud/billing_v1/types/cloud_catalog.py +++ b/packages/google-cloud-billing/google/cloud/billing_v1/types/cloud_catalog.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-billing/mypy.ini b/packages/google-cloud-billing/mypy.ini index f23e6b533aad..4505b485436b 100644 --- a/packages/google-cloud-billing/mypy.ini +++ b/packages/google-cloud-billing/mypy.ini @@ -1,3 +1,3 @@ [mypy] -python_version = 3.5 +python_version = 3.6 namespace_packages = True diff --git a/packages/google-cloud-billing/noxfile.py b/packages/google-cloud-billing/noxfile.py index c9c617d47717..27cb25249f1a 100644 --- a/packages/google-cloud-billing/noxfile.py +++ b/packages/google-cloud-billing/noxfile.py @@ -110,8 +110,7 @@ def system(session): # Install all test dependencies, then install this package into the # virtualenv's dist-packages. - session.install("mock", "pytest") - + session.install("mock", "pytest", "google-cloud-testutils") session.install("-e", ".") # Run py.test against the system tests. diff --git a/packages/google-cloud-billing/scripts/fixup_keywords.py b/packages/google-cloud-billing/scripts/fixup_keywords.py index bb6fc9cec266..36398898962c 100644 --- a/packages/google-cloud-billing/scripts/fixup_keywords.py +++ b/packages/google-cloud-billing/scripts/fixup_keywords.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ class billingCallTransformer(cst.CSTTransformer): METHOD_TO_PARAMS: Dict[str, Tuple[str]] = { 'create_billing_account': ('billing_account', ), 'get_billing_account': ('name', ), - 'get_iam_policy': ('resource', ), + 'get_iam_policy': ('resource', 'options', ), 'get_project_billing_info': ('name', ), 'list_billing_accounts': ('page_size', 'page_token', 'filter', ), 'list_project_billing_info': ('name', 'page_size', 'page_token', ), diff --git a/packages/google-cloud-billing/setup.cfg b/packages/google-cloud-billing/setup.cfg index 3bd555500e37..c3a2b39f6528 100644 --- a/packages/google-cloud-billing/setup.cfg +++ b/packages/google-cloud-billing/setup.cfg @@ -1,3 +1,19 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2020 Google LLC +# +# 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 +# +# https://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. + # Generated by synthtool. DO NOT EDIT! [bdist_wheel] universal = 1 diff --git a/packages/google-cloud-billing/setup.py b/packages/google-cloud-billing/setup.py index 6cd1fe771be0..ea168fedfe7d 100644 --- a/packages/google-cloud-billing/setup.py +++ b/packages/google-cloud-billing/setup.py @@ -40,9 +40,7 @@ platforms="Posix; MacOS X; Windows", include_package_data=True, install_requires=( - "google-api-core >= 1.8.0, < 2.0.0dev", - "googleapis-common-protos >= 1.5.8", - "grpcio >= 1.10.0", + "google-api-core[grpc] >= 1.17.0, < 2.0.0dev", "grpc-google-iam-v1", "proto-plus >= 0.4.0", ), diff --git a/packages/google-cloud-billing/synth.metadata b/packages/google-cloud-billing/synth.metadata index c2df9c30d71b..ebd98fcbed15 100644 --- a/packages/google-cloud-billing/synth.metadata +++ b/packages/google-cloud-billing/synth.metadata @@ -1,20 +1,25 @@ { - "updateTime": "2020-02-27T22:14:39.297369Z", "sources": [ + { + "git": { + "name": ".", + "remote": "https://github.com/googleapis/python-billing.git", + "sha": "137538dd6a5e08194acdc1324c8e9e55309168e4" + } + }, { "git": { "name": "googleapis", "remote": "https://github.com/googleapis/googleapis.git", "sha": "e9e90a787703ec5d388902e2cb796aaed3a385b4", - "internalRef": "297671458", - "log": "e9e90a787703ec5d388902e2cb796aaed3a385b4\nDialogflow weekly v2/v2beta1 library update:\n - adding get validation result\n - adding field mask override control for output audio config\nImportant updates are also posted at:\nhttps://cloud.google.com/dialogflow/docs/release-notes\n\nPiperOrigin-RevId: 297671458\n\n1a2b05cc3541a5f7714529c665aecc3ea042c646\nAdding .yaml and .json config files.\n\nPiperOrigin-RevId: 297570622\n\ndfe1cf7be44dee31d78f78e485d8c95430981d6e\nPublish `QueryOptions` proto.\n\nIntroduced a `query_options` input in `ExecuteSqlRequest`.\n\nPiperOrigin-RevId: 297497710\n\ndafc905f71e5d46f500b41ed715aad585be062c3\npubsub: revert pull init_rpc_timeout & max_rpc_timeout back to 25 seconds and reset multiplier to 1.0\n\nPiperOrigin-RevId: 297486523\n\n" + "internalRef": "297671458" } }, { - "template": { - "name": "python_split_library", - "origin": "synthtool.gcp", - "version": "2020.2.4" + "git": { + "name": "synthtool", + "remote": "https://github.com/googleapis/synthtool.git", + "sha": "470789cee75ce93c41348ad6aa4c49363a80399b" } } ], diff --git a/packages/google-cloud-billing/synth.py b/packages/google-cloud-billing/synth.py index 7b23b5358b1a..400b4b42dc21 100644 --- a/packages/google-cloud-billing/synth.py +++ b/packages/google-cloud-billing/synth.py @@ -49,7 +49,8 @@ # Expand flake errors permitted to accomodate the Microgenerator # TODO: remove extra error codes once issues below are resolved -# F401: https://github.com/googleapis/gapic-generator-python/issues/324 -s.replace(".flake8", "ignore = .*", "ignore = E203, E266, E501, W503, F401") +# https://github.com/googleapis/gapic-generator-python/issues/425 +s.replace(".flake8", "(ignore = .*)", "\g<1>, F401, f841") + s.shell.run(["nox", "-s", "blacken"], hide_output=False) diff --git a/packages/google-cloud-billing/tests/unit/billing_v1/test_cloud_billing.py b/packages/google-cloud-billing/tests/unit/billing_v1/test_cloud_billing.py index bc2a2b85b2b9..e87b45499f10 100644 --- a/packages/google-cloud-billing/tests/unit/billing_v1/test_cloud_billing.py +++ b/packages/google-cloud-billing/tests/unit/billing_v1/test_cloud_billing.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,14 +23,48 @@ from google import auth from google.api_core import client_options +from google.api_core import grpc_helpers from google.auth import credentials from google.cloud.billing_v1.services.cloud_billing import CloudBillingClient from google.cloud.billing_v1.services.cloud_billing import pagers from google.cloud.billing_v1.services.cloud_billing import transports from google.cloud.billing_v1.types import cloud_billing from google.iam.v1 import iam_policy_pb2 as iam_policy # type: ignore +from google.iam.v1 import options_pb2 as options # type: ignore from google.iam.v1 import policy_pb2 as policy # type: ignore from google.oauth2 import service_account +from google.protobuf import field_mask_pb2 as field_mask # type: ignore +from google.type import expr_pb2 as expr # type: ignore + + +def client_cert_source_callback(): + return b"cert bytes", b"key bytes" + + +def test__get_default_mtls_endpoint(): + api_endpoint = "example.googleapis.com" + api_mtls_endpoint = "example.mtls.googleapis.com" + sandbox_endpoint = "example.sandbox.googleapis.com" + sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" + non_googleapi = "api.example.com" + + assert CloudBillingClient._get_default_mtls_endpoint(None) is None + assert ( + CloudBillingClient._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint + ) + assert ( + CloudBillingClient._get_default_mtls_endpoint(api_mtls_endpoint) + == api_mtls_endpoint + ) + assert ( + CloudBillingClient._get_default_mtls_endpoint(sandbox_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + CloudBillingClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) + == sandbox_mtls_endpoint + ) + assert CloudBillingClient._get_default_mtls_endpoint(non_googleapi) == non_googleapi def test_cloud_billing_client_from_service_account_file(): @@ -49,28 +83,87 @@ def test_cloud_billing_client_from_service_account_file(): def test_cloud_billing_client_client_options(): - # Check the default options have their expected values. - assert ( - CloudBillingClient.DEFAULT_OPTIONS.api_endpoint == "cloudbilling.googleapis.com" - ) + # Check that if channel is provided we won't create a new one. + with mock.patch( + "google.cloud.billing_v1.services.cloud_billing.CloudBillingClient.get_transport_class" + ) as gtc: + transport = transports.CloudBillingGrpcTransport( + credentials=credentials.AnonymousCredentials() + ) + client = CloudBillingClient(transport=transport) + gtc.assert_not_called() - # Check that options can be customized. - options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + # Check mTLS is not triggered with empty client options. + options = client_options.ClientOptions() with mock.patch( "google.cloud.billing_v1.services.cloud_billing.CloudBillingClient.get_transport_class" ) as gtc: transport = gtc.return_value = mock.MagicMock() client = CloudBillingClient(client_options=options) - transport.assert_called_once_with(credentials=None, host="squid.clam.whelk") + transport.assert_called_once_with( + credentials=None, host=client.DEFAULT_ENDPOINT + ) + + # Check mTLS is not triggered if api_endpoint is provided but + # client_cert_source is None. + options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + with mock.patch( + "google.cloud.billing_v1.services.cloud_billing.transports.CloudBillingGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = CloudBillingClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint=None, + client_cert_source=None, + credentials=None, + host="squid.clam.whelk", + ) + + # Check mTLS is triggered if client_cert_source is provided. + options = client_options.ClientOptions( + client_cert_source=client_cert_source_callback + ) + with mock.patch( + "google.cloud.billing_v1.services.cloud_billing.transports.CloudBillingGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = CloudBillingClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT, + client_cert_source=client_cert_source_callback, + credentials=None, + host=client.DEFAULT_ENDPOINT, + ) + + # Check mTLS is triggered if api_endpoint and client_cert_source are provided. + options = client_options.ClientOptions( + api_endpoint="squid.clam.whelk", client_cert_source=client_cert_source_callback + ) + with mock.patch( + "google.cloud.billing_v1.services.cloud_billing.transports.CloudBillingGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = CloudBillingClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint="squid.clam.whelk", + client_cert_source=client_cert_source_callback, + credentials=None, + host="squid.clam.whelk", + ) def test_cloud_billing_client_client_options_from_dict(): with mock.patch( - "google.cloud.billing_v1.services.cloud_billing.CloudBillingClient.get_transport_class" - ) as gtc: - transport = gtc.return_value = mock.MagicMock() + "google.cloud.billing_v1.services.cloud_billing.transports.CloudBillingGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None client = CloudBillingClient(client_options={"api_endpoint": "squid.clam.whelk"}) - transport.assert_called_once_with(credentials=None, host="squid.clam.whelk") + grpc_transport.assert_called_once_with( + api_mtls_endpoint=None, + client_cert_source=None, + credentials=None, + host="squid.clam.whelk", + ) def test_get_billing_account(transport: str = "grpc"): @@ -105,7 +198,8 @@ def test_get_billing_account(transport: str = "grpc"): # Establish that the response is the type that we expect. assert isinstance(response, cloud_billing.BillingAccount) assert response.name == "name_value" - assert response.open == True + + assert response.open is True assert response.display_name == "display_name_value" assert response.master_billing_account == "master_billing_account_value" @@ -122,7 +216,7 @@ def test_get_billing_account_field_headers(): type(client._transport.get_billing_account), "__call__" ) as call: call.return_value = cloud_billing.BillingAccount() - response = client.get_billing_account(request) + client.get_billing_account(request) # Establish that the underlying gRPC stub method was called. assert len(call.mock_calls) == 1 @@ -230,7 +324,7 @@ def test_list_billing_accounts_pager(): ) results = [i for i in client.list_billing_accounts(request={})] assert len(results) == 6 - assert all([isinstance(i, cloud_billing.BillingAccount) for i in results]) + assert all(isinstance(i, cloud_billing.BillingAccount) for i in results) def test_list_billing_accounts_pages(): @@ -301,7 +395,8 @@ def test_update_billing_account(transport: str = "grpc"): # Establish that the response is the type that we expect. assert isinstance(response, cloud_billing.BillingAccount) assert response.name == "name_value" - assert response.open == True + + assert response.open is True assert response.display_name == "display_name_value" assert response.master_billing_account == "master_billing_account_value" @@ -375,7 +470,8 @@ def test_create_billing_account(transport: str = "grpc"): # Establish that the response is the type that we expect. assert isinstance(response, cloud_billing.BillingAccount) assert response.name == "name_value" - assert response.open == True + + assert response.open is True assert response.display_name == "display_name_value" assert response.master_billing_account == "master_billing_account_value" @@ -460,7 +556,7 @@ def test_list_project_billing_info_field_headers(): type(client._transport.list_project_billing_info), "__call__" ) as call: call.return_value = cloud_billing.ListProjectBillingInfoResponse() - response = client.list_project_billing_info(request) + client.list_project_billing_info(request) # Establish that the underlying gRPC stub method was called. assert len(call.mock_calls) == 1 @@ -538,7 +634,7 @@ def test_list_project_billing_info_pager(): ) results = [i for i in client.list_project_billing_info(request={})] assert len(results) == 6 - assert all([isinstance(i, cloud_billing.ProjectBillingInfo) for i in results]) + assert all(isinstance(i, cloud_billing.ProjectBillingInfo) for i in results) def test_list_project_billing_info_pages(): @@ -612,7 +708,8 @@ def test_get_project_billing_info(transport: str = "grpc"): assert response.name == "name_value" assert response.project_id == "project_id_value" assert response.billing_account_name == "billing_account_name_value" - assert response.billing_enabled == True + + assert response.billing_enabled is True def test_get_project_billing_info_field_headers(): @@ -627,7 +724,7 @@ def test_get_project_billing_info_field_headers(): type(client._transport.get_project_billing_info), "__call__" ) as call: call.return_value = cloud_billing.ProjectBillingInfo() - response = client.get_project_billing_info(request) + client.get_project_billing_info(request) # Establish that the underlying gRPC stub method was called. assert len(call.mock_calls) == 1 @@ -705,7 +802,8 @@ def test_update_project_billing_info(transport: str = "grpc"): assert response.name == "name_value" assert response.project_id == "project_id_value" assert response.billing_account_name == "billing_account_name_value" - assert response.billing_enabled == True + + assert response.billing_enabled is True def test_update_project_billing_info_flattened(): @@ -786,7 +884,7 @@ def test_get_iam_policy_field_headers(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object(type(client._transport.get_iam_policy), "__call__") as call: call.return_value = policy.Policy() - response = client.get_iam_policy(request) + client.get_iam_policy(request) # Establish that the underlying gRPC stub method was called. assert len(call.mock_calls) == 1 @@ -805,7 +903,12 @@ def test_get_iam_policy_from_dict(): # Designate an appropriate return value for the call. call.return_value = policy.Policy() - response = client.get_iam_policy(request={"resource": "resource_value"}) + response = client.get_iam_policy( + request={ + "resource": "resource_value", + "options": options.GetPolicyOptions(requested_policy_version=2598), + } + ) call.assert_called() @@ -1048,7 +1151,7 @@ def test_cloud_billing_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(auth, "default") as adc: adc.return_value = (credentials.AnonymousCredentials(), None) - client = CloudBillingClient() + CloudBillingClient() adc.assert_called_once_with( scopes=("https://www.googleapis.com/auth/cloud-platform",) ) @@ -1078,5 +1181,84 @@ def test_cloud_billing_host_with_port(): def test_cloud_billing_grpc_transport_channel(): channel = grpc.insecure_channel("http://localhost/") - transport = transports.CloudBillingGrpcTransport(channel=channel) - assert transport.grpc_channel is channel + + # Check that if channel is provided, mtls endpoint and client_cert_source + # won't be used. + callback = mock.MagicMock() + transport = transports.CloudBillingGrpcTransport( + host="squid.clam.whelk", + channel=channel, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=callback, + ) + assert transport.grpc_channel == channel + assert transport._host == "squid.clam.whelk:443" + assert not callback.called + + +@mock.patch("grpc.ssl_channel_credentials", autospec=True) +@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) +def test_cloud_billing_grpc_transport_channel_mtls_with_client_cert_source( + grpc_create_channel, grpc_ssl_channel_cred +): + # Check that if channel is None, but api_mtls_endpoint and client_cert_source + # are provided, then a mTLS channel will be created. + mock_cred = mock.Mock() + + mock_ssl_cred = mock.Mock() + grpc_ssl_channel_cred.return_value = mock_ssl_cred + + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + transport = transports.CloudBillingGrpcTransport( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=client_cert_source_callback, + ) + grpc_ssl_channel_cred.assert_called_once_with( + certificate_chain=b"cert bytes", private_key=b"key bytes" + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + ssl_credentials=mock_ssl_cred, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ) + assert transport.grpc_channel == mock_grpc_channel + + +@pytest.mark.parametrize( + "api_mtls_endpoint", ["mtls.squid.clam.whelk", "mtls.squid.clam.whelk:443"] +) +@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) +def test_cloud_billing_grpc_transport_channel_mtls_with_adc( + grpc_create_channel, api_mtls_endpoint +): + # Check that if channel and client_cert_source are None, but api_mtls_endpoint + # is provided, then a mTLS channel will be created with SSL ADC. + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + # Mock google.auth.transport.grpc.SslCredentials class. + mock_ssl_cred = mock.Mock() + with mock.patch.multiple( + "google.auth.transport.grpc.SslCredentials", + __init__=mock.Mock(return_value=None), + ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), + ): + mock_cred = mock.Mock() + transport = transports.CloudBillingGrpcTransport( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint=api_mtls_endpoint, + client_cert_source=None, + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + ssl_credentials=mock_ssl_cred, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ) + assert transport.grpc_channel == mock_grpc_channel diff --git a/packages/google-cloud-billing/tests/unit/billing_v1/test_cloud_catalog.py b/packages/google-cloud-billing/tests/unit/billing_v1/test_cloud_catalog.py index 5b767c178335..2c4510ae7ed9 100644 --- a/packages/google-cloud-billing/tests/unit/billing_v1/test_cloud_catalog.py +++ b/packages/google-cloud-billing/tests/unit/billing_v1/test_cloud_catalog.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,12 +23,44 @@ from google import auth from google.api_core import client_options +from google.api_core import grpc_helpers from google.auth import credentials from google.cloud.billing_v1.services.cloud_catalog import CloudCatalogClient from google.cloud.billing_v1.services.cloud_catalog import pagers from google.cloud.billing_v1.services.cloud_catalog import transports from google.cloud.billing_v1.types import cloud_catalog from google.oauth2 import service_account +from google.protobuf import timestamp_pb2 as timestamp # type: ignore + + +def client_cert_source_callback(): + return b"cert bytes", b"key bytes" + + +def test__get_default_mtls_endpoint(): + api_endpoint = "example.googleapis.com" + api_mtls_endpoint = "example.mtls.googleapis.com" + sandbox_endpoint = "example.sandbox.googleapis.com" + sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" + non_googleapi = "api.example.com" + + assert CloudCatalogClient._get_default_mtls_endpoint(None) is None + assert ( + CloudCatalogClient._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint + ) + assert ( + CloudCatalogClient._get_default_mtls_endpoint(api_mtls_endpoint) + == api_mtls_endpoint + ) + assert ( + CloudCatalogClient._get_default_mtls_endpoint(sandbox_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + CloudCatalogClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) + == sandbox_mtls_endpoint + ) + assert CloudCatalogClient._get_default_mtls_endpoint(non_googleapi) == non_googleapi def test_cloud_catalog_client_from_service_account_file(): @@ -47,28 +79,87 @@ def test_cloud_catalog_client_from_service_account_file(): def test_cloud_catalog_client_client_options(): - # Check the default options have their expected values. - assert ( - CloudCatalogClient.DEFAULT_OPTIONS.api_endpoint == "cloudbilling.googleapis.com" - ) + # Check that if channel is provided we won't create a new one. + with mock.patch( + "google.cloud.billing_v1.services.cloud_catalog.CloudCatalogClient.get_transport_class" + ) as gtc: + transport = transports.CloudCatalogGrpcTransport( + credentials=credentials.AnonymousCredentials() + ) + client = CloudCatalogClient(transport=transport) + gtc.assert_not_called() - # Check that options can be customized. - options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + # Check mTLS is not triggered with empty client options. + options = client_options.ClientOptions() with mock.patch( "google.cloud.billing_v1.services.cloud_catalog.CloudCatalogClient.get_transport_class" ) as gtc: transport = gtc.return_value = mock.MagicMock() client = CloudCatalogClient(client_options=options) - transport.assert_called_once_with(credentials=None, host="squid.clam.whelk") + transport.assert_called_once_with( + credentials=None, host=client.DEFAULT_ENDPOINT + ) + + # Check mTLS is not triggered if api_endpoint is provided but + # client_cert_source is None. + options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + with mock.patch( + "google.cloud.billing_v1.services.cloud_catalog.transports.CloudCatalogGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = CloudCatalogClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint=None, + client_cert_source=None, + credentials=None, + host="squid.clam.whelk", + ) + + # Check mTLS is triggered if client_cert_source is provided. + options = client_options.ClientOptions( + client_cert_source=client_cert_source_callback + ) + with mock.patch( + "google.cloud.billing_v1.services.cloud_catalog.transports.CloudCatalogGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = CloudCatalogClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT, + client_cert_source=client_cert_source_callback, + credentials=None, + host=client.DEFAULT_ENDPOINT, + ) + + # Check mTLS is triggered if api_endpoint and client_cert_source are provided. + options = client_options.ClientOptions( + api_endpoint="squid.clam.whelk", client_cert_source=client_cert_source_callback + ) + with mock.patch( + "google.cloud.billing_v1.services.cloud_catalog.transports.CloudCatalogGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = CloudCatalogClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint="squid.clam.whelk", + client_cert_source=client_cert_source_callback, + credentials=None, + host="squid.clam.whelk", + ) def test_cloud_catalog_client_client_options_from_dict(): with mock.patch( - "google.cloud.billing_v1.services.cloud_catalog.CloudCatalogClient.get_transport_class" - ) as gtc: - transport = gtc.return_value = mock.MagicMock() + "google.cloud.billing_v1.services.cloud_catalog.transports.CloudCatalogGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None client = CloudCatalogClient(client_options={"api_endpoint": "squid.clam.whelk"}) - transport.assert_called_once_with(credentials=None, host="squid.clam.whelk") + grpc_transport.assert_called_once_with( + api_mtls_endpoint=None, + client_cert_source=None, + credentials=None, + host="squid.clam.whelk", + ) def test_list_services(transport: str = "grpc"): @@ -126,7 +217,7 @@ def test_list_services_pager(): ) results = [i for i in client.list_services(request={})] assert len(results) == 6 - assert all([isinstance(i, cloud_catalog.Service) for i in results]) + assert all(isinstance(i, cloud_catalog.Service) for i in results) def test_list_services_pages(): @@ -197,7 +288,7 @@ def test_list_skus_field_headers(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object(type(client._transport.list_skus), "__call__") as call: call.return_value = cloud_catalog.ListSkusResponse() - response = client.list_skus(request) + client.list_skus(request) # Establish that the underlying gRPC stub method was called. assert len(call.mock_calls) == 1 @@ -259,7 +350,7 @@ def test_list_skus_pager(): ) results = [i for i in client.list_skus(request={})] assert len(results) == 6 - assert all([isinstance(i, cloud_catalog.Sku) for i in results]) + assert all(isinstance(i, cloud_catalog.Sku) for i in results) def test_list_skus_pages(): @@ -331,7 +422,7 @@ def test_cloud_catalog_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(auth, "default") as adc: adc.return_value = (credentials.AnonymousCredentials(), None) - client = CloudCatalogClient() + CloudCatalogClient() adc.assert_called_once_with( scopes=("https://www.googleapis.com/auth/cloud-platform",) ) @@ -361,5 +452,84 @@ def test_cloud_catalog_host_with_port(): def test_cloud_catalog_grpc_transport_channel(): channel = grpc.insecure_channel("http://localhost/") - transport = transports.CloudCatalogGrpcTransport(channel=channel) - assert transport.grpc_channel is channel + + # Check that if channel is provided, mtls endpoint and client_cert_source + # won't be used. + callback = mock.MagicMock() + transport = transports.CloudCatalogGrpcTransport( + host="squid.clam.whelk", + channel=channel, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=callback, + ) + assert transport.grpc_channel == channel + assert transport._host == "squid.clam.whelk:443" + assert not callback.called + + +@mock.patch("grpc.ssl_channel_credentials", autospec=True) +@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) +def test_cloud_catalog_grpc_transport_channel_mtls_with_client_cert_source( + grpc_create_channel, grpc_ssl_channel_cred +): + # Check that if channel is None, but api_mtls_endpoint and client_cert_source + # are provided, then a mTLS channel will be created. + mock_cred = mock.Mock() + + mock_ssl_cred = mock.Mock() + grpc_ssl_channel_cred.return_value = mock_ssl_cred + + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + transport = transports.CloudCatalogGrpcTransport( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=client_cert_source_callback, + ) + grpc_ssl_channel_cred.assert_called_once_with( + certificate_chain=b"cert bytes", private_key=b"key bytes" + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + ssl_credentials=mock_ssl_cred, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ) + assert transport.grpc_channel == mock_grpc_channel + + +@pytest.mark.parametrize( + "api_mtls_endpoint", ["mtls.squid.clam.whelk", "mtls.squid.clam.whelk:443"] +) +@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) +def test_cloud_catalog_grpc_transport_channel_mtls_with_adc( + grpc_create_channel, api_mtls_endpoint +): + # Check that if channel and client_cert_source are None, but api_mtls_endpoint + # is provided, then a mTLS channel will be created with SSL ADC. + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + # Mock google.auth.transport.grpc.SslCredentials class. + mock_ssl_cred = mock.Mock() + with mock.patch.multiple( + "google.auth.transport.grpc.SslCredentials", + __init__=mock.Mock(return_value=None), + ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), + ): + mock_cred = mock.Mock() + transport = transports.CloudCatalogGrpcTransport( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint=api_mtls_endpoint, + client_cert_source=None, + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + ssl_credentials=mock_ssl_cred, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ) + assert transport.grpc_channel == mock_grpc_channel