feat: Inital commit
This commit is contained in:
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+368
@@ -0,0 +1,368 @@
|
||||
# Changelog
|
||||
|
||||
## [0.13.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.12.1...auth/v0.13.0) (2024-12-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** Add logging support ([#11079](https://github.com/googleapis/google-cloud-go/issues/11079)) ([c80e31d](https://github.com/googleapis/google-cloud-go/commit/c80e31df5ecb33a810be3dfb9d9e27ac531aa91d))
|
||||
* **auth:** Pass logger from auth layer to metadata package ([#11288](https://github.com/googleapis/google-cloud-go/issues/11288)) ([b552efd](https://github.com/googleapis/google-cloud-go/commit/b552efd6ab34e5dfded18438e0fbfd925805614f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Check compute cred type before non-default flag for DP ([#11255](https://github.com/googleapis/google-cloud-go/issues/11255)) ([4347ca1](https://github.com/googleapis/google-cloud-go/commit/4347ca141892be8ae813399b4b437662a103bc90))
|
||||
|
||||
## [0.12.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.12.0...auth/v0.12.1) (2024-12-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Correct typo in link ([#11160](https://github.com/googleapis/google-cloud-go/issues/11160)) ([af6fb46](https://github.com/googleapis/google-cloud-go/commit/af6fb46d7cd694ddbe8c9d63bc4cdcd62b9fb2c1))
|
||||
|
||||
## [0.12.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.11.0...auth/v0.12.0) (2024-12-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** Add support for providing custom certificate URL ([#11006](https://github.com/googleapis/google-cloud-go/issues/11006)) ([ebf3657](https://github.com/googleapis/google-cloud-go/commit/ebf36579724afb375d3974cf1da38f703e3b7dbc)), refs [#11005](https://github.com/googleapis/google-cloud-go/issues/11005)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Ensure endpoints are present in Validator ([#11209](https://github.com/googleapis/google-cloud-go/issues/11209)) ([106cd53](https://github.com/googleapis/google-cloud-go/commit/106cd53309facaef1b8ea78376179f523f6912b9)), refs [#11006](https://github.com/googleapis/google-cloud-go/issues/11006) [#11190](https://github.com/googleapis/google-cloud-go/issues/11190) [#11189](https://github.com/googleapis/google-cloud-go/issues/11189) [#11188](https://github.com/googleapis/google-cloud-go/issues/11188)
|
||||
|
||||
## [0.11.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.10.2...auth/v0.11.0) (2024-11-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** Add universe domain support to mTLS ([#11159](https://github.com/googleapis/google-cloud-go/issues/11159)) ([117748b](https://github.com/googleapis/google-cloud-go/commit/117748ba1cfd4ae62a6a4feb7e30951cb2bc9344))
|
||||
|
||||
## [0.10.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.10.1...auth/v0.10.2) (2024-11-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Restore use of grpc.Dial ([#11118](https://github.com/googleapis/google-cloud-go/issues/11118)) ([2456b94](https://github.com/googleapis/google-cloud-go/commit/2456b943b7b8aaabd4d8bfb7572c0f477ae0db45)), refs [#7556](https://github.com/googleapis/google-cloud-go/issues/7556)
|
||||
|
||||
## [0.10.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.10.0...auth/v0.10.1) (2024-11-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Restore Application Default Credentials support to idtoken ([#11083](https://github.com/googleapis/google-cloud-go/issues/11083)) ([8771f2e](https://github.com/googleapis/google-cloud-go/commit/8771f2ea9807ab822083808e0678392edff3b4f2))
|
||||
* **auth:** Skip impersonate universe domain check if empty ([#11086](https://github.com/googleapis/google-cloud-go/issues/11086)) ([87159c1](https://github.com/googleapis/google-cloud-go/commit/87159c1059d4a18d1367ce62746a838a94964ab6))
|
||||
|
||||
## [0.10.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.9...auth/v0.10.0) (2024-10-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** Add universe domain support to credentials/impersonate ([#10953](https://github.com/googleapis/google-cloud-go/issues/10953)) ([e06cb64](https://github.com/googleapis/google-cloud-go/commit/e06cb6499f7eda3aef08ab18ff197016f667684b))
|
||||
|
||||
## [0.9.9](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.8...auth/v0.9.9) (2024-10-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Fallback cert lookups for missing files ([#11013](https://github.com/googleapis/google-cloud-go/issues/11013)) ([bd76695](https://github.com/googleapis/google-cloud-go/commit/bd766957ec238b7c40ddbabb369e612dc9b07313)), refs [#10844](https://github.com/googleapis/google-cloud-go/issues/10844)
|
||||
* **auth:** Replace MDS endpoint universe_domain with universe-domain ([#11000](https://github.com/googleapis/google-cloud-go/issues/11000)) ([6a1586f](https://github.com/googleapis/google-cloud-go/commit/6a1586f2ce9974684affaea84e7b629313b4d114))
|
||||
|
||||
## [0.9.8](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.7...auth/v0.9.8) (2024-10-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Restore OpenTelemetry handling in transports ([#10968](https://github.com/googleapis/google-cloud-go/issues/10968)) ([08c6d04](https://github.com/googleapis/google-cloud-go/commit/08c6d04901c1a20e219b2d86df41dbaa6d7d7b55)), refs [#10962](https://github.com/googleapis/google-cloud-go/issues/10962)
|
||||
* **auth:** Try talk to plaintext S2A if credentials can not be found for mTLS-S2A ([#10941](https://github.com/googleapis/google-cloud-go/issues/10941)) ([0f0bf2d](https://github.com/googleapis/google-cloud-go/commit/0f0bf2d18c97dd8b65bcf0099f0802b5631c6287))
|
||||
|
||||
## [0.9.7](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.6...auth/v0.9.7) (2024-10-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Restore support for non-default service accounts for DirectPath ([#10937](https://github.com/googleapis/google-cloud-go/issues/10937)) ([a38650e](https://github.com/googleapis/google-cloud-go/commit/a38650edbf420223077498cafa537aec74b37aad)), refs [#10907](https://github.com/googleapis/google-cloud-go/issues/10907)
|
||||
|
||||
## [0.9.6](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.5...auth/v0.9.6) (2024-09-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Make aws credentials provider retrieve fresh credentials ([#10920](https://github.com/googleapis/google-cloud-go/issues/10920)) ([250fbf8](https://github.com/googleapis/google-cloud-go/commit/250fbf87d858d865e399a241b7e537c4ff0c3dd8))
|
||||
|
||||
## [0.9.5](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.4...auth/v0.9.5) (2024-09-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Restore support for GOOGLE_CLOUD_UNIVERSE_DOMAIN env ([#10915](https://github.com/googleapis/google-cloud-go/issues/10915)) ([94caaaa](https://github.com/googleapis/google-cloud-go/commit/94caaaa061362d0e00ef6214afcc8a0a3e7ebfb2))
|
||||
* **auth:** Skip directpath credentials overwrite when it's not on GCE ([#10833](https://github.com/googleapis/google-cloud-go/issues/10833)) ([7e5e8d1](https://github.com/googleapis/google-cloud-go/commit/7e5e8d10b761b0a6e43e19a028528db361bc07b1))
|
||||
* **auth:** Use new context for non-blocking token refresh ([#10919](https://github.com/googleapis/google-cloud-go/issues/10919)) ([cf7102d](https://github.com/googleapis/google-cloud-go/commit/cf7102d33a21be1e5a9d47a49456b3a57c43b350))
|
||||
|
||||
## [0.9.4](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.3...auth/v0.9.4) (2024-09-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Enable self-signed JWT for non-GDU universe domain ([#10831](https://github.com/googleapis/google-cloud-go/issues/10831)) ([f9869f7](https://github.com/googleapis/google-cloud-go/commit/f9869f7903cfd34d1b97c25d0dc5669d2c5138e6))
|
||||
|
||||
## [0.9.3](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.2...auth/v0.9.3) (2024-09-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Choose quota project envvar over file when both present ([#10807](https://github.com/googleapis/google-cloud-go/issues/10807)) ([2d8dd77](https://github.com/googleapis/google-cloud-go/commit/2d8dd7700eff92d4b95027be55e26e1e7aa79181)), refs [#10804](https://github.com/googleapis/google-cloud-go/issues/10804)
|
||||
|
||||
## [0.9.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.1...auth/v0.9.2) (2024-08-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Handle non-Transport DefaultTransport ([#10733](https://github.com/googleapis/google-cloud-go/issues/10733)) ([98d91dc](https://github.com/googleapis/google-cloud-go/commit/98d91dc8316b247498fab41ab35e57a0446fe556)), refs [#10742](https://github.com/googleapis/google-cloud-go/issues/10742)
|
||||
* **auth:** Make sure quota option takes precedence over env/file ([#10797](https://github.com/googleapis/google-cloud-go/issues/10797)) ([f1b050d](https://github.com/googleapis/google-cloud-go/commit/f1b050d56d804b245cab048c2980d32b0eaceb4e)), refs [#10795](https://github.com/googleapis/google-cloud-go/issues/10795)
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* **auth:** Fix Go doc comment link ([#10751](https://github.com/googleapis/google-cloud-go/issues/10751)) ([015acfa](https://github.com/googleapis/google-cloud-go/commit/015acfab4d172650928bb1119bc2cd6307b9a437))
|
||||
|
||||
## [0.9.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.9.0...auth/v0.9.1) (2024-08-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Setting expireEarly to default when the value is 0 ([#10732](https://github.com/googleapis/google-cloud-go/issues/10732)) ([5e67869](https://github.com/googleapis/google-cloud-go/commit/5e67869a31e9e8ecb4eeebd2cfa11a761c3b1948))
|
||||
|
||||
## [0.9.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.8.1...auth/v0.9.0) (2024-08-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** Auth library can talk to S2A over mTLS ([#10634](https://github.com/googleapis/google-cloud-go/issues/10634)) ([5250a13](https://github.com/googleapis/google-cloud-go/commit/5250a13ec95b8d4eefbe0158f82857ff2189cb45))
|
||||
|
||||
## [0.8.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.8.0...auth/v0.8.1) (2024-08-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Make default client creation more lenient ([#10669](https://github.com/googleapis/google-cloud-go/issues/10669)) ([1afb9ee](https://github.com/googleapis/google-cloud-go/commit/1afb9ee1ee9de9810722800018133304a0ca34d1)), refs [#10638](https://github.com/googleapis/google-cloud-go/issues/10638)
|
||||
|
||||
## [0.8.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.7.3...auth/v0.8.0) (2024-08-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** Adds support for X509 workload identity federation ([#10373](https://github.com/googleapis/google-cloud-go/issues/10373)) ([5d07505](https://github.com/googleapis/google-cloud-go/commit/5d075056cbe27bb1da4072a26070c41f8999eb9b))
|
||||
|
||||
## [0.7.3](https://github.com/googleapis/google-cloud-go/compare/auth/v0.7.2...auth/v0.7.3) (2024-08-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/oauth2adapt:** Update dependencies ([257c40b](https://github.com/googleapis/google-cloud-go/commit/257c40bd6d7e59730017cf32bda8823d7a232758))
|
||||
* **auth:** Disable automatic universe domain check for MDS ([#10620](https://github.com/googleapis/google-cloud-go/issues/10620)) ([7cea5ed](https://github.com/googleapis/google-cloud-go/commit/7cea5edd5a0c1e6bca558696f5607879141910e8))
|
||||
* **auth:** Update dependencies ([257c40b](https://github.com/googleapis/google-cloud-go/commit/257c40bd6d7e59730017cf32bda8823d7a232758))
|
||||
|
||||
## [0.7.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.7.1...auth/v0.7.2) (2024-07-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Use default client for universe metadata lookup ([#10551](https://github.com/googleapis/google-cloud-go/issues/10551)) ([d9046fd](https://github.com/googleapis/google-cloud-go/commit/d9046fdd1435d1ce48f374806c1def4cb5ac6cd3)), refs [#10544](https://github.com/googleapis/google-cloud-go/issues/10544)
|
||||
|
||||
## [0.7.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.7.0...auth/v0.7.1) (2024-07-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Bump google.golang.org/grpc@v1.64.1 ([8ecc4e9](https://github.com/googleapis/google-cloud-go/commit/8ecc4e9622e5bbe9b90384d5848ab816027226c5))
|
||||
|
||||
## [0.7.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.6.1...auth/v0.7.0) (2024-07-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** Add workload X509 cert provider as a default cert provider ([#10479](https://github.com/googleapis/google-cloud-go/issues/10479)) ([c51ee6c](https://github.com/googleapis/google-cloud-go/commit/c51ee6cf65ce05b4d501083e49d468c75ac1ea63))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/oauth2adapt:** Bump google.golang.org/api@v0.187.0 ([8fa9e39](https://github.com/googleapis/google-cloud-go/commit/8fa9e398e512fd8533fd49060371e61b5725a85b))
|
||||
* **auth:** Bump google.golang.org/api@v0.187.0 ([8fa9e39](https://github.com/googleapis/google-cloud-go/commit/8fa9e398e512fd8533fd49060371e61b5725a85b))
|
||||
* **auth:** Check len of slices, not non-nil ([#10483](https://github.com/googleapis/google-cloud-go/issues/10483)) ([0a966a1](https://github.com/googleapis/google-cloud-go/commit/0a966a183e5f0e811977216d736d875b7233e942))
|
||||
|
||||
## [0.6.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.6.0...auth/v0.6.1) (2024-07-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Support gRPC API keys ([#10460](https://github.com/googleapis/google-cloud-go/issues/10460)) ([daa6646](https://github.com/googleapis/google-cloud-go/commit/daa6646d2af5d7fb5b30489f4934c7db89868c7c))
|
||||
* **auth:** Update http and grpc transports to support token exchange over mTLS ([#10397](https://github.com/googleapis/google-cloud-go/issues/10397)) ([c6dfdcf](https://github.com/googleapis/google-cloud-go/commit/c6dfdcf893c3f971eba15026c12db0a960ae81f2))
|
||||
|
||||
## [0.6.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.5.2...auth/v0.6.0) (2024-06-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** Add non-blocking token refresh for compute MDS ([#10263](https://github.com/googleapis/google-cloud-go/issues/10263)) ([9ac350d](https://github.com/googleapis/google-cloud-go/commit/9ac350da11a49b8e2174d3fc5b1a5070fec78b4e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Return error if envvar detected file returns an error ([#10431](https://github.com/googleapis/google-cloud-go/issues/10431)) ([e52b9a7](https://github.com/googleapis/google-cloud-go/commit/e52b9a7c45468827f5d220ab00965191faeb9d05))
|
||||
|
||||
## [0.5.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.5.1...auth/v0.5.2) (2024-06-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Fetch initial token when CachedTokenProviderOptions.DisableAutoRefresh is true ([#10415](https://github.com/googleapis/google-cloud-go/issues/10415)) ([3266763](https://github.com/googleapis/google-cloud-go/commit/32667635ca2efad05cd8c087c004ca07d7406913)), refs [#10414](https://github.com/googleapis/google-cloud-go/issues/10414)
|
||||
|
||||
## [0.5.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.5.0...auth/v0.5.1) (2024-05-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Pass through client to 2LO and 3LO flows ([#10290](https://github.com/googleapis/google-cloud-go/issues/10290)) ([685784e](https://github.com/googleapis/google-cloud-go/commit/685784ea84358c15e9214bdecb307d37aa3b6d2f))
|
||||
|
||||
## [0.5.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.4.2...auth/v0.5.0) (2024-05-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** Adds X509 workload certificate provider ([#10233](https://github.com/googleapis/google-cloud-go/issues/10233)) ([17a9db7](https://github.com/googleapis/google-cloud-go/commit/17a9db73af35e3d1a7a25ac4fd1377a103de6150))
|
||||
|
||||
## [0.4.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.4.1...auth/v0.4.2) (2024-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Enable client certificates by default only for GDU ([#10151](https://github.com/googleapis/google-cloud-go/issues/10151)) ([7c52978](https://github.com/googleapis/google-cloud-go/commit/7c529786275a39b7e00525f7d5e7be0d963e9e15))
|
||||
* **auth:** Handle non-Transport DefaultTransport ([#10162](https://github.com/googleapis/google-cloud-go/issues/10162)) ([fa3bfdb](https://github.com/googleapis/google-cloud-go/commit/fa3bfdb23aaa45b34394a8b61e753b3587506782)), refs [#10159](https://github.com/googleapis/google-cloud-go/issues/10159)
|
||||
* **auth:** Have refresh time match docs ([#10147](https://github.com/googleapis/google-cloud-go/issues/10147)) ([bcb5568](https://github.com/googleapis/google-cloud-go/commit/bcb5568c07a54dd3d2e869d15f502b0741a609e8))
|
||||
* **auth:** Update compute token fetching error with named prefix ([#10180](https://github.com/googleapis/google-cloud-go/issues/10180)) ([4573504](https://github.com/googleapis/google-cloud-go/commit/4573504828d2928bebedc875d87650ba227829ea))
|
||||
|
||||
## [0.4.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.4.0...auth/v0.4.1) (2024-05-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Don't try to detect default creds it opt configured ([#10143](https://github.com/googleapis/google-cloud-go/issues/10143)) ([804632e](https://github.com/googleapis/google-cloud-go/commit/804632e7c5b0b85ff522f7951114485e256eb5bc))
|
||||
|
||||
## [0.4.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.3.0...auth/v0.4.0) (2024-05-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** Enable client certificates by default ([#10102](https://github.com/googleapis/google-cloud-go/issues/10102)) ([9013e52](https://github.com/googleapis/google-cloud-go/commit/9013e5200a6ec0f178ed91acb255481ffb073a2c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Get s2a logic up to date ([#10093](https://github.com/googleapis/google-cloud-go/issues/10093)) ([4fe9ae4](https://github.com/googleapis/google-cloud-go/commit/4fe9ae4b7101af2a5221d6d6b2e77b479305bb06))
|
||||
|
||||
## [0.3.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.2.2...auth/v0.3.0) (2024-04-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth/httptransport:** Add ability to customize transport ([#10023](https://github.com/googleapis/google-cloud-go/issues/10023)) ([72c7f6b](https://github.com/googleapis/google-cloud-go/commit/72c7f6bbec3136cc7a62788fc7186bc33ef6c3b3)), refs [#9812](https://github.com/googleapis/google-cloud-go/issues/9812) [#9814](https://github.com/googleapis/google-cloud-go/issues/9814)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/credentials:** Error on bad file name if explicitly set ([#10018](https://github.com/googleapis/google-cloud-go/issues/10018)) ([55beaa9](https://github.com/googleapis/google-cloud-go/commit/55beaa993aaf052d8be39766afc6777c3c2a0bdd)), refs [#9809](https://github.com/googleapis/google-cloud-go/issues/9809)
|
||||
|
||||
## [0.2.2](https://github.com/googleapis/google-cloud-go/compare/auth/v0.2.1...auth/v0.2.2) (2024-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Add internal opt to skip validation on transports ([#9999](https://github.com/googleapis/google-cloud-go/issues/9999)) ([9e20ef8](https://github.com/googleapis/google-cloud-go/commit/9e20ef89f6287d6bd03b8697d5898dc43b4a77cf)), refs [#9823](https://github.com/googleapis/google-cloud-go/issues/9823)
|
||||
* **auth:** Set secure flag for gRPC conn pools ([#10002](https://github.com/googleapis/google-cloud-go/issues/10002)) ([14e3956](https://github.com/googleapis/google-cloud-go/commit/14e3956dfd736399731b5ee8d9b178ae085cf7ba)), refs [#9833](https://github.com/googleapis/google-cloud-go/issues/9833)
|
||||
|
||||
## [0.2.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.2.0...auth/v0.2.1) (2024-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** Default gRPC token type to Bearer if not set ([#9800](https://github.com/googleapis/google-cloud-go/issues/9800)) ([5284066](https://github.com/googleapis/google-cloud-go/commit/5284066670b6fe65d79089cfe0199c9660f87fc7))
|
||||
|
||||
## [0.2.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.1.1...auth/v0.2.0) (2024-04-15)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
In the below mentioned commits there were a few large breaking changes since the
|
||||
last release of the module.
|
||||
|
||||
1. The `Credentials` type has been moved to the root of the module as it is
|
||||
becoming the core abstraction for the whole module.
|
||||
2. Because of the above mentioned change many functions that previously
|
||||
returned a `TokenProvider` now return `Credentials`. Similarly, these
|
||||
functions have been renamed to be more specific.
|
||||
3. Most places that used to take an optional `TokenProvider` now accept
|
||||
`Credentials`. You can make a `Credentials` from a `TokenProvider` using the
|
||||
constructor found in the `auth` package.
|
||||
4. The `detect` package has been renamed to `credentials`. With this change some
|
||||
function signatures were also updated for better readability.
|
||||
5. Derivative auth flows like `impersonate` and `downscope` have been moved to
|
||||
be under the new `credentials` package.
|
||||
|
||||
Although these changes are disruptive we think that they are for the best of the
|
||||
long-term health of the module. We do not expect any more large breaking changes
|
||||
like these in future revisions, even before 1.0.0. This version will be the
|
||||
first version of the auth library that our client libraries start to use and
|
||||
depend on.
|
||||
|
||||
### Features
|
||||
|
||||
* **auth/credentials/externalaccount:** Add default TokenURL ([#9700](https://github.com/googleapis/google-cloud-go/issues/9700)) ([81830e6](https://github.com/googleapis/google-cloud-go/commit/81830e6848ceefd055aa4d08f933d1154455a0f6))
|
||||
* **auth:** Add downscope.Options.UniverseDomain ([#9634](https://github.com/googleapis/google-cloud-go/issues/9634)) ([52cf7d7](https://github.com/googleapis/google-cloud-go/commit/52cf7d780853594291c4e34302d618299d1f5a1d))
|
||||
* **auth:** Add universe domain to grpctransport and httptransport ([#9663](https://github.com/googleapis/google-cloud-go/issues/9663)) ([67d353b](https://github.com/googleapis/google-cloud-go/commit/67d353beefe3b607c08c891876fbd95ab89e5fe3)), refs [#9670](https://github.com/googleapis/google-cloud-go/issues/9670)
|
||||
* **auth:** Add UniverseDomain to DetectOptions ([#9536](https://github.com/googleapis/google-cloud-go/issues/9536)) ([3618d3f](https://github.com/googleapis/google-cloud-go/commit/3618d3f7061615c0e189f376c75abc201203b501))
|
||||
* **auth:** Make package externalaccount public ([#9633](https://github.com/googleapis/google-cloud-go/issues/9633)) ([a0978d8](https://github.com/googleapis/google-cloud-go/commit/a0978d8e96968399940ebd7d092539772bf9caac))
|
||||
* **auth:** Move credentials to base auth package ([#9590](https://github.com/googleapis/google-cloud-go/issues/9590)) ([1a04baf](https://github.com/googleapis/google-cloud-go/commit/1a04bafa83c27342b9308d785645e1e5423ea10d))
|
||||
* **auth:** Refactor public sigs to use Credentials ([#9603](https://github.com/googleapis/google-cloud-go/issues/9603)) ([69cb240](https://github.com/googleapis/google-cloud-go/commit/69cb240c530b1f7173a9af2555c19e9a1beb56c5))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/oauth2adapt:** Update protobuf dep to v1.33.0 ([30b038d](https://github.com/googleapis/google-cloud-go/commit/30b038d8cac0b8cd5dd4761c87f3f298760dd33a))
|
||||
* **auth:** Fix uint32 conversion ([9221c7f](https://github.com/googleapis/google-cloud-go/commit/9221c7fa12cef9d5fb7ddc92f41f1d6204971c7b))
|
||||
* **auth:** Port sts expires fix ([#9618](https://github.com/googleapis/google-cloud-go/issues/9618)) ([7bec97b](https://github.com/googleapis/google-cloud-go/commit/7bec97b2f51ed3ac4f9b88bf100d301da3f5d1bd))
|
||||
* **auth:** Read universe_domain from all credentials files ([#9632](https://github.com/googleapis/google-cloud-go/issues/9632)) ([16efbb5](https://github.com/googleapis/google-cloud-go/commit/16efbb52e39ea4a319e5ee1e95c0e0305b6d9824))
|
||||
* **auth:** Remove content-type header from idms get requests ([#9508](https://github.com/googleapis/google-cloud-go/issues/9508)) ([8589f41](https://github.com/googleapis/google-cloud-go/commit/8589f41599d265d7c3d46a3d86c9fab2329cbdd9))
|
||||
* **auth:** Update protobuf dep to v1.33.0 ([30b038d](https://github.com/googleapis/google-cloud-go/commit/30b038d8cac0b8cd5dd4761c87f3f298760dd33a))
|
||||
|
||||
## [0.1.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.1.0...auth/v0.1.1) (2024-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/impersonate:** Properly send default detect params ([#9529](https://github.com/googleapis/google-cloud-go/issues/9529)) ([5b6b8be](https://github.com/googleapis/google-cloud-go/commit/5b6b8bef577f82707e51f5cc5d258d5bdf90218f)), refs [#9136](https://github.com/googleapis/google-cloud-go/issues/9136)
|
||||
* **auth:** Update grpc-go to v1.56.3 ([343cea8](https://github.com/googleapis/google-cloud-go/commit/343cea8c43b1e31ae21ad50ad31d3b0b60143f8c))
|
||||
* **auth:** Update grpc-go to v1.59.0 ([81a97b0](https://github.com/googleapis/google-cloud-go/commit/81a97b06cb28b25432e4ece595c55a9857e960b7))
|
||||
|
||||
## 0.1.0 (2023-10-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** Add base auth package ([#8465](https://github.com/googleapis/google-cloud-go/issues/8465)) ([6a45f26](https://github.com/googleapis/google-cloud-go/commit/6a45f26b809b64edae21f312c18d4205f96b180e))
|
||||
* **auth:** Add cert support to httptransport ([#8569](https://github.com/googleapis/google-cloud-go/issues/8569)) ([37e3435](https://github.com/googleapis/google-cloud-go/commit/37e3435f8e98595eafab481bdfcb31a4c56fa993))
|
||||
* **auth:** Add Credentials.UniverseDomain() ([#8654](https://github.com/googleapis/google-cloud-go/issues/8654)) ([af0aa1e](https://github.com/googleapis/google-cloud-go/commit/af0aa1ed8015bc8fe0dd87a7549ae029107cbdb8))
|
||||
* **auth:** Add detect package ([#8491](https://github.com/googleapis/google-cloud-go/issues/8491)) ([d977419](https://github.com/googleapis/google-cloud-go/commit/d977419a3269f6acc193df77a2136a6eb4b4add7))
|
||||
* **auth:** Add downscope package ([#8532](https://github.com/googleapis/google-cloud-go/issues/8532)) ([dda9bff](https://github.com/googleapis/google-cloud-go/commit/dda9bff8ec70e6d104901b4105d13dcaa4e2404c))
|
||||
* **auth:** Add grpctransport package ([#8625](https://github.com/googleapis/google-cloud-go/issues/8625)) ([69a8347](https://github.com/googleapis/google-cloud-go/commit/69a83470bdcc7ed10c6c36d1abc3b7cfdb8a0ee5))
|
||||
* **auth:** Add httptransport package ([#8567](https://github.com/googleapis/google-cloud-go/issues/8567)) ([6898597](https://github.com/googleapis/google-cloud-go/commit/6898597d2ea95d630fcd00fd15c58c75ea843bff))
|
||||
* **auth:** Add idtoken package ([#8580](https://github.com/googleapis/google-cloud-go/issues/8580)) ([a79e693](https://github.com/googleapis/google-cloud-go/commit/a79e693e97e4e3e1c6742099af3dbc58866d88fe))
|
||||
* **auth:** Add impersonate package ([#8578](https://github.com/googleapis/google-cloud-go/issues/8578)) ([e29ba0c](https://github.com/googleapis/google-cloud-go/commit/e29ba0cb7bd3888ab9e808087027dc5a32474c04))
|
||||
* **auth:** Add support for external accounts in detect ([#8508](https://github.com/googleapis/google-cloud-go/issues/8508)) ([62210d5](https://github.com/googleapis/google-cloud-go/commit/62210d5d3e56e8e9f35db8e6ac0defec19582507))
|
||||
* **auth:** Port external account changes ([#8697](https://github.com/googleapis/google-cloud-go/issues/8697)) ([5823db5](https://github.com/googleapis/google-cloud-go/commit/5823db5d633069999b58b9131a7f9cd77e82c899))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/oauth2adapt:** Update golang.org/x/net to v0.17.0 ([174da47](https://github.com/googleapis/google-cloud-go/commit/174da47254fefb12921bbfc65b7829a453af6f5d))
|
||||
* **auth:** Update golang.org/x/net to v0.17.0 ([174da47](https://github.com/googleapis/google-cloud-go/commit/174da47254fefb12921bbfc65b7829a453af6f5d))
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# Google Auth Library for Go
|
||||
|
||||
[](https://pkg.go.dev/cloud.google.com/go/auth)
|
||||
|
||||
## Install
|
||||
|
||||
``` bash
|
||||
go get cloud.google.com/go/auth@latest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The most common way this library is used is transitively, by default, from any
|
||||
of our Go client libraries.
|
||||
|
||||
### Notable use-cases
|
||||
|
||||
- To create a credential directly please see examples in the
|
||||
[credentials](https://pkg.go.dev/cloud.google.com/go/auth/credentials)
|
||||
package.
|
||||
- To create a authenticated HTTP client please see examples in the
|
||||
[httptransport](https://pkg.go.dev/cloud.google.com/go/auth/httptransport)
|
||||
package.
|
||||
- To create a authenticated gRPC connection please see examples in the
|
||||
[grpctransport](https://pkg.go.dev/cloud.google.com/go/auth/grpctransport)
|
||||
package.
|
||||
- To create an ID token please see examples in the
|
||||
[idtoken](https://pkg.go.dev/cloud.google.com/go/auth/credentials/idtoken)
|
||||
package.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome. Please, see the
|
||||
[CONTRIBUTING](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md)
|
||||
document for details.
|
||||
|
||||
Please note that this project is released with a Contributor Code of Conduct.
|
||||
By participating in this project you agree to abide by its terms.
|
||||
See [Contributor Code of Conduct](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md#contributor-code-of-conduct)
|
||||
for more information.
|
||||
+618
@@ -0,0 +1,618 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package auth provides utilities for managing Google Cloud credentials,
|
||||
// including functionality for creating, caching, and refreshing OAuth2 tokens.
|
||||
// It offers customizable options for different OAuth2 flows, such as 2-legged
|
||||
// (2LO) and 3-legged (3LO) OAuth, along with support for PKCE and automatic
|
||||
// token management.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/jwt"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
)
|
||||
|
||||
const (
|
||||
// Parameter keys for AuthCodeURL method to support PKCE.
|
||||
codeChallengeKey = "code_challenge"
|
||||
codeChallengeMethodKey = "code_challenge_method"
|
||||
|
||||
// Parameter key for Exchange method to support PKCE.
|
||||
codeVerifierKey = "code_verifier"
|
||||
|
||||
// 3 minutes and 45 seconds before expiration. The shortest MDS cache is 4 minutes,
|
||||
// so we give it 15 seconds to refresh it's cache before attempting to refresh a token.
|
||||
defaultExpiryDelta = 225 * time.Second
|
||||
|
||||
universeDomainDefault = "googleapis.com"
|
||||
)
|
||||
|
||||
// tokenState represents different states for a [Token].
|
||||
type tokenState int
|
||||
|
||||
const (
|
||||
// fresh indicates that the [Token] is valid. It is not expired or close to
|
||||
// expired, or the token has no expiry.
|
||||
fresh tokenState = iota
|
||||
// stale indicates that the [Token] is close to expired, and should be
|
||||
// refreshed. The token can be used normally.
|
||||
stale
|
||||
// invalid indicates that the [Token] is expired or invalid. The token
|
||||
// cannot be used for a normal operation.
|
||||
invalid
|
||||
)
|
||||
|
||||
var (
|
||||
defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
||||
defaultHeader = &jwt.Header{Algorithm: jwt.HeaderAlgRSA256, Type: jwt.HeaderType}
|
||||
|
||||
// for testing
|
||||
timeNow = time.Now
|
||||
)
|
||||
|
||||
// TokenProvider specifies an interface for anything that can return a token.
|
||||
type TokenProvider interface {
|
||||
// Token returns a Token or an error.
|
||||
// The Token returned must be safe to use
|
||||
// concurrently.
|
||||
// The returned Token must not be modified.
|
||||
// The context provided must be sent along to any requests that are made in
|
||||
// the implementing code.
|
||||
Token(context.Context) (*Token, error)
|
||||
}
|
||||
|
||||
// Token holds the credential token used to authorized requests. All fields are
|
||||
// considered read-only.
|
||||
type Token struct {
|
||||
// Value is the token used to authorize requests. It is usually an access
|
||||
// token but may be other types of tokens such as ID tokens in some flows.
|
||||
Value string
|
||||
// Type is the type of token Value is. If uninitialized, it should be
|
||||
// assumed to be a "Bearer" token.
|
||||
Type string
|
||||
// Expiry is the time the token is set to expire.
|
||||
Expiry time.Time
|
||||
// Metadata may include, but is not limited to, the body of the token
|
||||
// response returned by the server.
|
||||
Metadata map[string]interface{} // TODO(codyoss): maybe make a method to flatten metadata to avoid []string for url.Values
|
||||
}
|
||||
|
||||
// IsValid reports that a [Token] is non-nil, has a [Token.Value], and has not
|
||||
// expired. A token is considered expired if [Token.Expiry] has passed or will
|
||||
// pass in the next 225 seconds.
|
||||
func (t *Token) IsValid() bool {
|
||||
return t.isValidWithEarlyExpiry(defaultExpiryDelta)
|
||||
}
|
||||
|
||||
// MetadataString is a convenience method for accessing string values in the
|
||||
// token's metadata. Returns an empty string if the metadata is nil or the value
|
||||
// for the given key cannot be cast to a string.
|
||||
func (t *Token) MetadataString(k string) string {
|
||||
if t.Metadata == nil {
|
||||
return ""
|
||||
}
|
||||
s, ok := t.Metadata[k].(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *Token) isValidWithEarlyExpiry(earlyExpiry time.Duration) bool {
|
||||
if t.isEmpty() {
|
||||
return false
|
||||
}
|
||||
if t.Expiry.IsZero() {
|
||||
return true
|
||||
}
|
||||
return !t.Expiry.Round(0).Add(-earlyExpiry).Before(timeNow())
|
||||
}
|
||||
|
||||
func (t *Token) isEmpty() bool {
|
||||
return t == nil || t.Value == ""
|
||||
}
|
||||
|
||||
// Credentials holds Google credentials, including
|
||||
// [Application Default Credentials].
|
||||
//
|
||||
// [Application Default Credentials]: https://developers.google.com/accounts/docs/application-default-credentials
|
||||
type Credentials struct {
|
||||
json []byte
|
||||
projectID CredentialsPropertyProvider
|
||||
quotaProjectID CredentialsPropertyProvider
|
||||
// universeDomain is the default service domain for a given Cloud universe.
|
||||
universeDomain CredentialsPropertyProvider
|
||||
|
||||
TokenProvider
|
||||
}
|
||||
|
||||
// JSON returns the bytes associated with the the file used to source
|
||||
// credentials if one was used.
|
||||
func (c *Credentials) JSON() []byte {
|
||||
return c.json
|
||||
}
|
||||
|
||||
// ProjectID returns the associated project ID from the underlying file or
|
||||
// environment.
|
||||
func (c *Credentials) ProjectID(ctx context.Context) (string, error) {
|
||||
if c.projectID == nil {
|
||||
return internal.GetProjectID(c.json, ""), nil
|
||||
}
|
||||
v, err := c.projectID.GetProperty(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return internal.GetProjectID(c.json, v), nil
|
||||
}
|
||||
|
||||
// QuotaProjectID returns the associated quota project ID from the underlying
|
||||
// file or environment.
|
||||
func (c *Credentials) QuotaProjectID(ctx context.Context) (string, error) {
|
||||
if c.quotaProjectID == nil {
|
||||
return internal.GetQuotaProject(c.json, ""), nil
|
||||
}
|
||||
v, err := c.quotaProjectID.GetProperty(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return internal.GetQuotaProject(c.json, v), nil
|
||||
}
|
||||
|
||||
// UniverseDomain returns the default service domain for a given Cloud universe.
|
||||
// The default value is "googleapis.com".
|
||||
func (c *Credentials) UniverseDomain(ctx context.Context) (string, error) {
|
||||
if c.universeDomain == nil {
|
||||
return universeDomainDefault, nil
|
||||
}
|
||||
v, err := c.universeDomain.GetProperty(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if v == "" {
|
||||
return universeDomainDefault, nil
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
// CredentialsPropertyProvider provides an implementation to fetch a property
|
||||
// value for [Credentials].
|
||||
type CredentialsPropertyProvider interface {
|
||||
GetProperty(context.Context) (string, error)
|
||||
}
|
||||
|
||||
// CredentialsPropertyFunc is a type adapter to allow the use of ordinary
|
||||
// functions as a [CredentialsPropertyProvider].
|
||||
type CredentialsPropertyFunc func(context.Context) (string, error)
|
||||
|
||||
// GetProperty loads the properly value provided the given context.
|
||||
func (p CredentialsPropertyFunc) GetProperty(ctx context.Context) (string, error) {
|
||||
return p(ctx)
|
||||
}
|
||||
|
||||
// CredentialsOptions are used to configure [Credentials].
|
||||
type CredentialsOptions struct {
|
||||
// TokenProvider is a means of sourcing a token for the credentials. Required.
|
||||
TokenProvider TokenProvider
|
||||
// JSON is the raw contents of the credentials file if sourced from a file.
|
||||
JSON []byte
|
||||
// ProjectIDProvider resolves the project ID associated with the
|
||||
// credentials.
|
||||
ProjectIDProvider CredentialsPropertyProvider
|
||||
// QuotaProjectIDProvider resolves the quota project ID associated with the
|
||||
// credentials.
|
||||
QuotaProjectIDProvider CredentialsPropertyProvider
|
||||
// UniverseDomainProvider resolves the universe domain with the credentials.
|
||||
UniverseDomainProvider CredentialsPropertyProvider
|
||||
}
|
||||
|
||||
// NewCredentials returns new [Credentials] from the provided options.
|
||||
func NewCredentials(opts *CredentialsOptions) *Credentials {
|
||||
creds := &Credentials{
|
||||
TokenProvider: opts.TokenProvider,
|
||||
json: opts.JSON,
|
||||
projectID: opts.ProjectIDProvider,
|
||||
quotaProjectID: opts.QuotaProjectIDProvider,
|
||||
universeDomain: opts.UniverseDomainProvider,
|
||||
}
|
||||
|
||||
return creds
|
||||
}
|
||||
|
||||
// CachedTokenProviderOptions provides options for configuring a cached
|
||||
// [TokenProvider].
|
||||
type CachedTokenProviderOptions struct {
|
||||
// DisableAutoRefresh makes the TokenProvider always return the same token,
|
||||
// even if it is expired. The default is false. Optional.
|
||||
DisableAutoRefresh bool
|
||||
// ExpireEarly configures the amount of time before a token expires, that it
|
||||
// should be refreshed. If unset, the default value is 3 minutes and 45
|
||||
// seconds. Optional.
|
||||
ExpireEarly time.Duration
|
||||
// DisableAsyncRefresh configures a synchronous workflow that refreshes
|
||||
// tokens in a blocking manner. The default is false. Optional.
|
||||
DisableAsyncRefresh bool
|
||||
}
|
||||
|
||||
func (ctpo *CachedTokenProviderOptions) autoRefresh() bool {
|
||||
if ctpo == nil {
|
||||
return true
|
||||
}
|
||||
return !ctpo.DisableAutoRefresh
|
||||
}
|
||||
|
||||
func (ctpo *CachedTokenProviderOptions) expireEarly() time.Duration {
|
||||
if ctpo == nil || ctpo.ExpireEarly == 0 {
|
||||
return defaultExpiryDelta
|
||||
}
|
||||
return ctpo.ExpireEarly
|
||||
}
|
||||
|
||||
func (ctpo *CachedTokenProviderOptions) blockingRefresh() bool {
|
||||
if ctpo == nil {
|
||||
return false
|
||||
}
|
||||
return ctpo.DisableAsyncRefresh
|
||||
}
|
||||
|
||||
// NewCachedTokenProvider wraps a [TokenProvider] to cache the tokens returned
|
||||
// by the underlying provider. By default it will refresh tokens asynchronously
|
||||
// a few minutes before they expire.
|
||||
func NewCachedTokenProvider(tp TokenProvider, opts *CachedTokenProviderOptions) TokenProvider {
|
||||
if ctp, ok := tp.(*cachedTokenProvider); ok {
|
||||
return ctp
|
||||
}
|
||||
return &cachedTokenProvider{
|
||||
tp: tp,
|
||||
autoRefresh: opts.autoRefresh(),
|
||||
expireEarly: opts.expireEarly(),
|
||||
blockingRefresh: opts.blockingRefresh(),
|
||||
}
|
||||
}
|
||||
|
||||
type cachedTokenProvider struct {
|
||||
tp TokenProvider
|
||||
autoRefresh bool
|
||||
expireEarly time.Duration
|
||||
blockingRefresh bool
|
||||
|
||||
mu sync.Mutex
|
||||
cachedToken *Token
|
||||
// isRefreshRunning ensures that the non-blocking refresh will only be
|
||||
// attempted once, even if multiple callers enter the Token method.
|
||||
isRefreshRunning bool
|
||||
// isRefreshErr ensures that the non-blocking refresh will only be attempted
|
||||
// once per refresh window if an error is encountered.
|
||||
isRefreshErr bool
|
||||
}
|
||||
|
||||
func (c *cachedTokenProvider) Token(ctx context.Context) (*Token, error) {
|
||||
if c.blockingRefresh {
|
||||
return c.tokenBlocking(ctx)
|
||||
}
|
||||
return c.tokenNonBlocking(ctx)
|
||||
}
|
||||
|
||||
func (c *cachedTokenProvider) tokenNonBlocking(ctx context.Context) (*Token, error) {
|
||||
switch c.tokenState() {
|
||||
case fresh:
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.cachedToken, nil
|
||||
case stale:
|
||||
// Call tokenAsync with a new Context because the user-provided context
|
||||
// may have a short timeout incompatible with async token refresh.
|
||||
c.tokenAsync(context.Background())
|
||||
// Return the stale token immediately to not block customer requests to Cloud services.
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.cachedToken, nil
|
||||
default: // invalid
|
||||
return c.tokenBlocking(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// tokenState reports the token's validity.
|
||||
func (c *cachedTokenProvider) tokenState() tokenState {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
t := c.cachedToken
|
||||
now := timeNow()
|
||||
if t == nil || t.Value == "" {
|
||||
return invalid
|
||||
} else if t.Expiry.IsZero() {
|
||||
return fresh
|
||||
} else if now.After(t.Expiry.Round(0)) {
|
||||
return invalid
|
||||
} else if now.After(t.Expiry.Round(0).Add(-c.expireEarly)) {
|
||||
return stale
|
||||
}
|
||||
return fresh
|
||||
}
|
||||
|
||||
// tokenAsync uses a bool to ensure that only one non-blocking token refresh
|
||||
// happens at a time, even if multiple callers have entered this function
|
||||
// concurrently. This avoids creating an arbitrary number of concurrent
|
||||
// goroutines. Retries should be attempted and managed within the Token method.
|
||||
// If the refresh attempt fails, no further attempts are made until the refresh
|
||||
// window expires and the token enters the invalid state, at which point the
|
||||
// blocking call to Token should likely return the same error on the main goroutine.
|
||||
func (c *cachedTokenProvider) tokenAsync(ctx context.Context) {
|
||||
fn := func() {
|
||||
c.mu.Lock()
|
||||
c.isRefreshRunning = true
|
||||
c.mu.Unlock()
|
||||
t, err := c.tp.Token(ctx)
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.isRefreshRunning = false
|
||||
if err != nil {
|
||||
// Discard errors from the non-blocking refresh, but prevent further
|
||||
// attempts.
|
||||
c.isRefreshErr = true
|
||||
return
|
||||
}
|
||||
c.cachedToken = t
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !c.isRefreshRunning && !c.isRefreshErr {
|
||||
go fn()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cachedTokenProvider) tokenBlocking(ctx context.Context) (*Token, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.isRefreshErr = false
|
||||
if c.cachedToken.IsValid() || (!c.autoRefresh && !c.cachedToken.isEmpty()) {
|
||||
return c.cachedToken, nil
|
||||
}
|
||||
t, err := c.tp.Token(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.cachedToken = t
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Error is a error associated with retrieving a [Token]. It can hold useful
|
||||
// additional details for debugging.
|
||||
type Error struct {
|
||||
// Response is the HTTP response associated with error. The body will always
|
||||
// be already closed and consumed.
|
||||
Response *http.Response
|
||||
// Body is the HTTP response body.
|
||||
Body []byte
|
||||
// Err is the underlying wrapped error.
|
||||
Err error
|
||||
|
||||
// code returned in the token response
|
||||
code string
|
||||
// description returned in the token response
|
||||
description string
|
||||
// uri returned in the token response
|
||||
uri string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if e.code != "" {
|
||||
s := fmt.Sprintf("auth: %q", e.code)
|
||||
if e.description != "" {
|
||||
s += fmt.Sprintf(" %q", e.description)
|
||||
}
|
||||
if e.uri != "" {
|
||||
s += fmt.Sprintf(" %q", e.uri)
|
||||
}
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("auth: cannot fetch token: %v\nResponse: %s", e.Response.StatusCode, e.Body)
|
||||
}
|
||||
|
||||
// Temporary returns true if the error is considered temporary and may be able
|
||||
// to be retried.
|
||||
func (e *Error) Temporary() bool {
|
||||
if e.Response == nil {
|
||||
return false
|
||||
}
|
||||
sc := e.Response.StatusCode
|
||||
return sc == http.StatusInternalServerError || sc == http.StatusServiceUnavailable || sc == http.StatusRequestTimeout || sc == http.StatusTooManyRequests
|
||||
}
|
||||
|
||||
func (e *Error) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// Style describes how the token endpoint wants to receive the ClientID and
|
||||
// ClientSecret.
|
||||
type Style int
|
||||
|
||||
const (
|
||||
// StyleUnknown means the value has not been initiated. Sending this in
|
||||
// a request will cause the token exchange to fail.
|
||||
StyleUnknown Style = iota
|
||||
// StyleInParams sends client info in the body of a POST request.
|
||||
StyleInParams
|
||||
// StyleInHeader sends client info using Basic Authorization header.
|
||||
StyleInHeader
|
||||
)
|
||||
|
||||
// Options2LO is the configuration settings for doing a 2-legged JWT OAuth2 flow.
|
||||
type Options2LO struct {
|
||||
// Email is the OAuth2 client ID. This value is set as the "iss" in the
|
||||
// JWT.
|
||||
Email string
|
||||
// PrivateKey contains the contents of an RSA private key or the
|
||||
// contents of a PEM file that contains a private key. It is used to sign
|
||||
// the JWT created.
|
||||
PrivateKey []byte
|
||||
// TokenURL is th URL the JWT is sent to. Required.
|
||||
TokenURL string
|
||||
// PrivateKeyID is the ID of the key used to sign the JWT. It is used as the
|
||||
// "kid" in the JWT header. Optional.
|
||||
PrivateKeyID string
|
||||
// Subject is the used for to impersonate a user. It is used as the "sub" in
|
||||
// the JWT.m Optional.
|
||||
Subject string
|
||||
// Scopes specifies requested permissions for the token. Optional.
|
||||
Scopes []string
|
||||
// Expires specifies the lifetime of the token. Optional.
|
||||
Expires time.Duration
|
||||
// Audience specifies the "aud" in the JWT. Optional.
|
||||
Audience string
|
||||
// PrivateClaims allows specifying any custom claims for the JWT. Optional.
|
||||
PrivateClaims map[string]interface{}
|
||||
|
||||
// Client is the client to be used to make the underlying token requests.
|
||||
// Optional.
|
||||
Client *http.Client
|
||||
// UseIDToken requests that the token returned be an ID token if one is
|
||||
// returned from the server. Optional.
|
||||
UseIDToken bool
|
||||
// Logger is used for debug logging. If provided, logging will be enabled
|
||||
// at the loggers configured level. By default logging is disabled unless
|
||||
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
|
||||
// logger will be used. Optional.
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
func (o *Options2LO) client() *http.Client {
|
||||
if o.Client != nil {
|
||||
return o.Client
|
||||
}
|
||||
return internal.DefaultClient()
|
||||
}
|
||||
|
||||
func (o *Options2LO) validate() error {
|
||||
if o == nil {
|
||||
return errors.New("auth: options must be provided")
|
||||
}
|
||||
if o.Email == "" {
|
||||
return errors.New("auth: email must be provided")
|
||||
}
|
||||
if len(o.PrivateKey) == 0 {
|
||||
return errors.New("auth: private key must be provided")
|
||||
}
|
||||
if o.TokenURL == "" {
|
||||
return errors.New("auth: token URL must be provided")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// New2LOTokenProvider returns a [TokenProvider] from the provided options.
|
||||
func New2LOTokenProvider(opts *Options2LO) (TokenProvider, error) {
|
||||
if err := opts.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tokenProvider2LO{opts: opts, Client: opts.client(), logger: internallog.New(opts.Logger)}, nil
|
||||
}
|
||||
|
||||
type tokenProvider2LO struct {
|
||||
opts *Options2LO
|
||||
Client *http.Client
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (tp tokenProvider2LO) Token(ctx context.Context) (*Token, error) {
|
||||
pk, err := internal.ParseKey(tp.opts.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claimSet := &jwt.Claims{
|
||||
Iss: tp.opts.Email,
|
||||
Scope: strings.Join(tp.opts.Scopes, " "),
|
||||
Aud: tp.opts.TokenURL,
|
||||
AdditionalClaims: tp.opts.PrivateClaims,
|
||||
Sub: tp.opts.Subject,
|
||||
}
|
||||
if t := tp.opts.Expires; t > 0 {
|
||||
claimSet.Exp = time.Now().Add(t).Unix()
|
||||
}
|
||||
if aud := tp.opts.Audience; aud != "" {
|
||||
claimSet.Aud = aud
|
||||
}
|
||||
h := *defaultHeader
|
||||
h.KeyID = tp.opts.PrivateKeyID
|
||||
payload, err := jwt.EncodeJWS(&h, claimSet, pk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := url.Values{}
|
||||
v.Set("grant_type", defaultGrantType)
|
||||
v.Set("assertion", payload)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", tp.opts.TokenURL, strings.NewReader(v.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
tp.logger.DebugContext(ctx, "2LO token request", "request", internallog.HTTPRequest(req, []byte(v.Encode())))
|
||||
resp, body, err := internal.DoRequest(tp.Client, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
|
||||
}
|
||||
tp.logger.DebugContext(ctx, "2LO token response", "response", internallog.HTTPResponse(resp, body))
|
||||
if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
|
||||
return nil, &Error{
|
||||
Response: resp,
|
||||
Body: body,
|
||||
}
|
||||
}
|
||||
// tokenRes is the JSON response body.
|
||||
var tokenRes struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
IDToken string `json:"id_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &tokenRes); err != nil {
|
||||
return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
|
||||
}
|
||||
token := &Token{
|
||||
Value: tokenRes.AccessToken,
|
||||
Type: tokenRes.TokenType,
|
||||
}
|
||||
token.Metadata = make(map[string]interface{})
|
||||
json.Unmarshal(body, &token.Metadata) // no error checks for optional fields
|
||||
|
||||
if secs := tokenRes.ExpiresIn; secs > 0 {
|
||||
token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
|
||||
}
|
||||
if v := tokenRes.IDToken; v != "" {
|
||||
// decode returned id token to get expiry
|
||||
claimSet, err := jwt.DecodeJWS(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auth: error decoding JWT token: %w", err)
|
||||
}
|
||||
token.Expiry = time.Unix(claimSet.Exp, 0)
|
||||
}
|
||||
if tp.opts.UseIDToken {
|
||||
if tokenRes.IDToken == "" {
|
||||
return nil, fmt.Errorf("auth: response doesn't have JWT token")
|
||||
}
|
||||
token.Value = tokenRes.IDToken
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
computeTokenMetadata = map[string]interface{}{
|
||||
"auth.google.tokenSource": "compute-metadata",
|
||||
"auth.google.serviceAccount": "default",
|
||||
}
|
||||
computeTokenURI = "instance/service-accounts/default/token"
|
||||
)
|
||||
|
||||
// computeTokenProvider creates a [cloud.google.com/go/auth.TokenProvider] that
|
||||
// uses the metadata service to retrieve tokens.
|
||||
func computeTokenProvider(opts *DetectOptions, client *metadata.Client) auth.TokenProvider {
|
||||
return auth.NewCachedTokenProvider(&computeProvider{
|
||||
scopes: opts.Scopes,
|
||||
client: client,
|
||||
}, &auth.CachedTokenProviderOptions{
|
||||
ExpireEarly: opts.EarlyTokenRefresh,
|
||||
DisableAsyncRefresh: opts.DisableAsyncRefresh,
|
||||
})
|
||||
}
|
||||
|
||||
// computeProvider fetches tokens from the google cloud metadata service.
|
||||
type computeProvider struct {
|
||||
scopes []string
|
||||
client *metadata.Client
|
||||
}
|
||||
|
||||
type metadataTokenResp struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresInSec int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
func (cs *computeProvider) Token(ctx context.Context) (*auth.Token, error) {
|
||||
tokenURI, err := url.Parse(computeTokenURI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cs.scopes) > 0 {
|
||||
v := url.Values{}
|
||||
v.Set("scopes", strings.Join(cs.scopes, ","))
|
||||
tokenURI.RawQuery = v.Encode()
|
||||
}
|
||||
tokenJSON, err := cs.client.GetWithContext(ctx, tokenURI.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
|
||||
}
|
||||
var res metadataTokenResp
|
||||
if err := json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res); err != nil {
|
||||
return nil, fmt.Errorf("credentials: invalid token JSON from metadata: %w", err)
|
||||
}
|
||||
if res.ExpiresInSec == 0 || res.AccessToken == "" {
|
||||
return nil, errors.New("credentials: incomplete token received from metadata")
|
||||
}
|
||||
return &auth.Token{
|
||||
Value: res.AccessToken,
|
||||
Type: res.TokenType,
|
||||
Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second),
|
||||
Metadata: computeTokenMetadata,
|
||||
}, nil
|
||||
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/credsfile"
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
)
|
||||
|
||||
const (
|
||||
// jwtTokenURL is Google's OAuth 2.0 token URL to use with the JWT(2LO) flow.
|
||||
jwtTokenURL = "https://oauth2.googleapis.com/token"
|
||||
|
||||
// Google's OAuth 2.0 default endpoints.
|
||||
googleAuthURL = "https://accounts.google.com/o/oauth2/auth"
|
||||
googleTokenURL = "https://oauth2.googleapis.com/token"
|
||||
|
||||
// GoogleMTLSTokenURL is Google's default OAuth2.0 mTLS endpoint.
|
||||
GoogleMTLSTokenURL = "https://oauth2.mtls.googleapis.com/token"
|
||||
|
||||
// Help on default credentials
|
||||
adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"
|
||||
)
|
||||
|
||||
var (
|
||||
// for testing
|
||||
allowOnGCECheck = true
|
||||
)
|
||||
|
||||
// OnGCE reports whether this process is running in Google Cloud.
|
||||
func OnGCE() bool {
|
||||
// TODO(codyoss): once all libs use this auth lib move metadata check here
|
||||
return allowOnGCECheck && metadata.OnGCE()
|
||||
}
|
||||
|
||||
// DetectDefault searches for "Application Default Credentials" and returns
|
||||
// a credential based on the [DetectOptions] provided.
|
||||
//
|
||||
// It looks for credentials in the following places, preferring the first
|
||||
// location found:
|
||||
//
|
||||
// - A JSON file whose path is specified by the GOOGLE_APPLICATION_CREDENTIALS
|
||||
// environment variable. For workload identity federation, refer to
|
||||
// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation
|
||||
// on how to generate the JSON configuration file for on-prem/non-Google
|
||||
// cloud platforms.
|
||||
// - A JSON file in a location known to the gcloud command-line tool. On
|
||||
// Windows, this is %APPDATA%/gcloud/application_default_credentials.json. On
|
||||
// other systems, $HOME/.config/gcloud/application_default_credentials.json.
|
||||
// - On Google Compute Engine, Google App Engine standard second generation
|
||||
// runtimes, and Google App Engine flexible environment, it fetches
|
||||
// credentials from the metadata server.
|
||||
func DetectDefault(opts *DetectOptions) (*auth.Credentials, error) {
|
||||
if err := opts.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(opts.CredentialsJSON) > 0 {
|
||||
return readCredentialsFileJSON(opts.CredentialsJSON, opts)
|
||||
}
|
||||
if opts.CredentialsFile != "" {
|
||||
return readCredentialsFile(opts.CredentialsFile, opts)
|
||||
}
|
||||
if filename := os.Getenv(credsfile.GoogleAppCredsEnvVar); filename != "" {
|
||||
creds, err := readCredentialsFile(filename, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
fileName := credsfile.GetWellKnownFileName()
|
||||
if b, err := os.ReadFile(fileName); err == nil {
|
||||
return readCredentialsFileJSON(b, opts)
|
||||
}
|
||||
|
||||
if OnGCE() {
|
||||
metadataClient := metadata.NewWithOptions(&metadata.Options{
|
||||
Logger: opts.logger(),
|
||||
})
|
||||
return auth.NewCredentials(&auth.CredentialsOptions{
|
||||
TokenProvider: computeTokenProvider(opts, metadataClient),
|
||||
ProjectIDProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
|
||||
return metadataClient.ProjectIDWithContext(ctx)
|
||||
}),
|
||||
UniverseDomainProvider: &internal.ComputeUniverseDomainProvider{
|
||||
MetadataClient: metadataClient,
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("credentials: could not find default credentials. See %v for more information", adcSetupURL)
|
||||
}
|
||||
|
||||
// DetectOptions provides configuration for [DetectDefault].
|
||||
type DetectOptions struct {
|
||||
// Scopes that credentials tokens should have. Example:
|
||||
// https://www.googleapis.com/auth/cloud-platform. Required if Audience is
|
||||
// not provided.
|
||||
Scopes []string
|
||||
// Audience that credentials tokens should have. Only applicable for 2LO
|
||||
// flows with service accounts. If specified, scopes should not be provided.
|
||||
Audience string
|
||||
// Subject is the user email used for [domain wide delegation](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority).
|
||||
// Optional.
|
||||
Subject string
|
||||
// EarlyTokenRefresh configures how early before a token expires that it
|
||||
// should be refreshed. Once the token’s time until expiration has entered
|
||||
// this refresh window the token is considered valid but stale. If unset,
|
||||
// the default value is 3 minutes and 45 seconds. Optional.
|
||||
EarlyTokenRefresh time.Duration
|
||||
// DisableAsyncRefresh configures a synchronous workflow that refreshes
|
||||
// stale tokens while blocking. The default is false. Optional.
|
||||
DisableAsyncRefresh bool
|
||||
// AuthHandlerOptions configures an authorization handler and other options
|
||||
// for 3LO flows. It is required, and only used, for client credential
|
||||
// flows.
|
||||
AuthHandlerOptions *auth.AuthorizationHandlerOptions
|
||||
// TokenURL allows to set the token endpoint for user credential flows. If
|
||||
// unset the default value is: https://oauth2.googleapis.com/token.
|
||||
// Optional.
|
||||
TokenURL string
|
||||
// STSAudience is the audience sent to when retrieving an STS token.
|
||||
// Currently this only used for GDCH auth flow, for which it is required.
|
||||
STSAudience string
|
||||
// CredentialsFile overrides detection logic and sources a credential file
|
||||
// from the provided filepath. If provided, CredentialsJSON must not be.
|
||||
// Optional.
|
||||
CredentialsFile string
|
||||
// CredentialsJSON overrides detection logic and uses the JSON bytes as the
|
||||
// source for the credential. If provided, CredentialsFile must not be.
|
||||
// Optional.
|
||||
CredentialsJSON []byte
|
||||
// UseSelfSignedJWT directs service account based credentials to create a
|
||||
// self-signed JWT with the private key found in the file, skipping any
|
||||
// network requests that would normally be made. Optional.
|
||||
UseSelfSignedJWT bool
|
||||
// Client configures the underlying client used to make network requests
|
||||
// when fetching tokens. Optional.
|
||||
Client *http.Client
|
||||
// UniverseDomain is the default service domain for a given Cloud universe.
|
||||
// The default value is "googleapis.com". This option is ignored for
|
||||
// authentication flows that do not support universe domain. Optional.
|
||||
UniverseDomain string
|
||||
// Logger is used for debug logging. If provided, logging will be enabled
|
||||
// at the loggers configured level. By default logging is disabled unless
|
||||
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
|
||||
// logger will be used. Optional.
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
func (o *DetectOptions) validate() error {
|
||||
if o == nil {
|
||||
return errors.New("credentials: options must be provided")
|
||||
}
|
||||
if len(o.Scopes) > 0 && o.Audience != "" {
|
||||
return errors.New("credentials: both scopes and audience were provided")
|
||||
}
|
||||
if len(o.CredentialsJSON) > 0 && o.CredentialsFile != "" {
|
||||
return errors.New("credentials: both credentials file and JSON were provided")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *DetectOptions) tokenURL() string {
|
||||
if o.TokenURL != "" {
|
||||
return o.TokenURL
|
||||
}
|
||||
return googleTokenURL
|
||||
}
|
||||
|
||||
func (o *DetectOptions) scopes() []string {
|
||||
scopes := make([]string, len(o.Scopes))
|
||||
copy(scopes, o.Scopes)
|
||||
return scopes
|
||||
}
|
||||
|
||||
func (o *DetectOptions) client() *http.Client {
|
||||
if o.Client != nil {
|
||||
return o.Client
|
||||
}
|
||||
return internal.DefaultClient()
|
||||
}
|
||||
|
||||
func (o *DetectOptions) logger() *slog.Logger {
|
||||
return internallog.New(o.Logger)
|
||||
}
|
||||
|
||||
func readCredentialsFile(filename string, opts *DetectOptions) (*auth.Credentials, error) {
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return readCredentialsFileJSON(b, opts)
|
||||
}
|
||||
|
||||
func readCredentialsFileJSON(b []byte, opts *DetectOptions) (*auth.Credentials, error) {
|
||||
// attempt to parse jsonData as a Google Developers Console client_credentials.json.
|
||||
config := clientCredConfigFromJSON(b, opts)
|
||||
if config != nil {
|
||||
if config.AuthHandlerOpts == nil {
|
||||
return nil, errors.New("credentials: auth handler must be specified for this credential filetype")
|
||||
}
|
||||
tp, err := auth.New3LOTokenProvider(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return auth.NewCredentials(&auth.CredentialsOptions{
|
||||
TokenProvider: tp,
|
||||
JSON: b,
|
||||
}), nil
|
||||
}
|
||||
return fileCredentials(b, opts)
|
||||
}
|
||||
|
||||
func clientCredConfigFromJSON(b []byte, opts *DetectOptions) *auth.Options3LO {
|
||||
var creds credsfile.ClientCredentialsFile
|
||||
var c *credsfile.Config3LO
|
||||
if err := json.Unmarshal(b, &creds); err != nil {
|
||||
return nil
|
||||
}
|
||||
switch {
|
||||
case creds.Web != nil:
|
||||
c = creds.Web
|
||||
case creds.Installed != nil:
|
||||
c = creds.Installed
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
if len(c.RedirectURIs) < 1 {
|
||||
return nil
|
||||
}
|
||||
var handleOpts *auth.AuthorizationHandlerOptions
|
||||
if opts.AuthHandlerOptions != nil {
|
||||
handleOpts = &auth.AuthorizationHandlerOptions{
|
||||
Handler: opts.AuthHandlerOptions.Handler,
|
||||
State: opts.AuthHandlerOptions.State,
|
||||
PKCEOpts: opts.AuthHandlerOptions.PKCEOpts,
|
||||
}
|
||||
}
|
||||
return &auth.Options3LO{
|
||||
ClientID: c.ClientID,
|
||||
ClientSecret: c.ClientSecret,
|
||||
RedirectURL: c.RedirectURIs[0],
|
||||
Scopes: opts.scopes(),
|
||||
AuthURL: c.AuthURI,
|
||||
TokenURL: c.TokenURI,
|
||||
Client: opts.client(),
|
||||
Logger: opts.logger(),
|
||||
EarlyTokenExpiry: opts.EarlyTokenRefresh,
|
||||
AuthHandlerOpts: handleOpts,
|
||||
// TODO(codyoss): refactor this out. We need to add in auto-detection
|
||||
// for this use case.
|
||||
AuthStyle: auth.StyleInParams,
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package credentials provides support for making OAuth2 authorized and
|
||||
// authenticated HTTP requests to Google APIs. It supports the Web server flow,
|
||||
// client-side credentials, service accounts, Google Compute Engine service
|
||||
// accounts, Google App Engine service accounts and workload identity federation
|
||||
// from non-Google cloud platforms.
|
||||
//
|
||||
// A brief overview of the package follows. For more information, please read
|
||||
// https://developers.google.com/accounts/docs/OAuth2
|
||||
// and
|
||||
// https://developers.google.com/accounts/docs/application-default-credentials.
|
||||
// For more information on using workload identity federation, refer to
|
||||
// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation.
|
||||
//
|
||||
// # Credentials
|
||||
//
|
||||
// The [cloud.google.com/go/auth.Credentials] type represents Google
|
||||
// credentials, including Application Default Credentials.
|
||||
//
|
||||
// Use [DetectDefault] to obtain Application Default Credentials.
|
||||
//
|
||||
// Application Default Credentials support workload identity federation to
|
||||
// access Google Cloud resources from non-Google Cloud platforms including Amazon
|
||||
// Web Services (AWS), Microsoft Azure or any identity provider that supports
|
||||
// OpenID Connect (OIDC). Workload identity federation is recommended for
|
||||
// non-Google Cloud environments as it avoids the need to download, manage, and
|
||||
// store service account private keys locally.
|
||||
//
|
||||
// # Workforce Identity Federation
|
||||
//
|
||||
// For more information on this feature see [cloud.google.com/go/auth/credentials/externalaccount].
|
||||
package credentials
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/credentials/internal/externalaccount"
|
||||
"cloud.google.com/go/auth/credentials/internal/externalaccountuser"
|
||||
"cloud.google.com/go/auth/credentials/internal/gdch"
|
||||
"cloud.google.com/go/auth/credentials/internal/impersonate"
|
||||
internalauth "cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/credsfile"
|
||||
)
|
||||
|
||||
func fileCredentials(b []byte, opts *DetectOptions) (*auth.Credentials, error) {
|
||||
fileType, err := credsfile.ParseFileType(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var projectID, universeDomain string
|
||||
var tp auth.TokenProvider
|
||||
switch fileType {
|
||||
case credsfile.ServiceAccountKey:
|
||||
f, err := credsfile.ParseServiceAccount(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tp, err = handleServiceAccount(f, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projectID = f.ProjectID
|
||||
universeDomain = resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
|
||||
case credsfile.UserCredentialsKey:
|
||||
f, err := credsfile.ParseUserCredentials(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tp, err = handleUserCredential(f, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
universeDomain = f.UniverseDomain
|
||||
case credsfile.ExternalAccountKey:
|
||||
f, err := credsfile.ParseExternalAccount(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tp, err = handleExternalAccount(f, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
universeDomain = resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
|
||||
case credsfile.ExternalAccountAuthorizedUserKey:
|
||||
f, err := credsfile.ParseExternalAccountAuthorizedUser(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tp, err = handleExternalAccountAuthorizedUser(f, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
universeDomain = f.UniverseDomain
|
||||
case credsfile.ImpersonatedServiceAccountKey:
|
||||
f, err := credsfile.ParseImpersonatedServiceAccount(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tp, err = handleImpersonatedServiceAccount(f, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
universeDomain = resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
|
||||
case credsfile.GDCHServiceAccountKey:
|
||||
f, err := credsfile.ParseGDCHServiceAccount(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tp, err = handleGDCHServiceAccount(f, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projectID = f.Project
|
||||
universeDomain = f.UniverseDomain
|
||||
default:
|
||||
return nil, fmt.Errorf("credentials: unsupported filetype %q", fileType)
|
||||
}
|
||||
return auth.NewCredentials(&auth.CredentialsOptions{
|
||||
TokenProvider: auth.NewCachedTokenProvider(tp, &auth.CachedTokenProviderOptions{
|
||||
ExpireEarly: opts.EarlyTokenRefresh,
|
||||
}),
|
||||
JSON: b,
|
||||
ProjectIDProvider: internalauth.StaticCredentialsProperty(projectID),
|
||||
// TODO(codyoss): only set quota project here if there was a user override
|
||||
UniverseDomainProvider: internalauth.StaticCredentialsProperty(universeDomain),
|
||||
}), nil
|
||||
}
|
||||
|
||||
// resolveUniverseDomain returns optsUniverseDomain if non-empty, in order to
|
||||
// support configuring universe-specific credentials in code. Auth flows
|
||||
// unsupported for universe domain should not use this func, but should instead
|
||||
// simply set the file universe domain on the credentials.
|
||||
func resolveUniverseDomain(optsUniverseDomain, fileUniverseDomain string) string {
|
||||
if optsUniverseDomain != "" {
|
||||
return optsUniverseDomain
|
||||
}
|
||||
return fileUniverseDomain
|
||||
}
|
||||
|
||||
func handleServiceAccount(f *credsfile.ServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
|
||||
ud := resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
|
||||
if opts.UseSelfSignedJWT {
|
||||
return configureSelfSignedJWT(f, opts)
|
||||
} else if ud != "" && ud != internalauth.DefaultUniverseDomain {
|
||||
// For non-GDU universe domains, token exchange is impossible and services
|
||||
// must support self-signed JWTs.
|
||||
opts.UseSelfSignedJWT = true
|
||||
return configureSelfSignedJWT(f, opts)
|
||||
}
|
||||
opts2LO := &auth.Options2LO{
|
||||
Email: f.ClientEmail,
|
||||
PrivateKey: []byte(f.PrivateKey),
|
||||
PrivateKeyID: f.PrivateKeyID,
|
||||
Scopes: opts.scopes(),
|
||||
TokenURL: f.TokenURL,
|
||||
Subject: opts.Subject,
|
||||
Client: opts.client(),
|
||||
Logger: opts.logger(),
|
||||
}
|
||||
if opts2LO.TokenURL == "" {
|
||||
opts2LO.TokenURL = jwtTokenURL
|
||||
}
|
||||
return auth.New2LOTokenProvider(opts2LO)
|
||||
}
|
||||
|
||||
func handleUserCredential(f *credsfile.UserCredentialsFile, opts *DetectOptions) (auth.TokenProvider, error) {
|
||||
opts3LO := &auth.Options3LO{
|
||||
ClientID: f.ClientID,
|
||||
ClientSecret: f.ClientSecret,
|
||||
Scopes: opts.scopes(),
|
||||
AuthURL: googleAuthURL,
|
||||
TokenURL: opts.tokenURL(),
|
||||
AuthStyle: auth.StyleInParams,
|
||||
EarlyTokenExpiry: opts.EarlyTokenRefresh,
|
||||
RefreshToken: f.RefreshToken,
|
||||
Client: opts.client(),
|
||||
Logger: opts.logger(),
|
||||
}
|
||||
return auth.New3LOTokenProvider(opts3LO)
|
||||
}
|
||||
|
||||
func handleExternalAccount(f *credsfile.ExternalAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
|
||||
externalOpts := &externalaccount.Options{
|
||||
Audience: f.Audience,
|
||||
SubjectTokenType: f.SubjectTokenType,
|
||||
TokenURL: f.TokenURL,
|
||||
TokenInfoURL: f.TokenInfoURL,
|
||||
ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL,
|
||||
ClientSecret: f.ClientSecret,
|
||||
ClientID: f.ClientID,
|
||||
CredentialSource: f.CredentialSource,
|
||||
QuotaProjectID: f.QuotaProjectID,
|
||||
Scopes: opts.scopes(),
|
||||
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
|
||||
Client: opts.client(),
|
||||
Logger: opts.logger(),
|
||||
IsDefaultClient: opts.Client == nil,
|
||||
}
|
||||
if f.ServiceAccountImpersonation != nil {
|
||||
externalOpts.ServiceAccountImpersonationLifetimeSeconds = f.ServiceAccountImpersonation.TokenLifetimeSeconds
|
||||
}
|
||||
return externalaccount.NewTokenProvider(externalOpts)
|
||||
}
|
||||
|
||||
func handleExternalAccountAuthorizedUser(f *credsfile.ExternalAccountAuthorizedUserFile, opts *DetectOptions) (auth.TokenProvider, error) {
|
||||
externalOpts := &externalaccountuser.Options{
|
||||
Audience: f.Audience,
|
||||
RefreshToken: f.RefreshToken,
|
||||
TokenURL: f.TokenURL,
|
||||
TokenInfoURL: f.TokenInfoURL,
|
||||
ClientID: f.ClientID,
|
||||
ClientSecret: f.ClientSecret,
|
||||
Scopes: opts.scopes(),
|
||||
Client: opts.client(),
|
||||
Logger: opts.logger(),
|
||||
}
|
||||
return externalaccountuser.NewTokenProvider(externalOpts)
|
||||
}
|
||||
|
||||
func handleImpersonatedServiceAccount(f *credsfile.ImpersonatedServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
|
||||
if f.ServiceAccountImpersonationURL == "" || f.CredSource == nil {
|
||||
return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
|
||||
}
|
||||
|
||||
tp, err := fileCredentials(f.CredSource, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return impersonate.NewTokenProvider(&impersonate.Options{
|
||||
URL: f.ServiceAccountImpersonationURL,
|
||||
Scopes: opts.scopes(),
|
||||
Tp: tp,
|
||||
Delegates: f.Delegates,
|
||||
Client: opts.client(),
|
||||
Logger: opts.logger(),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGDCHServiceAccount(f *credsfile.GDCHServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
|
||||
return gdch.NewTokenProvider(f, &gdch.Options{
|
||||
STSAudience: opts.STSAudience,
|
||||
Client: opts.client(),
|
||||
Logger: opts.logger(),
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+531
@@ -0,0 +1,531 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
)
|
||||
|
||||
var (
|
||||
// getenv aliases os.Getenv for testing
|
||||
getenv = os.Getenv
|
||||
)
|
||||
|
||||
const (
|
||||
// AWS Signature Version 4 signing algorithm identifier.
|
||||
awsAlgorithm = "AWS4-HMAC-SHA256"
|
||||
|
||||
// The termination string for the AWS credential scope value as defined in
|
||||
// https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
||||
awsRequestType = "aws4_request"
|
||||
|
||||
// The AWS authorization header name for the security session token if available.
|
||||
awsSecurityTokenHeader = "x-amz-security-token"
|
||||
|
||||
// The name of the header containing the session token for metadata endpoint calls
|
||||
awsIMDSv2SessionTokenHeader = "X-aws-ec2-metadata-token"
|
||||
|
||||
awsIMDSv2SessionTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds"
|
||||
|
||||
awsIMDSv2SessionTTL = "300"
|
||||
|
||||
// The AWS authorization header name for the auto-generated date.
|
||||
awsDateHeader = "x-amz-date"
|
||||
|
||||
defaultRegionalCredentialVerificationURL = "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
|
||||
|
||||
// Supported AWS configuration environment variables.
|
||||
awsAccessKeyIDEnvVar = "AWS_ACCESS_KEY_ID"
|
||||
awsDefaultRegionEnvVar = "AWS_DEFAULT_REGION"
|
||||
awsRegionEnvVar = "AWS_REGION"
|
||||
awsSecretAccessKeyEnvVar = "AWS_SECRET_ACCESS_KEY"
|
||||
awsSessionTokenEnvVar = "AWS_SESSION_TOKEN"
|
||||
|
||||
awsTimeFormatLong = "20060102T150405Z"
|
||||
awsTimeFormatShort = "20060102"
|
||||
awsProviderType = "aws"
|
||||
)
|
||||
|
||||
type awsSubjectProvider struct {
|
||||
EnvironmentID string
|
||||
RegionURL string
|
||||
RegionalCredVerificationURL string
|
||||
CredVerificationURL string
|
||||
IMDSv2SessionTokenURL string
|
||||
TargetResource string
|
||||
requestSigner *awsRequestSigner
|
||||
region string
|
||||
securityCredentialsProvider AwsSecurityCredentialsProvider
|
||||
reqOpts *RequestOptions
|
||||
|
||||
Client *http.Client
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (sp *awsSubjectProvider) subjectToken(ctx context.Context) (string, error) {
|
||||
// Set Defaults
|
||||
if sp.RegionalCredVerificationURL == "" {
|
||||
sp.RegionalCredVerificationURL = defaultRegionalCredentialVerificationURL
|
||||
}
|
||||
headers := make(map[string]string)
|
||||
if sp.shouldUseMetadataServer() {
|
||||
awsSessionToken, err := sp.getAWSSessionToken(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if awsSessionToken != "" {
|
||||
headers[awsIMDSv2SessionTokenHeader] = awsSessionToken
|
||||
}
|
||||
}
|
||||
|
||||
awsSecurityCredentials, err := sp.getSecurityCredentials(ctx, headers)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if sp.region, err = sp.getRegion(ctx, headers); err != nil {
|
||||
return "", err
|
||||
}
|
||||
sp.requestSigner = &awsRequestSigner{
|
||||
RegionName: sp.region,
|
||||
AwsSecurityCredentials: awsSecurityCredentials,
|
||||
}
|
||||
|
||||
// Generate the signed request to AWS STS GetCallerIdentity API.
|
||||
// Use the required regional endpoint. Otherwise, the request will fail.
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", strings.Replace(sp.RegionalCredVerificationURL, "{region}", sp.region, 1), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// The full, canonical resource name of the workload identity pool
|
||||
// provider, with or without the HTTPS prefix.
|
||||
// Including this header as part of the signature is recommended to
|
||||
// ensure data integrity.
|
||||
if sp.TargetResource != "" {
|
||||
req.Header.Set("x-goog-cloud-target-resource", sp.TargetResource)
|
||||
}
|
||||
sp.requestSigner.signRequest(req)
|
||||
|
||||
/*
|
||||
The GCP STS endpoint expects the headers to be formatted as:
|
||||
# [
|
||||
# {key: 'x-amz-date', value: '...'},
|
||||
# {key: 'Authorization', value: '...'},
|
||||
# ...
|
||||
# ]
|
||||
# And then serialized as:
|
||||
# quote(json.dumps({
|
||||
# url: '...',
|
||||
# method: 'POST',
|
||||
# headers: [{key: 'x-amz-date', value: '...'}, ...]
|
||||
# }))
|
||||
*/
|
||||
|
||||
awsSignedReq := awsRequest{
|
||||
URL: req.URL.String(),
|
||||
Method: "POST",
|
||||
}
|
||||
for headerKey, headerList := range req.Header {
|
||||
for _, headerValue := range headerList {
|
||||
awsSignedReq.Headers = append(awsSignedReq.Headers, awsRequestHeader{
|
||||
Key: headerKey,
|
||||
Value: headerValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(awsSignedReq.Headers, func(i, j int) bool {
|
||||
headerCompare := strings.Compare(awsSignedReq.Headers[i].Key, awsSignedReq.Headers[j].Key)
|
||||
if headerCompare == 0 {
|
||||
return strings.Compare(awsSignedReq.Headers[i].Value, awsSignedReq.Headers[j].Value) < 0
|
||||
}
|
||||
return headerCompare < 0
|
||||
})
|
||||
|
||||
result, err := json.Marshal(awsSignedReq)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return url.QueryEscape(string(result)), nil
|
||||
}
|
||||
|
||||
func (sp *awsSubjectProvider) providerType() string {
|
||||
if sp.securityCredentialsProvider != nil {
|
||||
return programmaticProviderType
|
||||
}
|
||||
return awsProviderType
|
||||
}
|
||||
|
||||
func (sp *awsSubjectProvider) getAWSSessionToken(ctx context.Context) (string, error) {
|
||||
if sp.IMDSv2SessionTokenURL == "" {
|
||||
return "", nil
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", sp.IMDSv2SessionTokenURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set(awsIMDSv2SessionTTLHeader, awsIMDSv2SessionTTL)
|
||||
|
||||
sp.logger.DebugContext(ctx, "aws session token request", "request", internallog.HTTPRequest(req, nil))
|
||||
resp, body, err := internal.DoRequest(sp.Client, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sp.logger.DebugContext(ctx, "aws session token response", "response", internallog.HTTPResponse(resp, body))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("credentials: unable to retrieve AWS session token: %s", body)
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (sp *awsSubjectProvider) getRegion(ctx context.Context, headers map[string]string) (string, error) {
|
||||
if sp.securityCredentialsProvider != nil {
|
||||
return sp.securityCredentialsProvider.AwsRegion(ctx, sp.reqOpts)
|
||||
}
|
||||
if canRetrieveRegionFromEnvironment() {
|
||||
if envAwsRegion := getenv(awsRegionEnvVar); envAwsRegion != "" {
|
||||
return envAwsRegion, nil
|
||||
}
|
||||
return getenv(awsDefaultRegionEnvVar), nil
|
||||
}
|
||||
|
||||
if sp.RegionURL == "" {
|
||||
return "", errors.New("credentials: unable to determine AWS region")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", sp.RegionURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for name, value := range headers {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
sp.logger.DebugContext(ctx, "aws region request", "request", internallog.HTTPRequest(req, nil))
|
||||
resp, body, err := internal.DoRequest(sp.Client, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sp.logger.DebugContext(ctx, "aws region response", "response", internallog.HTTPResponse(resp, body))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("credentials: unable to retrieve AWS region - %s", body)
|
||||
}
|
||||
|
||||
// This endpoint will return the region in format: us-east-2b.
|
||||
// Only the us-east-2 part should be used.
|
||||
bodyLen := len(body)
|
||||
if bodyLen == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return string(body[:bodyLen-1]), nil
|
||||
}
|
||||
|
||||
func (sp *awsSubjectProvider) getSecurityCredentials(ctx context.Context, headers map[string]string) (result *AwsSecurityCredentials, err error) {
|
||||
if sp.securityCredentialsProvider != nil {
|
||||
return sp.securityCredentialsProvider.AwsSecurityCredentials(ctx, sp.reqOpts)
|
||||
}
|
||||
if canRetrieveSecurityCredentialFromEnvironment() {
|
||||
return &AwsSecurityCredentials{
|
||||
AccessKeyID: getenv(awsAccessKeyIDEnvVar),
|
||||
SecretAccessKey: getenv(awsSecretAccessKeyEnvVar),
|
||||
SessionToken: getenv(awsSessionTokenEnvVar),
|
||||
}, nil
|
||||
}
|
||||
|
||||
roleName, err := sp.getMetadataRoleName(ctx, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
credentials, err := sp.getMetadataSecurityCredentials(ctx, roleName, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if credentials.AccessKeyID == "" {
|
||||
return result, errors.New("credentials: missing AccessKeyId credential")
|
||||
}
|
||||
if credentials.SecretAccessKey == "" {
|
||||
return result, errors.New("credentials: missing SecretAccessKey credential")
|
||||
}
|
||||
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func (sp *awsSubjectProvider) getMetadataSecurityCredentials(ctx context.Context, roleName string, headers map[string]string) (*AwsSecurityCredentials, error) {
|
||||
var result *AwsSecurityCredentials
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s", sp.CredVerificationURL, roleName), nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
for name, value := range headers {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
sp.logger.DebugContext(ctx, "aws security credential request", "request", internallog.HTTPRequest(req, nil))
|
||||
resp, body, err := internal.DoRequest(sp.Client, req)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
sp.logger.DebugContext(ctx, "aws security credential response", "response", internallog.HTTPResponse(resp, body))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return result, fmt.Errorf("credentials: unable to retrieve AWS security credentials - %s", body)
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (sp *awsSubjectProvider) getMetadataRoleName(ctx context.Context, headers map[string]string) (string, error) {
|
||||
if sp.CredVerificationURL == "" {
|
||||
return "", errors.New("credentials: unable to determine the AWS metadata server security credentials endpoint")
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", sp.CredVerificationURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for name, value := range headers {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
|
||||
sp.logger.DebugContext(ctx, "aws metadata role request", "request", internallog.HTTPRequest(req, nil))
|
||||
resp, body, err := internal.DoRequest(sp.Client, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sp.logger.DebugContext(ctx, "aws metadata role response", "response", internallog.HTTPResponse(resp, body))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("credentials: unable to retrieve AWS role name - %s", body)
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// awsRequestSigner is a utility class to sign http requests using a AWS V4 signature.
|
||||
type awsRequestSigner struct {
|
||||
RegionName string
|
||||
AwsSecurityCredentials *AwsSecurityCredentials
|
||||
}
|
||||
|
||||
// signRequest adds the appropriate headers to an http.Request
|
||||
// or returns an error if something prevented this.
|
||||
func (rs *awsRequestSigner) signRequest(req *http.Request) error {
|
||||
// req is assumed non-nil
|
||||
signedRequest := cloneRequest(req)
|
||||
timestamp := Now()
|
||||
signedRequest.Header.Set("host", requestHost(req))
|
||||
if rs.AwsSecurityCredentials.SessionToken != "" {
|
||||
signedRequest.Header.Set(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SessionToken)
|
||||
}
|
||||
if signedRequest.Header.Get("date") == "" {
|
||||
signedRequest.Header.Set(awsDateHeader, timestamp.Format(awsTimeFormatLong))
|
||||
}
|
||||
authorizationCode, err := rs.generateAuthentication(signedRequest, timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signedRequest.Header.Set("Authorization", authorizationCode)
|
||||
req.Header = signedRequest.Header
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp time.Time) (string, error) {
|
||||
canonicalHeaderColumns, canonicalHeaderData := canonicalHeaders(req)
|
||||
dateStamp := timestamp.Format(awsTimeFormatShort)
|
||||
serviceName := ""
|
||||
|
||||
if splitHost := strings.Split(requestHost(req), "."); len(splitHost) > 0 {
|
||||
serviceName = splitHost[0]
|
||||
}
|
||||
credentialScope := strings.Join([]string{dateStamp, rs.RegionName, serviceName, awsRequestType}, "/")
|
||||
requestString, err := canonicalRequest(req, canonicalHeaderColumns, canonicalHeaderData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
requestHash, err := getSha256([]byte(requestString))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
stringToSign := strings.Join([]string{awsAlgorithm, timestamp.Format(awsTimeFormatLong), credentialScope, requestHash}, "\n")
|
||||
signingKey := []byte("AWS4" + rs.AwsSecurityCredentials.SecretAccessKey)
|
||||
for _, signingInput := range []string{
|
||||
dateStamp, rs.RegionName, serviceName, awsRequestType, stringToSign,
|
||||
} {
|
||||
signingKey, err = getHmacSha256(signingKey, []byte(signingInput))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", awsAlgorithm, rs.AwsSecurityCredentials.AccessKeyID, credentialScope, canonicalHeaderColumns, hex.EncodeToString(signingKey)), nil
|
||||
}
|
||||
|
||||
func getSha256(input []byte) (string, error) {
|
||||
hash := sha256.New()
|
||||
if _, err := hash.Write(input); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func getHmacSha256(key, input []byte) ([]byte, error) {
|
||||
hash := hmac.New(sha256.New, key)
|
||||
if _, err := hash.Write(input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hash.Sum(nil), nil
|
||||
}
|
||||
|
||||
func cloneRequest(r *http.Request) *http.Request {
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
if r.Header != nil {
|
||||
r2.Header = make(http.Header, len(r.Header))
|
||||
|
||||
// Find total number of values.
|
||||
headerCount := 0
|
||||
for _, headerValues := range r.Header {
|
||||
headerCount += len(headerValues)
|
||||
}
|
||||
copiedHeaders := make([]string, headerCount) // shared backing array for headers' values
|
||||
|
||||
for headerKey, headerValues := range r.Header {
|
||||
headerCount = copy(copiedHeaders, headerValues)
|
||||
r2.Header[headerKey] = copiedHeaders[:headerCount:headerCount]
|
||||
copiedHeaders = copiedHeaders[headerCount:]
|
||||
}
|
||||
}
|
||||
return r2
|
||||
}
|
||||
|
||||
func canonicalPath(req *http.Request) string {
|
||||
result := req.URL.EscapedPath()
|
||||
if result == "" {
|
||||
return "/"
|
||||
}
|
||||
return path.Clean(result)
|
||||
}
|
||||
|
||||
func canonicalQuery(req *http.Request) string {
|
||||
queryValues := req.URL.Query()
|
||||
for queryKey := range queryValues {
|
||||
sort.Strings(queryValues[queryKey])
|
||||
}
|
||||
return queryValues.Encode()
|
||||
}
|
||||
|
||||
func canonicalHeaders(req *http.Request) (string, string) {
|
||||
// Header keys need to be sorted alphabetically.
|
||||
var headers []string
|
||||
lowerCaseHeaders := make(http.Header)
|
||||
for k, v := range req.Header {
|
||||
k := strings.ToLower(k)
|
||||
if _, ok := lowerCaseHeaders[k]; ok {
|
||||
// include additional values
|
||||
lowerCaseHeaders[k] = append(lowerCaseHeaders[k], v...)
|
||||
} else {
|
||||
headers = append(headers, k)
|
||||
lowerCaseHeaders[k] = v
|
||||
}
|
||||
}
|
||||
sort.Strings(headers)
|
||||
|
||||
var fullHeaders bytes.Buffer
|
||||
for _, header := range headers {
|
||||
headerValue := strings.Join(lowerCaseHeaders[header], ",")
|
||||
fullHeaders.WriteString(header)
|
||||
fullHeaders.WriteRune(':')
|
||||
fullHeaders.WriteString(headerValue)
|
||||
fullHeaders.WriteRune('\n')
|
||||
}
|
||||
|
||||
return strings.Join(headers, ";"), fullHeaders.String()
|
||||
}
|
||||
|
||||
func requestDataHash(req *http.Request) (string, error) {
|
||||
var requestData []byte
|
||||
if req.Body != nil {
|
||||
requestBody, err := req.GetBody()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer requestBody.Close()
|
||||
|
||||
requestData, err = internal.ReadAll(requestBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return getSha256(requestData)
|
||||
}
|
||||
|
||||
func requestHost(req *http.Request) string {
|
||||
if req.Host != "" {
|
||||
return req.Host
|
||||
}
|
||||
return req.URL.Host
|
||||
}
|
||||
|
||||
func canonicalRequest(req *http.Request, canonicalHeaderColumns, canonicalHeaderData string) (string, error) {
|
||||
dataHash, err := requestDataHash(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", req.Method, canonicalPath(req), canonicalQuery(req), canonicalHeaderData, canonicalHeaderColumns, dataHash), nil
|
||||
}
|
||||
|
||||
type awsRequestHeader struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type awsRequest struct {
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
Headers []awsRequestHeader `json:"headers"`
|
||||
}
|
||||
|
||||
// The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. Only one is
|
||||
// required.
|
||||
func canRetrieveRegionFromEnvironment() bool {
|
||||
return getenv(awsRegionEnvVar) != "" || getenv(awsDefaultRegionEnvVar) != ""
|
||||
}
|
||||
|
||||
// Check if both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are available.
|
||||
func canRetrieveSecurityCredentialFromEnvironment() bool {
|
||||
return getenv(awsAccessKeyIDEnvVar) != "" && getenv(awsSecretAccessKeyEnvVar) != ""
|
||||
}
|
||||
|
||||
func (sp *awsSubjectProvider) shouldUseMetadataServer() bool {
|
||||
return sp.securityCredentialsProvider == nil && (!canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment())
|
||||
}
|
||||
Generated
Vendored
+284
@@ -0,0 +1,284 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
executableSupportedMaxVersion = 1
|
||||
executableDefaultTimeout = 30 * time.Second
|
||||
executableSource = "response"
|
||||
executableProviderType = "executable"
|
||||
outputFileSource = "output file"
|
||||
|
||||
allowExecutablesEnvVar = "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"
|
||||
|
||||
jwtTokenType = "urn:ietf:params:oauth:token-type:jwt"
|
||||
idTokenType = "urn:ietf:params:oauth:token-type:id_token"
|
||||
saml2TokenType = "urn:ietf:params:oauth:token-type:saml2"
|
||||
)
|
||||
|
||||
var (
|
||||
serviceAccountImpersonationRE = regexp.MustCompile(`https://iamcredentials..+/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken`)
|
||||
)
|
||||
|
||||
type nonCacheableError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (nce nonCacheableError) Error() string {
|
||||
return nce.message
|
||||
}
|
||||
|
||||
// environment is a contract for testing
|
||||
type environment interface {
|
||||
existingEnv() []string
|
||||
getenv(string) string
|
||||
run(ctx context.Context, command string, env []string) ([]byte, error)
|
||||
now() time.Time
|
||||
}
|
||||
|
||||
type runtimeEnvironment struct{}
|
||||
|
||||
func (r runtimeEnvironment) existingEnv() []string {
|
||||
return os.Environ()
|
||||
}
|
||||
func (r runtimeEnvironment) getenv(key string) string {
|
||||
return os.Getenv(key)
|
||||
}
|
||||
func (r runtimeEnvironment) now() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
func (r runtimeEnvironment) run(ctx context.Context, command string, env []string) ([]byte, error) {
|
||||
splitCommand := strings.Fields(command)
|
||||
cmd := exec.CommandContext(ctx, splitCommand[0], splitCommand[1:]...)
|
||||
cmd.Env = env
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
return nil, exitCodeError(exitError)
|
||||
}
|
||||
return nil, executableError(err)
|
||||
}
|
||||
|
||||
bytesStdout := bytes.TrimSpace(stdout.Bytes())
|
||||
if len(bytesStdout) > 0 {
|
||||
return bytesStdout, nil
|
||||
}
|
||||
return bytes.TrimSpace(stderr.Bytes()), nil
|
||||
}
|
||||
|
||||
type executableSubjectProvider struct {
|
||||
Command string
|
||||
Timeout time.Duration
|
||||
OutputFile string
|
||||
client *http.Client
|
||||
opts *Options
|
||||
env environment
|
||||
}
|
||||
|
||||
type executableResponse struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
Success *bool `json:"success,omitempty"`
|
||||
TokenType string `json:"token_type,omitempty"`
|
||||
ExpirationTime int64 `json:"expiration_time,omitempty"`
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
SamlResponse string `json:"saml_response,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (sp *executableSubjectProvider) parseSubjectTokenFromSource(response []byte, source string, now int64) (string, error) {
|
||||
var result executableResponse
|
||||
if err := json.Unmarshal(response, &result); err != nil {
|
||||
return "", jsonParsingError(source, string(response))
|
||||
}
|
||||
// Validate
|
||||
if result.Version == 0 {
|
||||
return "", missingFieldError(source, "version")
|
||||
}
|
||||
if result.Success == nil {
|
||||
return "", missingFieldError(source, "success")
|
||||
}
|
||||
if !*result.Success {
|
||||
if result.Code == "" || result.Message == "" {
|
||||
return "", malformedFailureError()
|
||||
}
|
||||
return "", userDefinedError(result.Code, result.Message)
|
||||
}
|
||||
if result.Version > executableSupportedMaxVersion || result.Version < 0 {
|
||||
return "", unsupportedVersionError(source, result.Version)
|
||||
}
|
||||
if result.ExpirationTime == 0 && sp.OutputFile != "" {
|
||||
return "", missingFieldError(source, "expiration_time")
|
||||
}
|
||||
if result.TokenType == "" {
|
||||
return "", missingFieldError(source, "token_type")
|
||||
}
|
||||
if result.ExpirationTime != 0 && result.ExpirationTime < now {
|
||||
return "", tokenExpiredError()
|
||||
}
|
||||
|
||||
switch result.TokenType {
|
||||
case jwtTokenType, idTokenType:
|
||||
if result.IDToken == "" {
|
||||
return "", missingFieldError(source, "id_token")
|
||||
}
|
||||
return result.IDToken, nil
|
||||
case saml2TokenType:
|
||||
if result.SamlResponse == "" {
|
||||
return "", missingFieldError(source, "saml_response")
|
||||
}
|
||||
return result.SamlResponse, nil
|
||||
default:
|
||||
return "", tokenTypeError(source)
|
||||
}
|
||||
}
|
||||
|
||||
func (sp *executableSubjectProvider) subjectToken(ctx context.Context) (string, error) {
|
||||
if token, err := sp.getTokenFromOutputFile(); token != "" || err != nil {
|
||||
return token, err
|
||||
}
|
||||
return sp.getTokenFromExecutableCommand(ctx)
|
||||
}
|
||||
|
||||
func (sp *executableSubjectProvider) providerType() string {
|
||||
return executableProviderType
|
||||
}
|
||||
|
||||
func (sp *executableSubjectProvider) getTokenFromOutputFile() (token string, err error) {
|
||||
if sp.OutputFile == "" {
|
||||
// This ExecutableCredentialSource doesn't use an OutputFile.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
file, err := os.Open(sp.OutputFile)
|
||||
if err != nil {
|
||||
// No OutputFile found. Hasn't been created yet, so skip it.
|
||||
return "", nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data, err := internal.ReadAll(file)
|
||||
if err != nil || len(data) == 0 {
|
||||
// Cachefile exists, but no data found. Get new credential.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
token, err = sp.parseSubjectTokenFromSource(data, outputFileSource, sp.env.now().Unix())
|
||||
if err != nil {
|
||||
if _, ok := err.(nonCacheableError); ok {
|
||||
// If the cached token is expired we need a new token,
|
||||
// and if the cache contains a failure, we need to try again.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// There was an error in the cached token, and the developer should be aware of it.
|
||||
return "", err
|
||||
}
|
||||
// Token parsing succeeded. Use found token.
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (sp *executableSubjectProvider) executableEnvironment() []string {
|
||||
result := sp.env.existingEnv()
|
||||
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=%v", sp.opts.Audience))
|
||||
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=%v", sp.opts.SubjectTokenType))
|
||||
result = append(result, "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0")
|
||||
if sp.opts.ServiceAccountImpersonationURL != "" {
|
||||
matches := serviceAccountImpersonationRE.FindStringSubmatch(sp.opts.ServiceAccountImpersonationURL)
|
||||
if matches != nil {
|
||||
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL=%v", matches[1]))
|
||||
}
|
||||
}
|
||||
if sp.OutputFile != "" {
|
||||
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=%v", sp.OutputFile))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (sp *executableSubjectProvider) getTokenFromExecutableCommand(ctx context.Context) (string, error) {
|
||||
// For security reasons, we need our consumers to set this environment variable to allow executables to be run.
|
||||
if sp.env.getenv(allowExecutablesEnvVar) != "1" {
|
||||
return "", errors.New("credentials: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithDeadline(ctx, sp.env.now().Add(sp.Timeout))
|
||||
defer cancel()
|
||||
|
||||
output, err := sp.env.run(ctx, sp.Command, sp.executableEnvironment())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sp.parseSubjectTokenFromSource(output, executableSource, sp.env.now().Unix())
|
||||
}
|
||||
|
||||
func missingFieldError(source, field string) error {
|
||||
return fmt.Errorf("credentials: %q missing %q field", source, field)
|
||||
}
|
||||
|
||||
func jsonParsingError(source, data string) error {
|
||||
return fmt.Errorf("credentials: unable to parse %q: %v", source, data)
|
||||
}
|
||||
|
||||
func malformedFailureError() error {
|
||||
return nonCacheableError{"credentials: response must include `error` and `message` fields when unsuccessful"}
|
||||
}
|
||||
|
||||
func userDefinedError(code, message string) error {
|
||||
return nonCacheableError{fmt.Sprintf("credentials: response contains unsuccessful response: (%v) %v", code, message)}
|
||||
}
|
||||
|
||||
func unsupportedVersionError(source string, version int) error {
|
||||
return fmt.Errorf("credentials: %v contains unsupported version: %v", source, version)
|
||||
}
|
||||
|
||||
func tokenExpiredError() error {
|
||||
return nonCacheableError{"credentials: the token returned by the executable is expired"}
|
||||
}
|
||||
|
||||
func tokenTypeError(source string) error {
|
||||
return fmt.Errorf("credentials: %v contains unsupported token type", source)
|
||||
}
|
||||
|
||||
func exitCodeError(err *exec.ExitError) error {
|
||||
return fmt.Errorf("credentials: executable command failed with exit code %v: %w", err.ExitCode(), err)
|
||||
}
|
||||
|
||||
func executableError(err error) error {
|
||||
return fmt.Errorf("credentials: executable command failed: %w", err)
|
||||
}
|
||||
Generated
Vendored
+428
@@ -0,0 +1,428 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/credentials/internal/impersonate"
|
||||
"cloud.google.com/go/auth/credentials/internal/stsexchange"
|
||||
"cloud.google.com/go/auth/internal/credsfile"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
)
|
||||
|
||||
const (
|
||||
timeoutMinimum = 5 * time.Second
|
||||
timeoutMaximum = 120 * time.Second
|
||||
|
||||
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
|
||||
defaultTokenURL = "https://sts.UNIVERSE_DOMAIN/v1/token"
|
||||
defaultUniverseDomain = "googleapis.com"
|
||||
)
|
||||
|
||||
var (
|
||||
// Now aliases time.Now for testing
|
||||
Now = func() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
|
||||
)
|
||||
|
||||
// Options stores the configuration for fetching tokens with external credentials.
|
||||
type Options struct {
|
||||
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
|
||||
// identity pool or the workforce pool and the provider identifier in that pool.
|
||||
Audience string
|
||||
// SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec
|
||||
// e.g. `urn:ietf:params:oauth:token-type:jwt`.
|
||||
SubjectTokenType string
|
||||
// TokenURL is the STS token exchange endpoint.
|
||||
TokenURL string
|
||||
// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
|
||||
// user attributes like account identifier, eg. email, username, uid, etc). This is
|
||||
// needed for gCloud session account identification.
|
||||
TokenInfoURL string
|
||||
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
|
||||
// required for workload identity pools when APIs to be accessed have not integrated with UberMint.
|
||||
ServiceAccountImpersonationURL string
|
||||
// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
|
||||
// token will be valid for.
|
||||
ServiceAccountImpersonationLifetimeSeconds int
|
||||
// ClientSecret is currently only required if token_info endpoint also
|
||||
// needs to be called with the generated GCP access token. When provided, STS will be
|
||||
// called with additional basic authentication using client_id as username and client_secret as password.
|
||||
ClientSecret string
|
||||
// ClientID is only required in conjunction with ClientSecret, as described above.
|
||||
ClientID string
|
||||
// CredentialSource contains the necessary information to retrieve the token itself, as well
|
||||
// as some environmental information.
|
||||
CredentialSource *credsfile.CredentialSource
|
||||
// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
|
||||
// will set the x-goog-user-project which overrides the project associated with the credentials.
|
||||
QuotaProjectID string
|
||||
// Scopes contains the desired scopes for the returned access token.
|
||||
Scopes []string
|
||||
// WorkforcePoolUserProject should be set when it is a workforce pool and
|
||||
// not a workload identity pool. The underlying principal must still have
|
||||
// serviceusage.services.use IAM permission to use the project for
|
||||
// billing/quota. Optional.
|
||||
WorkforcePoolUserProject string
|
||||
// UniverseDomain is the default service domain for a given Cloud universe.
|
||||
// This value will be used in the default STS token URL. The default value
|
||||
// is "googleapis.com". It will not be used if TokenURL is set. Optional.
|
||||
UniverseDomain string
|
||||
// SubjectTokenProvider is an optional token provider for OIDC/SAML
|
||||
// credentials. One of SubjectTokenProvider, AWSSecurityCredentialProvider
|
||||
// or CredentialSource must be provided. Optional.
|
||||
SubjectTokenProvider SubjectTokenProvider
|
||||
// AwsSecurityCredentialsProvider is an AWS Security Credential provider
|
||||
// for AWS credentials. One of SubjectTokenProvider,
|
||||
// AWSSecurityCredentialProvider or CredentialSource must be provided. Optional.
|
||||
AwsSecurityCredentialsProvider AwsSecurityCredentialsProvider
|
||||
// Client for token request.
|
||||
Client *http.Client
|
||||
// IsDefaultClient marks whether the client passed in is a default client that can be overriden.
|
||||
// This is important for X509 credentials which should create a new client if the default was used
|
||||
// but should respect a client explicitly passed in by the user.
|
||||
IsDefaultClient bool
|
||||
// Logger is used for debug logging. If provided, logging will be enabled
|
||||
// at the loggers configured level. By default logging is disabled unless
|
||||
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
|
||||
// logger will be used. Optional.
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
// SubjectTokenProvider can be used to supply a subject token to exchange for a
|
||||
// GCP access token.
|
||||
type SubjectTokenProvider interface {
|
||||
// SubjectToken should return a valid subject token or an error.
|
||||
// The external account token provider does not cache the returned subject
|
||||
// token, so caching logic should be implemented in the provider to prevent
|
||||
// multiple requests for the same subject token.
|
||||
SubjectToken(ctx context.Context, opts *RequestOptions) (string, error)
|
||||
}
|
||||
|
||||
// RequestOptions contains information about the requested subject token or AWS
|
||||
// security credentials from the Google external account credential.
|
||||
type RequestOptions struct {
|
||||
// Audience is the requested audience for the external account credential.
|
||||
Audience string
|
||||
// Subject token type is the requested subject token type for the external
|
||||
// account credential. Expected values include:
|
||||
// “urn:ietf:params:oauth:token-type:jwt”
|
||||
// “urn:ietf:params:oauth:token-type:id-token”
|
||||
// “urn:ietf:params:oauth:token-type:saml2”
|
||||
// “urn:ietf:params:aws:token-type:aws4_request”
|
||||
SubjectTokenType string
|
||||
}
|
||||
|
||||
// AwsSecurityCredentialsProvider can be used to supply AwsSecurityCredentials
|
||||
// and an AWS Region to exchange for a GCP access token.
|
||||
type AwsSecurityCredentialsProvider interface {
|
||||
// AwsRegion should return the AWS region or an error.
|
||||
AwsRegion(ctx context.Context, opts *RequestOptions) (string, error)
|
||||
// GetAwsSecurityCredentials should return a valid set of
|
||||
// AwsSecurityCredentials or an error. The external account token provider
|
||||
// does not cache the returned security credentials, so caching logic should
|
||||
// be implemented in the provider to prevent multiple requests for the
|
||||
// same security credentials.
|
||||
AwsSecurityCredentials(ctx context.Context, opts *RequestOptions) (*AwsSecurityCredentials, error)
|
||||
}
|
||||
|
||||
// AwsSecurityCredentials models AWS security credentials.
|
||||
type AwsSecurityCredentials struct {
|
||||
// AccessKeyId is the AWS Access Key ID - Required.
|
||||
AccessKeyID string `json:"AccessKeyID"`
|
||||
// SecretAccessKey is the AWS Secret Access Key - Required.
|
||||
SecretAccessKey string `json:"SecretAccessKey"`
|
||||
// SessionToken is the AWS Session token. This should be provided for
|
||||
// temporary AWS security credentials - Optional.
|
||||
SessionToken string `json:"Token"`
|
||||
}
|
||||
|
||||
func (o *Options) validate() error {
|
||||
if o.Audience == "" {
|
||||
return fmt.Errorf("externalaccount: Audience must be set")
|
||||
}
|
||||
if o.SubjectTokenType == "" {
|
||||
return fmt.Errorf("externalaccount: Subject token type must be set")
|
||||
}
|
||||
if o.WorkforcePoolUserProject != "" {
|
||||
if valid := validWorkforceAudiencePattern.MatchString(o.Audience); !valid {
|
||||
return fmt.Errorf("externalaccount: workforce_pool_user_project should not be set for non-workforce pool credentials")
|
||||
}
|
||||
}
|
||||
count := 0
|
||||
if o.CredentialSource != nil {
|
||||
count++
|
||||
}
|
||||
if o.SubjectTokenProvider != nil {
|
||||
count++
|
||||
}
|
||||
if o.AwsSecurityCredentialsProvider != nil {
|
||||
count++
|
||||
}
|
||||
if count == 0 {
|
||||
return fmt.Errorf("externalaccount: one of CredentialSource, SubjectTokenProvider, or AwsSecurityCredentialsProvider must be set")
|
||||
}
|
||||
if count > 1 {
|
||||
return fmt.Errorf("externalaccount: only one of CredentialSource, SubjectTokenProvider, or AwsSecurityCredentialsProvider must be set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// client returns the http client that should be used for the token exchange. If a non-default client
|
||||
// is provided, then the client configured in the options will always be returned. If a default client
|
||||
// is provided and the options are configured for X509 credentials, a new client will be created.
|
||||
func (o *Options) client() (*http.Client, error) {
|
||||
// If a client was provided and no override certificate config location was provided, use the provided client.
|
||||
if o.CredentialSource == nil || o.CredentialSource.Certificate == nil || (!o.IsDefaultClient && o.CredentialSource.Certificate.CertificateConfigLocation == "") {
|
||||
return o.Client, nil
|
||||
}
|
||||
|
||||
// If a new client should be created, validate and use the certificate source to create a new mTLS client.
|
||||
cert := o.CredentialSource.Certificate
|
||||
if !cert.UseDefaultCertificateConfig && cert.CertificateConfigLocation == "" {
|
||||
return nil, errors.New("credentials: \"certificate\" object must either specify a certificate_config_location or use_default_certificate_config should be true")
|
||||
}
|
||||
if cert.UseDefaultCertificateConfig && cert.CertificateConfigLocation != "" {
|
||||
return nil, errors.New("credentials: \"certificate\" object cannot specify both a certificate_config_location and use_default_certificate_config=true")
|
||||
}
|
||||
return createX509Client(cert.CertificateConfigLocation)
|
||||
}
|
||||
|
||||
// resolveTokenURL sets the default STS token endpoint with the configured
|
||||
// universe domain.
|
||||
func (o *Options) resolveTokenURL() {
|
||||
if o.TokenURL != "" {
|
||||
return
|
||||
} else if o.UniverseDomain != "" {
|
||||
o.TokenURL = strings.Replace(defaultTokenURL, universeDomainPlaceholder, o.UniverseDomain, 1)
|
||||
} else {
|
||||
o.TokenURL = strings.Replace(defaultTokenURL, universeDomainPlaceholder, defaultUniverseDomain, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider]
|
||||
// configured with the provided options.
|
||||
func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
|
||||
if err := opts.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts.resolveTokenURL()
|
||||
logger := internallog.New(opts.Logger)
|
||||
stp, err := newSubjectTokenProvider(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := opts.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tp := &tokenProvider{
|
||||
client: client,
|
||||
opts: opts,
|
||||
stp: stp,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
if opts.ServiceAccountImpersonationURL == "" {
|
||||
return auth.NewCachedTokenProvider(tp, nil), nil
|
||||
}
|
||||
|
||||
scopes := make([]string, len(opts.Scopes))
|
||||
copy(scopes, opts.Scopes)
|
||||
// needed for impersonation
|
||||
tp.opts.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
|
||||
imp, err := impersonate.NewTokenProvider(&impersonate.Options{
|
||||
Client: client,
|
||||
URL: opts.ServiceAccountImpersonationURL,
|
||||
Scopes: scopes,
|
||||
Tp: auth.NewCachedTokenProvider(tp, nil),
|
||||
TokenLifetimeSeconds: opts.ServiceAccountImpersonationLifetimeSeconds,
|
||||
Logger: logger,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return auth.NewCachedTokenProvider(imp, nil), nil
|
||||
}
|
||||
|
||||
type subjectTokenProvider interface {
|
||||
subjectToken(ctx context.Context) (string, error)
|
||||
providerType() string
|
||||
}
|
||||
|
||||
// tokenProvider is the provider that handles external credentials. It is used to retrieve Tokens.
|
||||
type tokenProvider struct {
|
||||
client *http.Client
|
||||
logger *slog.Logger
|
||||
opts *Options
|
||||
stp subjectTokenProvider
|
||||
}
|
||||
|
||||
func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
|
||||
subjectToken, err := tp.stp.subjectToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stsRequest := &stsexchange.TokenRequest{
|
||||
GrantType: stsexchange.GrantType,
|
||||
Audience: tp.opts.Audience,
|
||||
Scope: tp.opts.Scopes,
|
||||
RequestedTokenType: stsexchange.TokenType,
|
||||
SubjectToken: subjectToken,
|
||||
SubjectTokenType: tp.opts.SubjectTokenType,
|
||||
}
|
||||
header := make(http.Header)
|
||||
header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
header.Add("x-goog-api-client", getGoogHeaderValue(tp.opts, tp.stp))
|
||||
clientAuth := stsexchange.ClientAuthentication{
|
||||
AuthStyle: auth.StyleInHeader,
|
||||
ClientID: tp.opts.ClientID,
|
||||
ClientSecret: tp.opts.ClientSecret,
|
||||
}
|
||||
var options map[string]interface{}
|
||||
// Do not pass workforce_pool_user_project when client authentication is used.
|
||||
// The client ID is sufficient for determining the user project.
|
||||
if tp.opts.WorkforcePoolUserProject != "" && tp.opts.ClientID == "" {
|
||||
options = map[string]interface{}{
|
||||
"userProject": tp.opts.WorkforcePoolUserProject,
|
||||
}
|
||||
}
|
||||
stsResp, err := stsexchange.ExchangeToken(ctx, &stsexchange.Options{
|
||||
Client: tp.client,
|
||||
Endpoint: tp.opts.TokenURL,
|
||||
Request: stsRequest,
|
||||
Authentication: clientAuth,
|
||||
Headers: header,
|
||||
ExtraOpts: options,
|
||||
Logger: tp.logger,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tok := &auth.Token{
|
||||
Value: stsResp.AccessToken,
|
||||
Type: stsResp.TokenType,
|
||||
}
|
||||
// The RFC8693 doesn't define the explicit 0 of "expires_in" field behavior.
|
||||
if stsResp.ExpiresIn <= 0 {
|
||||
return nil, fmt.Errorf("credentials: got invalid expiry from security token service")
|
||||
}
|
||||
tok.Expiry = Now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
// newSubjectTokenProvider determines the type of credsfile.CredentialSource needed to create a
|
||||
// subjectTokenProvider
|
||||
func newSubjectTokenProvider(o *Options) (subjectTokenProvider, error) {
|
||||
logger := internallog.New(o.Logger)
|
||||
reqOpts := &RequestOptions{Audience: o.Audience, SubjectTokenType: o.SubjectTokenType}
|
||||
if o.AwsSecurityCredentialsProvider != nil {
|
||||
return &awsSubjectProvider{
|
||||
securityCredentialsProvider: o.AwsSecurityCredentialsProvider,
|
||||
TargetResource: o.Audience,
|
||||
reqOpts: reqOpts,
|
||||
logger: logger,
|
||||
}, nil
|
||||
} else if o.SubjectTokenProvider != nil {
|
||||
return &programmaticProvider{stp: o.SubjectTokenProvider, opts: reqOpts}, nil
|
||||
} else if len(o.CredentialSource.EnvironmentID) > 3 && o.CredentialSource.EnvironmentID[:3] == "aws" {
|
||||
if awsVersion, err := strconv.Atoi(o.CredentialSource.EnvironmentID[3:]); err == nil {
|
||||
if awsVersion != 1 {
|
||||
return nil, fmt.Errorf("credentials: aws version '%d' is not supported in the current build", awsVersion)
|
||||
}
|
||||
|
||||
awsProvider := &awsSubjectProvider{
|
||||
EnvironmentID: o.CredentialSource.EnvironmentID,
|
||||
RegionURL: o.CredentialSource.RegionURL,
|
||||
RegionalCredVerificationURL: o.CredentialSource.RegionalCredVerificationURL,
|
||||
CredVerificationURL: o.CredentialSource.URL,
|
||||
TargetResource: o.Audience,
|
||||
Client: o.Client,
|
||||
logger: logger,
|
||||
}
|
||||
if o.CredentialSource.IMDSv2SessionTokenURL != "" {
|
||||
awsProvider.IMDSv2SessionTokenURL = o.CredentialSource.IMDSv2SessionTokenURL
|
||||
}
|
||||
|
||||
return awsProvider, nil
|
||||
}
|
||||
} else if o.CredentialSource.File != "" {
|
||||
return &fileSubjectProvider{File: o.CredentialSource.File, Format: o.CredentialSource.Format}, nil
|
||||
} else if o.CredentialSource.URL != "" {
|
||||
return &urlSubjectProvider{
|
||||
URL: o.CredentialSource.URL,
|
||||
Headers: o.CredentialSource.Headers,
|
||||
Format: o.CredentialSource.Format,
|
||||
Client: o.Client,
|
||||
Logger: logger,
|
||||
}, nil
|
||||
} else if o.CredentialSource.Executable != nil {
|
||||
ec := o.CredentialSource.Executable
|
||||
if ec.Command == "" {
|
||||
return nil, errors.New("credentials: missing `command` field — executable command must be provided")
|
||||
}
|
||||
|
||||
execProvider := &executableSubjectProvider{}
|
||||
execProvider.Command = ec.Command
|
||||
if ec.TimeoutMillis == 0 {
|
||||
execProvider.Timeout = executableDefaultTimeout
|
||||
} else {
|
||||
execProvider.Timeout = time.Duration(ec.TimeoutMillis) * time.Millisecond
|
||||
if execProvider.Timeout < timeoutMinimum || execProvider.Timeout > timeoutMaximum {
|
||||
return nil, fmt.Errorf("credentials: invalid `timeout_millis` field — executable timeout must be between %v and %v seconds", timeoutMinimum.Seconds(), timeoutMaximum.Seconds())
|
||||
}
|
||||
}
|
||||
execProvider.OutputFile = ec.OutputFile
|
||||
execProvider.client = o.Client
|
||||
execProvider.opts = o
|
||||
execProvider.env = runtimeEnvironment{}
|
||||
return execProvider, nil
|
||||
} else if o.CredentialSource.Certificate != nil {
|
||||
cert := o.CredentialSource.Certificate
|
||||
if !cert.UseDefaultCertificateConfig && cert.CertificateConfigLocation == "" {
|
||||
return nil, errors.New("credentials: \"certificate\" object must either specify a certificate_config_location or use_default_certificate_config should be true")
|
||||
}
|
||||
if cert.UseDefaultCertificateConfig && cert.CertificateConfigLocation != "" {
|
||||
return nil, errors.New("credentials: \"certificate\" object cannot specify both a certificate_config_location and use_default_certificate_config=true")
|
||||
}
|
||||
return &x509Provider{}, nil
|
||||
}
|
||||
return nil, errors.New("credentials: unable to parse credential source")
|
||||
}
|
||||
|
||||
func getGoogHeaderValue(conf *Options, p subjectTokenProvider) string {
|
||||
return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
|
||||
goVersion(),
|
||||
"unknown",
|
||||
p.providerType(),
|
||||
conf.ServiceAccountImpersonationURL != "",
|
||||
conf.ServiceAccountImpersonationLifetimeSeconds != 0)
|
||||
}
|
||||
Generated
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/credsfile"
|
||||
)
|
||||
|
||||
const (
|
||||
fileProviderType = "file"
|
||||
)
|
||||
|
||||
type fileSubjectProvider struct {
|
||||
File string
|
||||
Format *credsfile.Format
|
||||
}
|
||||
|
||||
func (sp *fileSubjectProvider) subjectToken(context.Context) (string, error) {
|
||||
tokenFile, err := os.Open(sp.File)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("credentials: failed to open credential file %q: %w", sp.File, err)
|
||||
}
|
||||
defer tokenFile.Close()
|
||||
tokenBytes, err := internal.ReadAll(tokenFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("credentials: failed to read credential file: %w", err)
|
||||
}
|
||||
tokenBytes = bytes.TrimSpace(tokenBytes)
|
||||
|
||||
if sp.Format == nil {
|
||||
return string(tokenBytes), nil
|
||||
}
|
||||
switch sp.Format.Type {
|
||||
case fileTypeJSON:
|
||||
jsonData := make(map[string]interface{})
|
||||
err = json.Unmarshal(tokenBytes, &jsonData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("credentials: failed to unmarshal subject token file: %w", err)
|
||||
}
|
||||
val, ok := jsonData[sp.Format.SubjectTokenFieldName]
|
||||
if !ok {
|
||||
return "", errors.New("credentials: provided subject_token_field_name not found in credentials")
|
||||
}
|
||||
token, ok := val.(string)
|
||||
if !ok {
|
||||
return "", errors.New("credentials: improperly formatted subject token")
|
||||
}
|
||||
return token, nil
|
||||
case fileTypeText:
|
||||
return string(tokenBytes), nil
|
||||
default:
|
||||
return "", errors.New("credentials: invalid credential_source file format type: " + sp.Format.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (sp *fileSubjectProvider) providerType() string {
|
||||
return fileProviderType
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
// version is a package internal global variable for testing purposes.
|
||||
version = runtime.Version
|
||||
)
|
||||
|
||||
// versionUnknown is only used when the runtime version cannot be determined.
|
||||
const versionUnknown = "UNKNOWN"
|
||||
|
||||
// goVersion returns a Go runtime version derived from the runtime environment
|
||||
// that is modified to be suitable for reporting in a header, meaning it has no
|
||||
// whitespace. If it is unable to determine the Go runtime version, it returns
|
||||
// versionUnknown.
|
||||
func goVersion() string {
|
||||
const develPrefix = "devel +"
|
||||
|
||||
s := version()
|
||||
if strings.HasPrefix(s, develPrefix) {
|
||||
s = s[len(develPrefix):]
|
||||
if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
|
||||
s = s[:p]
|
||||
}
|
||||
return s
|
||||
} else if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
|
||||
s = s[:p]
|
||||
}
|
||||
|
||||
notSemverRune := func(r rune) bool {
|
||||
return !strings.ContainsRune("0123456789.", r)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(s, "go1") {
|
||||
s = s[2:]
|
||||
var prerelease string
|
||||
if p := strings.IndexFunc(s, notSemverRune); p >= 0 {
|
||||
s, prerelease = s[:p], s[p:]
|
||||
}
|
||||
if strings.HasSuffix(s, ".") {
|
||||
s += "0"
|
||||
} else if strings.Count(s, ".") < 2 {
|
||||
s += ".0"
|
||||
}
|
||||
if prerelease != "" {
|
||||
// Some release candidates already have a dash in them.
|
||||
if !strings.HasPrefix(prerelease, "-") {
|
||||
prerelease = "-" + prerelease
|
||||
}
|
||||
s += prerelease
|
||||
}
|
||||
return s
|
||||
}
|
||||
return versionUnknown
|
||||
}
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import "context"
|
||||
|
||||
type programmaticProvider struct {
|
||||
opts *RequestOptions
|
||||
stp SubjectTokenProvider
|
||||
}
|
||||
|
||||
func (pp *programmaticProvider) providerType() string {
|
||||
return programmaticProviderType
|
||||
}
|
||||
|
||||
func (pp *programmaticProvider) subjectToken(ctx context.Context) (string, error) {
|
||||
return pp.stp.SubjectToken(ctx, pp.opts)
|
||||
}
|
||||
Generated
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/credsfile"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
)
|
||||
|
||||
const (
|
||||
fileTypeText = "text"
|
||||
fileTypeJSON = "json"
|
||||
urlProviderType = "url"
|
||||
programmaticProviderType = "programmatic"
|
||||
x509ProviderType = "x509"
|
||||
)
|
||||
|
||||
type urlSubjectProvider struct {
|
||||
URL string
|
||||
Headers map[string]string
|
||||
Format *credsfile.Format
|
||||
Client *http.Client
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
func (sp *urlSubjectProvider) subjectToken(ctx context.Context) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", sp.URL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("credentials: HTTP request for URL-sourced credential failed: %w", err)
|
||||
}
|
||||
|
||||
for key, val := range sp.Headers {
|
||||
req.Header.Add(key, val)
|
||||
}
|
||||
sp.Logger.DebugContext(ctx, "url subject token request", "request", internallog.HTTPRequest(req, nil))
|
||||
resp, body, err := internal.DoRequest(sp.Client, req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("credentials: invalid response when retrieving subject token: %w", err)
|
||||
}
|
||||
sp.Logger.DebugContext(ctx, "url subject token response", "response", internallog.HTTPResponse(resp, body))
|
||||
if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
|
||||
return "", fmt.Errorf("credentials: status code %d: %s", c, body)
|
||||
}
|
||||
|
||||
if sp.Format == nil {
|
||||
return string(body), nil
|
||||
}
|
||||
switch sp.Format.Type {
|
||||
case "json":
|
||||
jsonData := make(map[string]interface{})
|
||||
err = json.Unmarshal(body, &jsonData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("credentials: failed to unmarshal subject token file: %w", err)
|
||||
}
|
||||
val, ok := jsonData[sp.Format.SubjectTokenFieldName]
|
||||
if !ok {
|
||||
return "", errors.New("credentials: provided subject_token_field_name not found in credentials")
|
||||
}
|
||||
token, ok := val.(string)
|
||||
if !ok {
|
||||
return "", errors.New("credentials: improperly formatted subject token")
|
||||
}
|
||||
return token, nil
|
||||
case fileTypeText:
|
||||
return string(body), nil
|
||||
default:
|
||||
return "", errors.New("credentials: invalid credential_source file format type: " + sp.Format.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (sp *urlSubjectProvider) providerType() string {
|
||||
return urlProviderType
|
||||
}
|
||||
Generated
Vendored
+63
@@ -0,0 +1,63 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth/internal/transport/cert"
|
||||
)
|
||||
|
||||
// x509Provider implements the subjectTokenProvider type for
|
||||
// x509 workload identity credentials. Because x509 credentials
|
||||
// rely on an mTLS connection to represent the 3rd party identity
|
||||
// rather than a subject token, this provider will always return
|
||||
// an empty string when a subject token is requested by the external account
|
||||
// token provider.
|
||||
type x509Provider struct {
|
||||
}
|
||||
|
||||
func (xp *x509Provider) providerType() string {
|
||||
return x509ProviderType
|
||||
}
|
||||
|
||||
func (xp *x509Provider) subjectToken(ctx context.Context) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// createX509Client creates a new client that is configured with mTLS, using the
|
||||
// certificate configuration specified in the credential source.
|
||||
func createX509Client(certificateConfigLocation string) (*http.Client, error) {
|
||||
certProvider, err := cert.NewWorkloadX509CertProvider(certificateConfigLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trans := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
trans.TLSClientConfig = &tls.Config{
|
||||
GetClientCertificate: certProvider,
|
||||
}
|
||||
|
||||
// Create a client with default settings plus the X509 workload cert and key.
|
||||
client := &http.Client{
|
||||
Transport: trans,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
Generated
Vendored
+115
@@ -0,0 +1,115 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package externalaccountuser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/credentials/internal/stsexchange"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
)
|
||||
|
||||
// Options stores the configuration for fetching tokens with external authorized
|
||||
// user credentials.
|
||||
type Options struct {
|
||||
// Audience is the Secure Token Service (STS) audience which contains the
|
||||
// resource name for the workforce pool and the provider identifier in that
|
||||
// pool.
|
||||
Audience string
|
||||
// RefreshToken is the OAuth 2.0 refresh token.
|
||||
RefreshToken string
|
||||
// TokenURL is the STS token exchange endpoint for refresh.
|
||||
TokenURL string
|
||||
// TokenInfoURL is the STS endpoint URL for token introspection. Optional.
|
||||
TokenInfoURL string
|
||||
// ClientID is only required in conjunction with ClientSecret, as described
|
||||
// below.
|
||||
ClientID string
|
||||
// ClientSecret is currently only required if token_info endpoint also needs
|
||||
// to be called with the generated a cloud access token. When provided, STS
|
||||
// will be called with additional basic authentication using client_id as
|
||||
// username and client_secret as password.
|
||||
ClientSecret string
|
||||
// Scopes contains the desired scopes for the returned access token.
|
||||
Scopes []string
|
||||
|
||||
// Client for token request.
|
||||
Client *http.Client
|
||||
// Logger for logging.
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
func (c *Options) validate() bool {
|
||||
return c.ClientID != "" && c.ClientSecret != "" && c.RefreshToken != "" && c.TokenURL != ""
|
||||
}
|
||||
|
||||
// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider]
|
||||
// configured with the provided options.
|
||||
func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
|
||||
if !opts.validate() {
|
||||
return nil, errors.New("credentials: invalid external_account_authorized_user configuration")
|
||||
}
|
||||
|
||||
tp := &tokenProvider{
|
||||
o: opts,
|
||||
}
|
||||
return auth.NewCachedTokenProvider(tp, nil), nil
|
||||
}
|
||||
|
||||
type tokenProvider struct {
|
||||
o *Options
|
||||
}
|
||||
|
||||
func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
|
||||
opts := tp.o
|
||||
|
||||
clientAuth := stsexchange.ClientAuthentication{
|
||||
AuthStyle: auth.StyleInHeader,
|
||||
ClientID: opts.ClientID,
|
||||
ClientSecret: opts.ClientSecret,
|
||||
}
|
||||
headers := make(http.Header)
|
||||
headers.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
stsResponse, err := stsexchange.RefreshAccessToken(ctx, &stsexchange.Options{
|
||||
Client: opts.Client,
|
||||
Endpoint: opts.TokenURL,
|
||||
RefreshToken: opts.RefreshToken,
|
||||
Authentication: clientAuth,
|
||||
Headers: headers,
|
||||
Logger: internallog.New(tp.o.Logger),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if stsResponse.ExpiresIn < 0 {
|
||||
return nil, errors.New("credentials: invalid expiry from security token service")
|
||||
}
|
||||
|
||||
// guarded by the wrapping with CachedTokenProvider
|
||||
if stsResponse.RefreshToken != "" {
|
||||
opts.RefreshToken = stsResponse.RefreshToken
|
||||
}
|
||||
return &auth.Token{
|
||||
Value: stsResponse.AccessToken,
|
||||
Expiry: time.Now().UTC().Add(time.Duration(stsResponse.ExpiresIn) * time.Second),
|
||||
Type: internal.TokenTypeBearer,
|
||||
}, nil
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gdch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/credsfile"
|
||||
"cloud.google.com/go/auth/internal/jwt"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
)
|
||||
|
||||
const (
|
||||
// GrantType is the grant type for the token request.
|
||||
GrantType = "urn:ietf:params:oauth:token-type:token-exchange"
|
||||
requestTokenType = "urn:ietf:params:oauth:token-type:access_token"
|
||||
subjectTokenType = "urn:k8s:params:oauth:token-type:serviceaccount"
|
||||
)
|
||||
|
||||
var (
|
||||
gdchSupportFormatVersions map[string]bool = map[string]bool{
|
||||
"1": true,
|
||||
}
|
||||
)
|
||||
|
||||
// Options for [NewTokenProvider].
|
||||
type Options struct {
|
||||
STSAudience string
|
||||
Client *http.Client
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider] from a
|
||||
// GDCH cred file.
|
||||
func NewTokenProvider(f *credsfile.GDCHServiceAccountFile, o *Options) (auth.TokenProvider, error) {
|
||||
if !gdchSupportFormatVersions[f.FormatVersion] {
|
||||
return nil, fmt.Errorf("credentials: unsupported gdch_service_account format %q", f.FormatVersion)
|
||||
}
|
||||
if o.STSAudience == "" {
|
||||
return nil, errors.New("credentials: STSAudience must be set for the GDCH auth flows")
|
||||
}
|
||||
signer, err := internal.ParseKey([]byte(f.PrivateKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certPool, err := loadCertPool(f.CertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tp := gdchProvider{
|
||||
serviceIdentity: fmt.Sprintf("system:serviceaccount:%s:%s", f.Project, f.Name),
|
||||
tokenURL: f.TokenURL,
|
||||
aud: o.STSAudience,
|
||||
signer: signer,
|
||||
pkID: f.PrivateKeyID,
|
||||
certPool: certPool,
|
||||
client: o.Client,
|
||||
logger: internallog.New(o.Logger),
|
||||
}
|
||||
return tp, nil
|
||||
}
|
||||
|
||||
func loadCertPool(path string) (*x509.CertPool, error) {
|
||||
pool := x509.NewCertPool()
|
||||
pem, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: failed to read certificate: %w", err)
|
||||
}
|
||||
pool.AppendCertsFromPEM(pem)
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
type gdchProvider struct {
|
||||
serviceIdentity string
|
||||
tokenURL string
|
||||
aud string
|
||||
signer crypto.Signer
|
||||
pkID string
|
||||
certPool *x509.CertPool
|
||||
|
||||
client *http.Client
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (g gdchProvider) Token(ctx context.Context) (*auth.Token, error) {
|
||||
addCertToTransport(g.client, g.certPool)
|
||||
iat := time.Now()
|
||||
exp := iat.Add(time.Hour)
|
||||
claims := jwt.Claims{
|
||||
Iss: g.serviceIdentity,
|
||||
Sub: g.serviceIdentity,
|
||||
Aud: g.tokenURL,
|
||||
Iat: iat.Unix(),
|
||||
Exp: exp.Unix(),
|
||||
}
|
||||
h := jwt.Header{
|
||||
Algorithm: jwt.HeaderAlgRSA256,
|
||||
Type: jwt.HeaderType,
|
||||
KeyID: string(g.pkID),
|
||||
}
|
||||
payload, err := jwt.EncodeJWS(&h, &claims, g.signer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := url.Values{}
|
||||
v.Set("grant_type", GrantType)
|
||||
v.Set("audience", g.aud)
|
||||
v.Set("requested_token_type", requestTokenType)
|
||||
v.Set("subject_token", payload)
|
||||
v.Set("subject_token_type", subjectTokenType)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", g.tokenURL, strings.NewReader(v.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
g.logger.DebugContext(ctx, "gdch token request", "request", internallog.HTTPRequest(req, []byte(v.Encode())))
|
||||
resp, body, err := internal.DoRequest(g.client, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
|
||||
}
|
||||
g.logger.DebugContext(ctx, "gdch token response", "response", internallog.HTTPResponse(resp, body))
|
||||
if c := resp.StatusCode; c < http.StatusOK || c > http.StatusMultipleChoices {
|
||||
return nil, &auth.Error{
|
||||
Response: resp,
|
||||
Body: body,
|
||||
}
|
||||
}
|
||||
|
||||
var tokenRes struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"` // relative seconds from now
|
||||
}
|
||||
if err := json.Unmarshal(body, &tokenRes); err != nil {
|
||||
return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
|
||||
}
|
||||
token := &auth.Token{
|
||||
Value: tokenRes.AccessToken,
|
||||
Type: tokenRes.TokenType,
|
||||
}
|
||||
raw := make(map[string]interface{})
|
||||
json.Unmarshal(body, &raw) // no error checks for optional fields
|
||||
token.Metadata = raw
|
||||
|
||||
if secs := tokenRes.ExpiresIn; secs > 0 {
|
||||
token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// addCertToTransport makes a best effort attempt at adding in the cert info to
|
||||
// the client. It tries to keep all configured transport settings if the
|
||||
// underlying transport is an http.Transport. Or else it overwrites the
|
||||
// transport with defaults adding in the certs.
|
||||
func addCertToTransport(hc *http.Client, certPool *x509.CertPool) {
|
||||
trans, ok := hc.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
trans = http.DefaultTransport.(*http.Transport).Clone()
|
||||
}
|
||||
trans.TLSClientConfig = &tls.Config{
|
||||
RootCAs: certPool,
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package impersonate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTokenLifetime = "3600s"
|
||||
authHeaderKey = "Authorization"
|
||||
)
|
||||
|
||||
// generateAccesstokenReq is used for service account impersonation
|
||||
type generateAccessTokenReq struct {
|
||||
Delegates []string `json:"delegates,omitempty"`
|
||||
Lifetime string `json:"lifetime,omitempty"`
|
||||
Scope []string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
type impersonateTokenResponse struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
ExpireTime string `json:"expireTime"`
|
||||
}
|
||||
|
||||
// NewTokenProvider uses a source credential, stored in Ts, to request an access token to the provided URL.
|
||||
// Scopes can be defined when the access token is requested.
|
||||
func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
|
||||
if err := opts.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// Options for [NewTokenProvider].
|
||||
type Options struct {
|
||||
// Tp is the source credential used to generate a token on the
|
||||
// impersonated service account. Required.
|
||||
Tp auth.TokenProvider
|
||||
|
||||
// URL is the endpoint to call to generate a token
|
||||
// on behalf of the service account. Required.
|
||||
URL string
|
||||
// Scopes that the impersonated credential should have. Required.
|
||||
Scopes []string
|
||||
// Delegates are the service account email addresses in a delegation chain.
|
||||
// Each service account must be granted roles/iam.serviceAccountTokenCreator
|
||||
// on the next service account in the chain. Optional.
|
||||
Delegates []string
|
||||
// TokenLifetimeSeconds is the number of seconds the impersonation token will
|
||||
// be valid for. Defaults to 1 hour if unset. Optional.
|
||||
TokenLifetimeSeconds int
|
||||
// Client configures the underlying client used to make network requests
|
||||
// when fetching tokens. Required.
|
||||
Client *http.Client
|
||||
// Logger is used for debug logging. If provided, logging will be enabled
|
||||
// at the loggers configured level. By default logging is disabled unless
|
||||
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
|
||||
// logger will be used. Optional.
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
func (o *Options) validate() error {
|
||||
if o.Tp == nil {
|
||||
return errors.New("credentials: missing required 'source_credentials' field in impersonated credentials")
|
||||
}
|
||||
if o.URL == "" {
|
||||
return errors.New("credentials: missing required 'service_account_impersonation_url' field in impersonated credentials")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Token performs the exchange to get a temporary service account token to allow access to GCP.
|
||||
func (o *Options) Token(ctx context.Context) (*auth.Token, error) {
|
||||
logger := internallog.New(o.Logger)
|
||||
lifetime := defaultTokenLifetime
|
||||
if o.TokenLifetimeSeconds != 0 {
|
||||
lifetime = fmt.Sprintf("%ds", o.TokenLifetimeSeconds)
|
||||
}
|
||||
reqBody := generateAccessTokenReq{
|
||||
Lifetime: lifetime,
|
||||
Scope: o.Scopes,
|
||||
Delegates: o.Delegates,
|
||||
}
|
||||
b, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: unable to marshal request: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", o.URL, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: unable to create impersonation request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if err := setAuthHeader(ctx, o.Tp, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.DebugContext(ctx, "impersonated token request", "request", internallog.HTTPRequest(req, b))
|
||||
resp, body, err := internal.DoRequest(o.Client, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: unable to generate access token: %w", err)
|
||||
}
|
||||
logger.DebugContext(ctx, "impersonated token response", "response", internallog.HTTPResponse(resp, body))
|
||||
if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
|
||||
return nil, fmt.Errorf("credentials: status code %d: %s", c, body)
|
||||
}
|
||||
|
||||
var accessTokenResp impersonateTokenResponse
|
||||
if err := json.Unmarshal(body, &accessTokenResp); err != nil {
|
||||
return nil, fmt.Errorf("credentials: unable to parse response: %w", err)
|
||||
}
|
||||
expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: unable to parse expiry: %w", err)
|
||||
}
|
||||
return &auth.Token{
|
||||
Value: accessTokenResp.AccessToken,
|
||||
Expiry: expiry,
|
||||
Type: internal.TokenTypeBearer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setAuthHeader(ctx context.Context, tp auth.TokenProvider, r *http.Request) error {
|
||||
t, err := tp.Token(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
typ := t.Type
|
||||
if typ == "" {
|
||||
typ = internal.TokenTypeBearer
|
||||
}
|
||||
r.Header.Set(authHeaderKey, typ+" "+t.Value)
|
||||
return nil
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package stsexchange
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
)
|
||||
|
||||
const (
|
||||
// GrantType for a sts exchange.
|
||||
GrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
|
||||
// TokenType for a sts exchange.
|
||||
TokenType = "urn:ietf:params:oauth:token-type:access_token"
|
||||
|
||||
jwtTokenType = "urn:ietf:params:oauth:token-type:jwt"
|
||||
)
|
||||
|
||||
// Options stores the configuration for making an sts exchange request.
|
||||
type Options struct {
|
||||
Client *http.Client
|
||||
Logger *slog.Logger
|
||||
Endpoint string
|
||||
Request *TokenRequest
|
||||
Authentication ClientAuthentication
|
||||
Headers http.Header
|
||||
// ExtraOpts are optional fields marshalled into the `options` field of the
|
||||
// request body.
|
||||
ExtraOpts map[string]interface{}
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
// RefreshAccessToken performs the token exchange using a refresh token flow.
|
||||
func RefreshAccessToken(ctx context.Context, opts *Options) (*TokenResponse, error) {
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "refresh_token")
|
||||
data.Set("refresh_token", opts.RefreshToken)
|
||||
return doRequest(ctx, opts, data)
|
||||
}
|
||||
|
||||
// ExchangeToken performs an oauth2 token exchange with the provided endpoint.
|
||||
func ExchangeToken(ctx context.Context, opts *Options) (*TokenResponse, error) {
|
||||
data := url.Values{}
|
||||
data.Set("audience", opts.Request.Audience)
|
||||
data.Set("grant_type", GrantType)
|
||||
data.Set("requested_token_type", TokenType)
|
||||
data.Set("subject_token_type", opts.Request.SubjectTokenType)
|
||||
data.Set("subject_token", opts.Request.SubjectToken)
|
||||
data.Set("scope", strings.Join(opts.Request.Scope, " "))
|
||||
if opts.ExtraOpts != nil {
|
||||
opts, err := json.Marshal(opts.ExtraOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: failed to marshal additional options: %w", err)
|
||||
}
|
||||
data.Set("options", string(opts))
|
||||
}
|
||||
return doRequest(ctx, opts, data)
|
||||
}
|
||||
|
||||
func doRequest(ctx context.Context, opts *Options, data url.Values) (*TokenResponse, error) {
|
||||
opts.Authentication.InjectAuthentication(data, opts.Headers)
|
||||
encodedData := data.Encode()
|
||||
logger := internallog.New(opts.Logger)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", opts.Endpoint, strings.NewReader(encodedData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: failed to properly build http request: %w", err)
|
||||
|
||||
}
|
||||
for key, list := range opts.Headers {
|
||||
for _, val := range list {
|
||||
req.Header.Add(key, val)
|
||||
}
|
||||
}
|
||||
req.Header.Set("Content-Length", strconv.Itoa(len(encodedData)))
|
||||
|
||||
logger.DebugContext(ctx, "sts token request", "request", internallog.HTTPRequest(req, []byte(encodedData)))
|
||||
resp, body, err := internal.DoRequest(opts.Client, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: invalid response from Secure Token Server: %w", err)
|
||||
}
|
||||
logger.DebugContext(ctx, "sts token response", "response", internallog.HTTPResponse(resp, body))
|
||||
if c := resp.StatusCode; c < http.StatusOK || c > http.StatusMultipleChoices {
|
||||
return nil, fmt.Errorf("credentials: status code %d: %s", c, body)
|
||||
}
|
||||
var stsResp TokenResponse
|
||||
if err := json.Unmarshal(body, &stsResp); err != nil {
|
||||
return nil, fmt.Errorf("credentials: failed to unmarshal response body from Secure Token Server: %w", err)
|
||||
}
|
||||
|
||||
return &stsResp, nil
|
||||
}
|
||||
|
||||
// TokenRequest contains fields necessary to make an oauth2 token
|
||||
// exchange.
|
||||
type TokenRequest struct {
|
||||
ActingParty struct {
|
||||
ActorToken string
|
||||
ActorTokenType string
|
||||
}
|
||||
GrantType string
|
||||
Resource string
|
||||
Audience string
|
||||
Scope []string
|
||||
RequestedTokenType string
|
||||
SubjectToken string
|
||||
SubjectTokenType string
|
||||
}
|
||||
|
||||
// TokenResponse is used to decode the remote server response during
|
||||
// an oauth2 token exchange.
|
||||
type TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
IssuedTokenType string `json:"issued_token_type"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// ClientAuthentication represents an OAuth client ID and secret and the
|
||||
// mechanism for passing these credentials as stated in rfc6749#2.3.1.
|
||||
type ClientAuthentication struct {
|
||||
AuthStyle auth.Style
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
// InjectAuthentication is used to add authentication to a Secure Token Service
|
||||
// exchange request. It modifies either the passed url.Values or http.Header
|
||||
// depending on the desired authentication format.
|
||||
func (c *ClientAuthentication) InjectAuthentication(values url.Values, headers http.Header) {
|
||||
if c.ClientID == "" || c.ClientSecret == "" || values == nil || headers == nil {
|
||||
return
|
||||
}
|
||||
switch c.AuthStyle {
|
||||
case auth.StyleInHeader:
|
||||
plainHeader := c.ClientID + ":" + c.ClientSecret
|
||||
headers.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(plainHeader)))
|
||||
default:
|
||||
values.Set("client_id", c.ClientID)
|
||||
values.Set("client_secret", c.ClientSecret)
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/credsfile"
|
||||
"cloud.google.com/go/auth/internal/jwt"
|
||||
)
|
||||
|
||||
var (
|
||||
// for testing
|
||||
now func() time.Time = time.Now
|
||||
)
|
||||
|
||||
// configureSelfSignedJWT uses the private key in the service account to create
|
||||
// a JWT without making a network call.
|
||||
func configureSelfSignedJWT(f *credsfile.ServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
|
||||
if len(opts.scopes()) == 0 && opts.Audience == "" {
|
||||
return nil, errors.New("credentials: both scopes and audience are empty")
|
||||
}
|
||||
signer, err := internal.ParseKey([]byte(f.PrivateKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: could not parse key: %w", err)
|
||||
}
|
||||
return &selfSignedTokenProvider{
|
||||
email: f.ClientEmail,
|
||||
audience: opts.Audience,
|
||||
scopes: opts.scopes(),
|
||||
signer: signer,
|
||||
pkID: f.PrivateKeyID,
|
||||
logger: opts.logger(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type selfSignedTokenProvider struct {
|
||||
email string
|
||||
audience string
|
||||
scopes []string
|
||||
signer crypto.Signer
|
||||
pkID string
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (tp *selfSignedTokenProvider) Token(context.Context) (*auth.Token, error) {
|
||||
iat := now()
|
||||
exp := iat.Add(time.Hour)
|
||||
scope := strings.Join(tp.scopes, " ")
|
||||
c := &jwt.Claims{
|
||||
Iss: tp.email,
|
||||
Sub: tp.email,
|
||||
Aud: tp.audience,
|
||||
Scope: scope,
|
||||
Iat: iat.Unix(),
|
||||
Exp: exp.Unix(),
|
||||
}
|
||||
h := &jwt.Header{
|
||||
Algorithm: jwt.HeaderAlgRSA256,
|
||||
Type: jwt.HeaderType,
|
||||
KeyID: string(tp.pkID),
|
||||
}
|
||||
tok, err := jwt.EncodeJWS(h, c, tp.signer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: could not encode JWT: %w", err)
|
||||
}
|
||||
tp.logger.Debug("created self-signed JWT", "token", tok)
|
||||
return &auth.Token{Value: tok, Type: internal.TokenTypeBearer, Expiry: exp}, nil
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package grpctransport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultTCPUserTimeout is the default TCP_USER_TIMEOUT socket option. By
|
||||
// default is 20 seconds.
|
||||
tcpUserTimeoutMilliseconds = 20000
|
||||
|
||||
// Copied from golang.org/x/sys/unix.TCP_USER_TIMEOUT.
|
||||
tcpUserTimeoutOp = 0x12
|
||||
)
|
||||
|
||||
func init() {
|
||||
// timeoutDialerOption is a grpc.DialOption that contains dialer with
|
||||
// socket option TCP_USER_TIMEOUT. This dialer requires go versions 1.11+.
|
||||
timeoutDialerOption = grpc.WithContextDialer(dialTCPUserTimeout)
|
||||
}
|
||||
|
||||
func dialTCPUserTimeout(ctx context.Context, addr string) (net.Conn, error) {
|
||||
control := func(network, address string, c syscall.RawConn) error {
|
||||
var syscallErr error
|
||||
controlErr := c.Control(func(fd uintptr) {
|
||||
syscallErr = syscall.SetsockoptInt(
|
||||
int(fd), syscall.IPPROTO_TCP, tcpUserTimeoutOp, tcpUserTimeoutMilliseconds)
|
||||
})
|
||||
if syscallErr != nil {
|
||||
return syscallErr
|
||||
}
|
||||
if controlErr != nil {
|
||||
return controlErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
d := &net.Dialer{
|
||||
Control: control,
|
||||
}
|
||||
return d.DialContext(ctx, "tcp", addr)
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grpctransport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/internal/compute"
|
||||
"google.golang.org/grpc"
|
||||
grpcgoogle "google.golang.org/grpc/credentials/google"
|
||||
)
|
||||
|
||||
func isDirectPathEnabled(endpoint string, opts *Options) bool {
|
||||
if opts.InternalOptions != nil && !opts.InternalOptions.EnableDirectPath {
|
||||
return false
|
||||
}
|
||||
if !checkDirectPathEndPoint(endpoint) {
|
||||
return false
|
||||
}
|
||||
if b, _ := strconv.ParseBool(os.Getenv(disableDirectPathEnvVar)); b {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func checkDirectPathEndPoint(endpoint string) bool {
|
||||
// Only [dns:///]host[:port] is supported, not other schemes (e.g., "tcp://" or "unix://").
|
||||
// Also don't try direct path if the user has chosen an alternate name resolver
|
||||
// (i.e., via ":///" prefix).
|
||||
if strings.Contains(endpoint, "://") && !strings.HasPrefix(endpoint, "dns:///") {
|
||||
return false
|
||||
}
|
||||
|
||||
if endpoint == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isTokenProviderDirectPathCompatible(tp auth.TokenProvider, o *Options) bool {
|
||||
if tp == nil {
|
||||
return false
|
||||
}
|
||||
tok, err := tp.Token(context.Background())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if tok == nil {
|
||||
return false
|
||||
}
|
||||
if tok.MetadataString("auth.google.tokenSource") != "compute-metadata" {
|
||||
return false
|
||||
}
|
||||
if o.InternalOptions != nil && o.InternalOptions.EnableNonDefaultSAForDirectPath {
|
||||
return true
|
||||
}
|
||||
if tok.MetadataString("auth.google.serviceAccount") != "default" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isDirectPathXdsUsed(o *Options) bool {
|
||||
// Method 1: Enable DirectPath xDS by env;
|
||||
if b, _ := strconv.ParseBool(os.Getenv(enableDirectPathXdsEnvVar)); b {
|
||||
return true
|
||||
}
|
||||
// Method 2: Enable DirectPath xDS by option;
|
||||
if o.InternalOptions != nil && o.InternalOptions.EnableDirectPathXds {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// configureDirectPath returns some dial options and an endpoint to use if the
|
||||
// configuration allows the use of direct path. If it does not the provided
|
||||
// grpcOpts and endpoint are returned.
|
||||
func configureDirectPath(grpcOpts []grpc.DialOption, opts *Options, endpoint string, creds *auth.Credentials) ([]grpc.DialOption, string) {
|
||||
if isDirectPathEnabled(endpoint, opts) && compute.OnComputeEngine() && isTokenProviderDirectPathCompatible(creds, opts) {
|
||||
// Overwrite all of the previously specific DialOptions, DirectPath uses its own set of credentials and certificates.
|
||||
grpcOpts = []grpc.DialOption{
|
||||
grpc.WithCredentialsBundle(grpcgoogle.NewDefaultCredentialsWithOptions(grpcgoogle.DefaultCredentialsOptions{PerRPCCreds: &grpcCredentialsProvider{creds: creds}}))}
|
||||
if timeoutDialerOption != nil {
|
||||
grpcOpts = append(grpcOpts, timeoutDialerOption)
|
||||
}
|
||||
// Check if google-c2p resolver is enabled for DirectPath
|
||||
if isDirectPathXdsUsed(opts) {
|
||||
// google-c2p resolver target must not have a port number
|
||||
if addr, _, err := net.SplitHostPort(endpoint); err == nil {
|
||||
endpoint = "google-c2p:///" + addr
|
||||
} else {
|
||||
endpoint = "google-c2p:///" + endpoint
|
||||
}
|
||||
} else {
|
||||
if !strings.HasPrefix(endpoint, "dns:///") {
|
||||
endpoint = "dns:///" + endpoint
|
||||
}
|
||||
grpcOpts = append(grpcOpts,
|
||||
// For now all DirectPath go clients will be using the following lb config, but in future
|
||||
// when different services need different configs, then we should change this to a
|
||||
// per-service config.
|
||||
grpc.WithDisableServiceConfig(),
|
||||
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"grpclb":{"childPolicy":[{"pick_first":{}}]}}]}`))
|
||||
}
|
||||
// TODO: add support for system parameters (quota project, request reason) via chained interceptor.
|
||||
}
|
||||
return grpcOpts, endpoint
|
||||
}
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package grpctransport provides functionality for managing gRPC client
|
||||
// connections to Google Cloud services.
|
||||
package grpctransport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/credentials"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/transport"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||
"google.golang.org/grpc"
|
||||
grpccreds "google.golang.org/grpc/credentials"
|
||||
grpcinsecure "google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/stats"
|
||||
)
|
||||
|
||||
const (
|
||||
// Check env to disable DirectPath traffic.
|
||||
disableDirectPathEnvVar = "GOOGLE_CLOUD_DISABLE_DIRECT_PATH"
|
||||
|
||||
// Check env to decide if using google-c2p resolver for DirectPath traffic.
|
||||
enableDirectPathXdsEnvVar = "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS"
|
||||
|
||||
quotaProjectHeaderKey = "X-goog-user-project"
|
||||
)
|
||||
|
||||
var (
|
||||
// Set at init time by dial_socketopt.go. If nil, socketopt is not supported.
|
||||
timeoutDialerOption grpc.DialOption
|
||||
)
|
||||
|
||||
// otelStatsHandler is a singleton otelgrpc.clientHandler to be used across
|
||||
// all dial connections to avoid the memory leak documented in
|
||||
// https://github.com/open-telemetry/opentelemetry-go-contrib/issues/4226
|
||||
//
|
||||
// TODO: When this module depends on a version of otelgrpc containing the fix,
|
||||
// replace this singleton with inline usage for simplicity.
|
||||
// The fix should be in https://github.com/open-telemetry/opentelemetry-go/pull/5797.
|
||||
var (
|
||||
initOtelStatsHandlerOnce sync.Once
|
||||
otelStatsHandler stats.Handler
|
||||
)
|
||||
|
||||
// otelGRPCStatsHandler returns singleton otelStatsHandler for reuse across all
|
||||
// dial connections.
|
||||
func otelGRPCStatsHandler() stats.Handler {
|
||||
initOtelStatsHandlerOnce.Do(func() {
|
||||
otelStatsHandler = otelgrpc.NewClientHandler()
|
||||
})
|
||||
return otelStatsHandler
|
||||
}
|
||||
|
||||
// ClientCertProvider is a function that returns a TLS client certificate to be
|
||||
// used when opening TLS connections. It follows the same semantics as
|
||||
// [crypto/tls.Config.GetClientCertificate].
|
||||
type ClientCertProvider = func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
|
||||
|
||||
// Options used to configure a [GRPCClientConnPool] from [Dial].
|
||||
type Options struct {
|
||||
// DisableTelemetry disables default telemetry (OpenTelemetry). An example
|
||||
// reason to do so would be to bind custom telemetry that overrides the
|
||||
// defaults.
|
||||
DisableTelemetry bool
|
||||
// DisableAuthentication specifies that no authentication should be used. It
|
||||
// is suitable only for testing and for accessing public resources, like
|
||||
// public Google Cloud Storage buckets.
|
||||
DisableAuthentication bool
|
||||
// Endpoint overrides the default endpoint to be used for a service.
|
||||
Endpoint string
|
||||
// Metadata is extra gRPC metadata that will be appended to every outgoing
|
||||
// request.
|
||||
Metadata map[string]string
|
||||
// GRPCDialOpts are dial options that will be passed to `grpc.Dial` when
|
||||
// establishing a`grpc.Conn``
|
||||
GRPCDialOpts []grpc.DialOption
|
||||
// PoolSize is specifies how many connections to balance between when making
|
||||
// requests. If unset or less than 1, the value defaults to 1.
|
||||
PoolSize int
|
||||
// Credentials used to add Authorization metadata to all requests. If set
|
||||
// DetectOpts are ignored.
|
||||
Credentials *auth.Credentials
|
||||
// ClientCertProvider is a function that returns a TLS client certificate to
|
||||
// be used when opening TLS connections. It follows the same semantics as
|
||||
// crypto/tls.Config.GetClientCertificate.
|
||||
ClientCertProvider ClientCertProvider
|
||||
// DetectOpts configures settings for detect Application Default
|
||||
// Credentials.
|
||||
DetectOpts *credentials.DetectOptions
|
||||
// UniverseDomain is the default service domain for a given Cloud universe.
|
||||
// The default value is "googleapis.com". This is the universe domain
|
||||
// configured for the client, which will be compared to the universe domain
|
||||
// that is separately configured for the credentials.
|
||||
UniverseDomain string
|
||||
// APIKey specifies an API key to be used as the basis for authentication.
|
||||
// If set DetectOpts are ignored.
|
||||
APIKey string
|
||||
// Logger is used for debug logging. If provided, logging will be enabled
|
||||
// at the loggers configured level. By default logging is disabled unless
|
||||
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
|
||||
// logger will be used. Optional.
|
||||
Logger *slog.Logger
|
||||
|
||||
// InternalOptions are NOT meant to be set directly by consumers of this
|
||||
// package, they should only be set by generated client code.
|
||||
InternalOptions *InternalOptions
|
||||
}
|
||||
|
||||
// client returns the client a user set for the detect options or nil if one was
|
||||
// not set.
|
||||
func (o *Options) client() *http.Client {
|
||||
if o.DetectOpts != nil && o.DetectOpts.Client != nil {
|
||||
return o.DetectOpts.Client
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Options) logger() *slog.Logger {
|
||||
return internallog.New(o.Logger)
|
||||
}
|
||||
|
||||
func (o *Options) validate() error {
|
||||
if o == nil {
|
||||
return errors.New("grpctransport: opts required to be non-nil")
|
||||
}
|
||||
if o.InternalOptions != nil && o.InternalOptions.SkipValidation {
|
||||
return nil
|
||||
}
|
||||
hasCreds := o.APIKey != "" ||
|
||||
o.Credentials != nil ||
|
||||
(o.DetectOpts != nil && len(o.DetectOpts.CredentialsJSON) > 0) ||
|
||||
(o.DetectOpts != nil && o.DetectOpts.CredentialsFile != "")
|
||||
if o.DisableAuthentication && hasCreds {
|
||||
return errors.New("grpctransport: DisableAuthentication is incompatible with options that set or detect credentials")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Options) resolveDetectOptions() *credentials.DetectOptions {
|
||||
io := o.InternalOptions
|
||||
// soft-clone these so we are not updating a ref the user holds and may reuse
|
||||
do := transport.CloneDetectOptions(o.DetectOpts)
|
||||
|
||||
// If scoped JWTs are enabled user provided an aud, allow self-signed JWT.
|
||||
if (io != nil && io.EnableJWTWithScope) || do.Audience != "" {
|
||||
do.UseSelfSignedJWT = true
|
||||
}
|
||||
// Only default scopes if user did not also set an audience.
|
||||
if len(do.Scopes) == 0 && do.Audience == "" && io != nil && len(io.DefaultScopes) > 0 {
|
||||
do.Scopes = make([]string, len(io.DefaultScopes))
|
||||
copy(do.Scopes, io.DefaultScopes)
|
||||
}
|
||||
if len(do.Scopes) == 0 && do.Audience == "" && io != nil {
|
||||
do.Audience = o.InternalOptions.DefaultAudience
|
||||
}
|
||||
if o.ClientCertProvider != nil {
|
||||
tlsConfig := &tls.Config{
|
||||
GetClientCertificate: o.ClientCertProvider,
|
||||
}
|
||||
do.Client = transport.DefaultHTTPClientWithTLS(tlsConfig)
|
||||
do.TokenURL = credentials.GoogleMTLSTokenURL
|
||||
}
|
||||
if do.Logger == nil {
|
||||
do.Logger = o.logger()
|
||||
}
|
||||
return do
|
||||
}
|
||||
|
||||
// InternalOptions are only meant to be set by generated client code. These are
|
||||
// not meant to be set directly by consumers of this package. Configuration in
|
||||
// this type is considered EXPERIMENTAL and may be removed at any time in the
|
||||
// future without warning.
|
||||
type InternalOptions struct {
|
||||
// EnableNonDefaultSAForDirectPath overrides the default requirement for
|
||||
// using the default service account for DirectPath.
|
||||
EnableNonDefaultSAForDirectPath bool
|
||||
// EnableDirectPath overrides the default attempt to use DirectPath.
|
||||
EnableDirectPath bool
|
||||
// EnableDirectPathXds overrides the default DirectPath type. It is only
|
||||
// valid when DirectPath is enabled.
|
||||
EnableDirectPathXds bool
|
||||
// EnableJWTWithScope specifies if scope can be used with self-signed JWT.
|
||||
EnableJWTWithScope bool
|
||||
// DefaultAudience specifies a default audience to be used as the audience
|
||||
// field ("aud") for the JWT token authentication.
|
||||
DefaultAudience string
|
||||
// DefaultEndpointTemplate combined with UniverseDomain specifies
|
||||
// the default endpoint.
|
||||
DefaultEndpointTemplate string
|
||||
// DefaultMTLSEndpoint specifies the default mTLS endpoint.
|
||||
DefaultMTLSEndpoint string
|
||||
// DefaultScopes specifies the default OAuth2 scopes to be used for a
|
||||
// service.
|
||||
DefaultScopes []string
|
||||
// SkipValidation bypasses validation on Options. It should only be used
|
||||
// internally for clients that needs more control over their transport.
|
||||
SkipValidation bool
|
||||
}
|
||||
|
||||
// Dial returns a GRPCClientConnPool that can be used to communicate with a
|
||||
// Google cloud service, configured with the provided [Options]. It
|
||||
// automatically appends Authorization metadata to all outgoing requests.
|
||||
func Dial(ctx context.Context, secure bool, opts *Options) (GRPCClientConnPool, error) {
|
||||
if err := opts.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts.PoolSize <= 1 {
|
||||
conn, err := dial(ctx, secure, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &singleConnPool{conn}, nil
|
||||
}
|
||||
pool := &roundRobinConnPool{}
|
||||
for i := 0; i < opts.PoolSize; i++ {
|
||||
conn, err := dial(ctx, secure, opts)
|
||||
if err != nil {
|
||||
// ignore close error, if any
|
||||
defer pool.Close()
|
||||
return nil, err
|
||||
}
|
||||
pool.conns = append(pool.conns, conn)
|
||||
}
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// return a GRPCClientConnPool if pool == 1 or else a pool of of them if >1
|
||||
func dial(ctx context.Context, secure bool, opts *Options) (*grpc.ClientConn, error) {
|
||||
tOpts := &transport.Options{
|
||||
Endpoint: opts.Endpoint,
|
||||
ClientCertProvider: opts.ClientCertProvider,
|
||||
Client: opts.client(),
|
||||
UniverseDomain: opts.UniverseDomain,
|
||||
Logger: opts.logger(),
|
||||
}
|
||||
if io := opts.InternalOptions; io != nil {
|
||||
tOpts.DefaultEndpointTemplate = io.DefaultEndpointTemplate
|
||||
tOpts.DefaultMTLSEndpoint = io.DefaultMTLSEndpoint
|
||||
tOpts.EnableDirectPath = io.EnableDirectPath
|
||||
tOpts.EnableDirectPathXds = io.EnableDirectPathXds
|
||||
}
|
||||
transportCreds, endpoint, err := transport.GetGRPCTransportCredsAndEndpoint(tOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !secure {
|
||||
transportCreds = grpcinsecure.NewCredentials()
|
||||
}
|
||||
|
||||
// Initialize gRPC dial options with transport-level security options.
|
||||
grpcOpts := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(transportCreds),
|
||||
}
|
||||
|
||||
// Ensure the token exchange HTTP transport uses the same ClientCertProvider as the GRPC API transport.
|
||||
opts.ClientCertProvider, err = transport.GetClientCertificateProvider(tOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.APIKey != "" {
|
||||
grpcOpts = append(grpcOpts,
|
||||
grpc.WithPerRPCCredentials(&grpcKeyProvider{
|
||||
apiKey: opts.APIKey,
|
||||
metadata: opts.Metadata,
|
||||
secure: secure,
|
||||
}),
|
||||
)
|
||||
} else if !opts.DisableAuthentication {
|
||||
metadata := opts.Metadata
|
||||
|
||||
var creds *auth.Credentials
|
||||
if opts.Credentials != nil {
|
||||
creds = opts.Credentials
|
||||
} else {
|
||||
var err error
|
||||
creds, err = credentials.DetectDefault(opts.resolveDetectOptions())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
qp, err := creds.QuotaProjectID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if qp != "" {
|
||||
if metadata == nil {
|
||||
metadata = make(map[string]string, 1)
|
||||
}
|
||||
// Don't overwrite user specified quota
|
||||
if _, ok := metadata[quotaProjectHeaderKey]; !ok {
|
||||
metadata[quotaProjectHeaderKey] = qp
|
||||
}
|
||||
}
|
||||
grpcOpts = append(grpcOpts,
|
||||
grpc.WithPerRPCCredentials(&grpcCredentialsProvider{
|
||||
creds: creds,
|
||||
metadata: metadata,
|
||||
clientUniverseDomain: opts.UniverseDomain,
|
||||
}),
|
||||
)
|
||||
|
||||
// Attempt Direct Path
|
||||
grpcOpts, endpoint = configureDirectPath(grpcOpts, opts, endpoint, creds)
|
||||
}
|
||||
|
||||
// Add tracing, but before the other options, so that clients can override the
|
||||
// gRPC stats handler.
|
||||
// This assumes that gRPC options are processed in order, left to right.
|
||||
grpcOpts = addOpenTelemetryStatsHandler(grpcOpts, opts)
|
||||
grpcOpts = append(grpcOpts, opts.GRPCDialOpts...)
|
||||
|
||||
return grpc.Dial(endpoint, grpcOpts...)
|
||||
}
|
||||
|
||||
// grpcKeyProvider satisfies https://pkg.go.dev/google.golang.org/grpc/credentials#PerRPCCredentials.
|
||||
type grpcKeyProvider struct {
|
||||
apiKey string
|
||||
metadata map[string]string
|
||||
secure bool
|
||||
}
|
||||
|
||||
func (g *grpcKeyProvider) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||
metadata := make(map[string]string, len(g.metadata)+1)
|
||||
metadata["X-goog-api-key"] = g.apiKey
|
||||
for k, v := range g.metadata {
|
||||
metadata[k] = v
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func (g *grpcKeyProvider) RequireTransportSecurity() bool {
|
||||
return g.secure
|
||||
}
|
||||
|
||||
// grpcCredentialsProvider satisfies https://pkg.go.dev/google.golang.org/grpc/credentials#PerRPCCredentials.
|
||||
type grpcCredentialsProvider struct {
|
||||
creds *auth.Credentials
|
||||
|
||||
secure bool
|
||||
|
||||
// Additional metadata attached as headers.
|
||||
metadata map[string]string
|
||||
clientUniverseDomain string
|
||||
}
|
||||
|
||||
// getClientUniverseDomain returns the default service domain for a given Cloud
|
||||
// universe, with the following precedence:
|
||||
//
|
||||
// 1. A non-empty option.WithUniverseDomain or similar client option.
|
||||
// 2. A non-empty environment variable GOOGLE_CLOUD_UNIVERSE_DOMAIN.
|
||||
// 3. The default value "googleapis.com".
|
||||
//
|
||||
// This is the universe domain configured for the client, which will be compared
|
||||
// to the universe domain that is separately configured for the credentials.
|
||||
func (c *grpcCredentialsProvider) getClientUniverseDomain() string {
|
||||
if c.clientUniverseDomain != "" {
|
||||
return c.clientUniverseDomain
|
||||
}
|
||||
if envUD := os.Getenv(internal.UniverseDomainEnvVar); envUD != "" {
|
||||
return envUD
|
||||
}
|
||||
return internal.DefaultUniverseDomain
|
||||
}
|
||||
|
||||
func (c *grpcCredentialsProvider) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||
token, err := c.creds.Token(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token.MetadataString("auth.google.tokenSource") != "compute-metadata" {
|
||||
credentialsUniverseDomain, err := c.creds.UniverseDomain(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := transport.ValidateUniverseDomain(c.getClientUniverseDomain(), credentialsUniverseDomain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if c.secure {
|
||||
ri, _ := grpccreds.RequestInfoFromContext(ctx)
|
||||
if err = grpccreds.CheckSecurityLevel(ri.AuthInfo, grpccreds.PrivacyAndIntegrity); err != nil {
|
||||
return nil, fmt.Errorf("unable to transfer credentials PerRPCCredentials: %v", err)
|
||||
}
|
||||
}
|
||||
metadata := make(map[string]string, len(c.metadata)+1)
|
||||
setAuthMetadata(token, metadata)
|
||||
for k, v := range c.metadata {
|
||||
metadata[k] = v
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
// setAuthMetadata uses the provided token to set the Authorization metadata.
|
||||
// If the token.Type is empty, the type is assumed to be Bearer.
|
||||
func setAuthMetadata(token *auth.Token, m map[string]string) {
|
||||
typ := token.Type
|
||||
if typ == "" {
|
||||
typ = internal.TokenTypeBearer
|
||||
}
|
||||
m["authorization"] = typ + " " + token.Value
|
||||
}
|
||||
|
||||
func (c *grpcCredentialsProvider) RequireTransportSecurity() bool {
|
||||
return c.secure
|
||||
}
|
||||
|
||||
func addOpenTelemetryStatsHandler(dialOpts []grpc.DialOption, opts *Options) []grpc.DialOption {
|
||||
if opts.DisableTelemetry {
|
||||
return dialOpts
|
||||
}
|
||||
return append(dialOpts, grpc.WithStatsHandler(otelGRPCStatsHandler()))
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grpctransport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// GRPCClientConnPool is an interface that satisfies
|
||||
// [google.golang.org/grpc.ClientConnInterface] and has some utility functions
|
||||
// that are needed for connection lifecycle when using in a client library. It
|
||||
// may be a pool or a single connection. This interface is not intended to, and
|
||||
// can't be, implemented by others.
|
||||
type GRPCClientConnPool interface {
|
||||
// Connection returns a [google.golang.org/grpc.ClientConn] from the pool.
|
||||
//
|
||||
// ClientConn aren't returned to the pool and should not be closed directly.
|
||||
Connection() *grpc.ClientConn
|
||||
|
||||
// Len returns the number of connections in the pool. It will always return
|
||||
// the same value.
|
||||
Len() int
|
||||
|
||||
// Close closes every ClientConn in the pool. The error returned by Close
|
||||
// may be a single error or multiple errors.
|
||||
Close() error
|
||||
|
||||
grpc.ClientConnInterface
|
||||
|
||||
// private ensure others outside this package can't implement this type
|
||||
private()
|
||||
}
|
||||
|
||||
// singleConnPool is a special case for a single connection.
|
||||
type singleConnPool struct {
|
||||
*grpc.ClientConn
|
||||
}
|
||||
|
||||
func (p *singleConnPool) Connection() *grpc.ClientConn { return p.ClientConn }
|
||||
func (p *singleConnPool) Len() int { return 1 }
|
||||
func (p *singleConnPool) private() {}
|
||||
|
||||
type roundRobinConnPool struct {
|
||||
conns []*grpc.ClientConn
|
||||
|
||||
idx uint32 // access via sync/atomic
|
||||
}
|
||||
|
||||
func (p *roundRobinConnPool) Len() int {
|
||||
return len(p.conns)
|
||||
}
|
||||
|
||||
func (p *roundRobinConnPool) Connection() *grpc.ClientConn {
|
||||
i := atomic.AddUint32(&p.idx, 1)
|
||||
return p.conns[i%uint32(len(p.conns))]
|
||||
}
|
||||
|
||||
func (p *roundRobinConnPool) Close() error {
|
||||
var errs multiError
|
||||
for _, conn := range p.conns {
|
||||
if err := conn.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (p *roundRobinConnPool) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error {
|
||||
return p.Connection().Invoke(ctx, method, args, reply, opts...)
|
||||
}
|
||||
|
||||
func (p *roundRobinConnPool) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
return p.Connection().NewStream(ctx, desc, method, opts...)
|
||||
}
|
||||
|
||||
func (p *roundRobinConnPool) private() {}
|
||||
|
||||
// multiError represents errors from multiple conns in the group.
|
||||
type multiError []error
|
||||
|
||||
func (m multiError) Error() string {
|
||||
s, n := "", 0
|
||||
for _, e := range m {
|
||||
if e != nil {
|
||||
if n == 0 {
|
||||
s = e.Error()
|
||||
}
|
||||
n++
|
||||
}
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return "(0 errors)"
|
||||
case 1:
|
||||
return s
|
||||
case 2:
|
||||
return s + " (and 1 other error)"
|
||||
}
|
||||
return fmt.Sprintf("%s (and %d other errors)", s, n-1)
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package httptransport provides functionality for managing HTTP client
|
||||
// connections to Google Cloud services.
|
||||
package httptransport
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
detect "cloud.google.com/go/auth/credentials"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/transport"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
)
|
||||
|
||||
// ClientCertProvider is a function that returns a TLS client certificate to be
|
||||
// used when opening TLS connections. It follows the same semantics as
|
||||
// [crypto/tls.Config.GetClientCertificate].
|
||||
type ClientCertProvider = func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
|
||||
|
||||
// Options used to configure a [net/http.Client] from [NewClient].
|
||||
type Options struct {
|
||||
// DisableTelemetry disables default telemetry (OpenTelemetry). An example
|
||||
// reason to do so would be to bind custom telemetry that overrides the
|
||||
// defaults.
|
||||
DisableTelemetry bool
|
||||
// DisableAuthentication specifies that no authentication should be used. It
|
||||
// is suitable only for testing and for accessing public resources, like
|
||||
// public Google Cloud Storage buckets.
|
||||
DisableAuthentication bool
|
||||
// Headers are extra HTTP headers that will be appended to every outgoing
|
||||
// request.
|
||||
Headers http.Header
|
||||
// BaseRoundTripper overrides the base transport used for serving requests.
|
||||
// If specified ClientCertProvider is ignored.
|
||||
BaseRoundTripper http.RoundTripper
|
||||
// Endpoint overrides the default endpoint to be used for a service.
|
||||
Endpoint string
|
||||
// APIKey specifies an API key to be used as the basis for authentication.
|
||||
// If set DetectOpts are ignored.
|
||||
APIKey string
|
||||
// Credentials used to add Authorization header to all requests. If set
|
||||
// DetectOpts are ignored.
|
||||
Credentials *auth.Credentials
|
||||
// ClientCertProvider is a function that returns a TLS client certificate to
|
||||
// be used when opening TLS connections. It follows the same semantics as
|
||||
// crypto/tls.Config.GetClientCertificate.
|
||||
ClientCertProvider ClientCertProvider
|
||||
// DetectOpts configures settings for detect Application Default
|
||||
// Credentials.
|
||||
DetectOpts *detect.DetectOptions
|
||||
// UniverseDomain is the default service domain for a given Cloud universe.
|
||||
// The default value is "googleapis.com". This is the universe domain
|
||||
// configured for the client, which will be compared to the universe domain
|
||||
// that is separately configured for the credentials.
|
||||
UniverseDomain string
|
||||
// Logger is used for debug logging. If provided, logging will be enabled
|
||||
// at the loggers configured level. By default logging is disabled unless
|
||||
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
|
||||
// logger will be used. Optional.
|
||||
Logger *slog.Logger
|
||||
|
||||
// InternalOptions are NOT meant to be set directly by consumers of this
|
||||
// package, they should only be set by generated client code.
|
||||
InternalOptions *InternalOptions
|
||||
}
|
||||
|
||||
func (o *Options) validate() error {
|
||||
if o == nil {
|
||||
return errors.New("httptransport: opts required to be non-nil")
|
||||
}
|
||||
if o.InternalOptions != nil && o.InternalOptions.SkipValidation {
|
||||
return nil
|
||||
}
|
||||
hasCreds := o.APIKey != "" ||
|
||||
o.Credentials != nil ||
|
||||
(o.DetectOpts != nil && len(o.DetectOpts.CredentialsJSON) > 0) ||
|
||||
(o.DetectOpts != nil && o.DetectOpts.CredentialsFile != "")
|
||||
if o.DisableAuthentication && hasCreds {
|
||||
return errors.New("httptransport: DisableAuthentication is incompatible with options that set or detect credentials")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// client returns the client a user set for the detect options or nil if one was
|
||||
// not set.
|
||||
func (o *Options) client() *http.Client {
|
||||
if o.DetectOpts != nil && o.DetectOpts.Client != nil {
|
||||
return o.DetectOpts.Client
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Options) logger() *slog.Logger {
|
||||
return internallog.New(o.Logger)
|
||||
}
|
||||
|
||||
func (o *Options) resolveDetectOptions() *detect.DetectOptions {
|
||||
io := o.InternalOptions
|
||||
// soft-clone these so we are not updating a ref the user holds and may reuse
|
||||
do := transport.CloneDetectOptions(o.DetectOpts)
|
||||
|
||||
// If scoped JWTs are enabled user provided an aud, allow self-signed JWT.
|
||||
if (io != nil && io.EnableJWTWithScope) || do.Audience != "" {
|
||||
do.UseSelfSignedJWT = true
|
||||
}
|
||||
// Only default scopes if user did not also set an audience.
|
||||
if len(do.Scopes) == 0 && do.Audience == "" && io != nil && len(io.DefaultScopes) > 0 {
|
||||
do.Scopes = make([]string, len(io.DefaultScopes))
|
||||
copy(do.Scopes, io.DefaultScopes)
|
||||
}
|
||||
if len(do.Scopes) == 0 && do.Audience == "" && io != nil {
|
||||
do.Audience = o.InternalOptions.DefaultAudience
|
||||
}
|
||||
if o.ClientCertProvider != nil {
|
||||
tlsConfig := &tls.Config{
|
||||
GetClientCertificate: o.ClientCertProvider,
|
||||
}
|
||||
do.Client = transport.DefaultHTTPClientWithTLS(tlsConfig)
|
||||
do.TokenURL = detect.GoogleMTLSTokenURL
|
||||
}
|
||||
if do.Logger == nil {
|
||||
do.Logger = o.logger()
|
||||
}
|
||||
return do
|
||||
}
|
||||
|
||||
// InternalOptions are only meant to be set by generated client code. These are
|
||||
// not meant to be set directly by consumers of this package. Configuration in
|
||||
// this type is considered EXPERIMENTAL and may be removed at any time in the
|
||||
// future without warning.
|
||||
type InternalOptions struct {
|
||||
// EnableJWTWithScope specifies if scope can be used with self-signed JWT.
|
||||
EnableJWTWithScope bool
|
||||
// DefaultAudience specifies a default audience to be used as the audience
|
||||
// field ("aud") for the JWT token authentication.
|
||||
DefaultAudience string
|
||||
// DefaultEndpointTemplate combined with UniverseDomain specifies the
|
||||
// default endpoint.
|
||||
DefaultEndpointTemplate string
|
||||
// DefaultMTLSEndpoint specifies the default mTLS endpoint.
|
||||
DefaultMTLSEndpoint string
|
||||
// DefaultScopes specifies the default OAuth2 scopes to be used for a
|
||||
// service.
|
||||
DefaultScopes []string
|
||||
// SkipValidation bypasses validation on Options. It should only be used
|
||||
// internally for clients that need more control over their transport.
|
||||
SkipValidation bool
|
||||
// SkipUniverseDomainValidation skips the verification that the universe
|
||||
// domain configured for the client matches the universe domain configured
|
||||
// for the credentials. It should only be used internally for clients that
|
||||
// need more control over their transport. The default is false.
|
||||
SkipUniverseDomainValidation bool
|
||||
}
|
||||
|
||||
// AddAuthorizationMiddleware adds a middleware to the provided client's
|
||||
// transport that sets the Authorization header with the value produced by the
|
||||
// provided [cloud.google.com/go/auth.Credentials]. An error is returned only
|
||||
// if client or creds is nil.
|
||||
//
|
||||
// This function does not support setting a universe domain value on the client.
|
||||
func AddAuthorizationMiddleware(client *http.Client, creds *auth.Credentials) error {
|
||||
if client == nil || creds == nil {
|
||||
return fmt.Errorf("httptransport: client and tp must not be nil")
|
||||
}
|
||||
base := client.Transport
|
||||
if base == nil {
|
||||
if dt, ok := http.DefaultTransport.(*http.Transport); ok {
|
||||
base = dt.Clone()
|
||||
} else {
|
||||
// Directly reuse the DefaultTransport if the application has
|
||||
// replaced it with an implementation of RoundTripper other than
|
||||
// http.Transport.
|
||||
base = http.DefaultTransport
|
||||
}
|
||||
}
|
||||
client.Transport = &authTransport{
|
||||
creds: creds,
|
||||
base: base,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewClient returns a [net/http.Client] that can be used to communicate with a
|
||||
// Google cloud service, configured with the provided [Options]. It
|
||||
// automatically appends Authorization headers to all outgoing requests.
|
||||
func NewClient(opts *Options) (*http.Client, error) {
|
||||
if err := opts.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tOpts := &transport.Options{
|
||||
Endpoint: opts.Endpoint,
|
||||
ClientCertProvider: opts.ClientCertProvider,
|
||||
Client: opts.client(),
|
||||
UniverseDomain: opts.UniverseDomain,
|
||||
Logger: opts.logger(),
|
||||
}
|
||||
if io := opts.InternalOptions; io != nil {
|
||||
tOpts.DefaultEndpointTemplate = io.DefaultEndpointTemplate
|
||||
tOpts.DefaultMTLSEndpoint = io.DefaultMTLSEndpoint
|
||||
}
|
||||
clientCertProvider, dialTLSContext, err := transport.GetHTTPTransportConfig(tOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseRoundTripper := opts.BaseRoundTripper
|
||||
if baseRoundTripper == nil {
|
||||
baseRoundTripper = defaultBaseTransport(clientCertProvider, dialTLSContext)
|
||||
}
|
||||
// Ensure the token exchange transport uses the same ClientCertProvider as the API transport.
|
||||
opts.ClientCertProvider = clientCertProvider
|
||||
trans, err := newTransport(baseRoundTripper, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: trans,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetAuthHeader uses the provided token to set the Authorization header on a
|
||||
// request. If the token.Type is empty, the type is assumed to be Bearer.
|
||||
func SetAuthHeader(token *auth.Token, req *http.Request) {
|
||||
typ := token.Type
|
||||
if typ == "" {
|
||||
typ = internal.TokenTypeBearer
|
||||
}
|
||||
req.Header.Set("Authorization", typ+" "+token.Value)
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httptransport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/credentials"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/transport"
|
||||
"cloud.google.com/go/auth/internal/transport/cert"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
quotaProjectHeaderKey = "X-goog-user-project"
|
||||
)
|
||||
|
||||
func newTransport(base http.RoundTripper, opts *Options) (http.RoundTripper, error) {
|
||||
var headers = opts.Headers
|
||||
ht := &headerTransport{
|
||||
base: base,
|
||||
headers: headers,
|
||||
}
|
||||
var trans http.RoundTripper = ht
|
||||
trans = addOpenTelemetryTransport(trans, opts)
|
||||
switch {
|
||||
case opts.DisableAuthentication:
|
||||
// Do nothing.
|
||||
case opts.APIKey != "":
|
||||
qp := internal.GetQuotaProject(nil, opts.Headers.Get(quotaProjectHeaderKey))
|
||||
if qp != "" {
|
||||
if headers == nil {
|
||||
headers = make(map[string][]string, 1)
|
||||
}
|
||||
headers.Set(quotaProjectHeaderKey, qp)
|
||||
}
|
||||
trans = &apiKeyTransport{
|
||||
Transport: trans,
|
||||
Key: opts.APIKey,
|
||||
}
|
||||
default:
|
||||
var creds *auth.Credentials
|
||||
if opts.Credentials != nil {
|
||||
creds = opts.Credentials
|
||||
} else {
|
||||
var err error
|
||||
creds, err = credentials.DetectDefault(opts.resolveDetectOptions())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
qp, err := creds.QuotaProjectID(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if qp != "" {
|
||||
if headers == nil {
|
||||
headers = make(map[string][]string, 1)
|
||||
}
|
||||
// Don't overwrite user specified quota
|
||||
if v := headers.Get(quotaProjectHeaderKey); v == "" {
|
||||
headers.Set(quotaProjectHeaderKey, qp)
|
||||
}
|
||||
}
|
||||
var skipUD bool
|
||||
if iOpts := opts.InternalOptions; iOpts != nil {
|
||||
skipUD = iOpts.SkipUniverseDomainValidation
|
||||
}
|
||||
creds.TokenProvider = auth.NewCachedTokenProvider(creds.TokenProvider, nil)
|
||||
trans = &authTransport{
|
||||
base: trans,
|
||||
creds: creds,
|
||||
clientUniverseDomain: opts.UniverseDomain,
|
||||
skipUniverseDomainValidation: skipUD,
|
||||
}
|
||||
}
|
||||
return trans, nil
|
||||
}
|
||||
|
||||
// defaultBaseTransport returns the base HTTP transport.
|
||||
// On App Engine, this is urlfetch.Transport.
|
||||
// Otherwise, use a default transport, taking most defaults from
|
||||
// http.DefaultTransport.
|
||||
// If TLSCertificate is available, set TLSClientConfig as well.
|
||||
func defaultBaseTransport(clientCertSource cert.Provider, dialTLSContext func(context.Context, string, string) (net.Conn, error)) http.RoundTripper {
|
||||
defaultTransport, ok := http.DefaultTransport.(*http.Transport)
|
||||
if !ok {
|
||||
defaultTransport = transport.BaseTransport()
|
||||
}
|
||||
trans := defaultTransport.Clone()
|
||||
trans.MaxIdleConnsPerHost = 100
|
||||
|
||||
if clientCertSource != nil {
|
||||
trans.TLSClientConfig = &tls.Config{
|
||||
GetClientCertificate: clientCertSource,
|
||||
}
|
||||
}
|
||||
if dialTLSContext != nil {
|
||||
// If DialTLSContext is set, TLSClientConfig wil be ignored
|
||||
trans.DialTLSContext = dialTLSContext
|
||||
}
|
||||
|
||||
// Configures the ReadIdleTimeout HTTP/2 option for the
|
||||
// transport. This allows broken idle connections to be pruned more quickly,
|
||||
// preventing the client from attempting to re-use connections that will no
|
||||
// longer work.
|
||||
http2Trans, err := http2.ConfigureTransports(trans)
|
||||
if err == nil {
|
||||
http2Trans.ReadIdleTimeout = time.Second * 31
|
||||
}
|
||||
|
||||
return trans
|
||||
}
|
||||
|
||||
type apiKeyTransport struct {
|
||||
// Key is the API Key to set on requests.
|
||||
Key string
|
||||
// Transport is the underlying HTTP transport.
|
||||
// If nil, http.DefaultTransport is used.
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *apiKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
newReq := *req
|
||||
args := newReq.URL.Query()
|
||||
args.Set("key", t.Key)
|
||||
newReq.URL.RawQuery = args.Encode()
|
||||
return t.Transport.RoundTrip(&newReq)
|
||||
}
|
||||
|
||||
type headerTransport struct {
|
||||
headers http.Header
|
||||
base http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
rt := t.base
|
||||
newReq := *req
|
||||
newReq.Header = make(http.Header)
|
||||
for k, vv := range req.Header {
|
||||
newReq.Header[k] = vv
|
||||
}
|
||||
|
||||
for k, v := range t.headers {
|
||||
newReq.Header[k] = v
|
||||
}
|
||||
|
||||
return rt.RoundTrip(&newReq)
|
||||
}
|
||||
|
||||
func addOpenTelemetryTransport(trans http.RoundTripper, opts *Options) http.RoundTripper {
|
||||
if opts.DisableTelemetry {
|
||||
return trans
|
||||
}
|
||||
return otelhttp.NewTransport(trans)
|
||||
}
|
||||
|
||||
type authTransport struct {
|
||||
creds *auth.Credentials
|
||||
base http.RoundTripper
|
||||
clientUniverseDomain string
|
||||
skipUniverseDomainValidation bool
|
||||
}
|
||||
|
||||
// getClientUniverseDomain returns the default service domain for a given Cloud
|
||||
// universe, with the following precedence:
|
||||
//
|
||||
// 1. A non-empty option.WithUniverseDomain or similar client option.
|
||||
// 2. A non-empty environment variable GOOGLE_CLOUD_UNIVERSE_DOMAIN.
|
||||
// 3. The default value "googleapis.com".
|
||||
//
|
||||
// This is the universe domain configured for the client, which will be compared
|
||||
// to the universe domain that is separately configured for the credentials.
|
||||
func (t *authTransport) getClientUniverseDomain() string {
|
||||
if t.clientUniverseDomain != "" {
|
||||
return t.clientUniverseDomain
|
||||
}
|
||||
if envUD := os.Getenv(internal.UniverseDomainEnvVar); envUD != "" {
|
||||
return envUD
|
||||
}
|
||||
return internal.DefaultUniverseDomain
|
||||
}
|
||||
|
||||
// RoundTrip authorizes and authenticates the request with an
|
||||
// access token from Transport's Source. Per the RoundTripper contract we must
|
||||
// not modify the initial request, so we clone it, and we must close the body
|
||||
// on any errors that happens during our token logic.
|
||||
func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
reqBodyClosed := false
|
||||
if req.Body != nil {
|
||||
defer func() {
|
||||
if !reqBodyClosed {
|
||||
req.Body.Close()
|
||||
}
|
||||
}()
|
||||
}
|
||||
token, err := t.creds.Token(req.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !t.skipUniverseDomainValidation && token.MetadataString("auth.google.tokenSource") != "compute-metadata" {
|
||||
credentialsUniverseDomain, err := t.creds.UniverseDomain(req.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := transport.ValidateUniverseDomain(t.getClientUniverseDomain(), credentialsUniverseDomain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
req2 := req.Clone(req.Context())
|
||||
SetAuthHeader(token, req2)
|
||||
reqBodyClosed = true
|
||||
return t.base.RoundTrip(req2)
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package compute
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
vmOnGCEOnce sync.Once
|
||||
vmOnGCE bool
|
||||
)
|
||||
|
||||
// OnComputeEngine returns whether the client is running on GCE.
|
||||
//
|
||||
// This is a copy of the gRPC internal googlecloud.OnGCE() func at:
|
||||
// https://github.com/grpc/grpc-go/blob/master/internal/googlecloud/googlecloud.go
|
||||
// The functionality is similar to the metadata.OnGCE() func at:
|
||||
// https://github.com/googleapis/google-cloud-go/blob/main/compute/metadata/metadata.go
|
||||
// The difference is that OnComputeEngine() does not perform HTTP or DNS check on the metadata server.
|
||||
// In particular, OnComputeEngine() will return false on Serverless.
|
||||
func OnComputeEngine() bool {
|
||||
vmOnGCEOnce.Do(func() {
|
||||
mf, err := manufacturer()
|
||||
if err != nil {
|
||||
log.Printf("Failed to read manufacturer, vmOnGCE=false: %v", err)
|
||||
return
|
||||
}
|
||||
vmOnGCE = isRunningOnGCE(mf, runtime.GOOS)
|
||||
})
|
||||
return vmOnGCE
|
||||
}
|
||||
|
||||
// isRunningOnGCE checks whether the local system, without doing a network request, is
|
||||
// running on GCP.
|
||||
func isRunningOnGCE(manufacturer []byte, goos string) bool {
|
||||
name := string(manufacturer)
|
||||
switch goos {
|
||||
case "linux":
|
||||
name = strings.TrimSpace(name)
|
||||
return name == "Google" || name == "Google Compute Engine"
|
||||
case "windows":
|
||||
name = strings.Replace(name, " ", "", -1)
|
||||
name = strings.Replace(name, "\n", "", -1)
|
||||
name = strings.Replace(name, "\r", "", -1)
|
||||
return name == "Google"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
//go:build !(linux || windows)
|
||||
// +build !linux,!windows
|
||||
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package compute
|
||||
|
||||
func manufacturer() ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package compute
|
||||
|
||||
import "os"
|
||||
|
||||
const linuxProductNameFile = "/sys/class/dmi/id/product_name"
|
||||
|
||||
func manufacturer() ([]byte, error) {
|
||||
return os.ReadFile(linuxProductNameFile)
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package compute
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
windowsCheckCommand = "powershell.exe"
|
||||
windowsCheckCommandArgs = "Get-WmiObject -Class Win32_BIOS"
|
||||
powershellOutputFilter = "Manufacturer"
|
||||
windowsManufacturerRegex = ":(.*)"
|
||||
)
|
||||
|
||||
func manufacturer() ([]byte, error) {
|
||||
cmd := exec.Command(windowsCheckCommand, windowsCheckCommandArgs)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, line := range strings.Split(strings.TrimSuffix(string(out), "\n"), "\n") {
|
||||
if strings.HasPrefix(line, powershellOutputFilter) {
|
||||
re := regexp.MustCompile(windowsManufacturerRegex)
|
||||
name := re.FindString(line)
|
||||
name = strings.TrimLeft(name, ":")
|
||||
return []byte(name), nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("cannot determine the machine's manufacturer")
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package credsfile is meant to hide implementation details from the pubic
|
||||
// surface of the detect package. It should not import any other packages in
|
||||
// this module. It is located under the main internal package so other
|
||||
// sub-packages can use these parsed types as well.
|
||||
package credsfile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
// GoogleAppCredsEnvVar is the environment variable for setting the
|
||||
// application default credentials.
|
||||
GoogleAppCredsEnvVar = "GOOGLE_APPLICATION_CREDENTIALS"
|
||||
userCredsFilename = "application_default_credentials.json"
|
||||
)
|
||||
|
||||
// CredentialType represents different credential filetypes Google credentials
|
||||
// can be.
|
||||
type CredentialType int
|
||||
|
||||
const (
|
||||
// UnknownCredType is an unidentified file type.
|
||||
UnknownCredType CredentialType = iota
|
||||
// UserCredentialsKey represents a user creds file type.
|
||||
UserCredentialsKey
|
||||
// ServiceAccountKey represents a service account file type.
|
||||
ServiceAccountKey
|
||||
// ImpersonatedServiceAccountKey represents a impersonated service account
|
||||
// file type.
|
||||
ImpersonatedServiceAccountKey
|
||||
// ExternalAccountKey represents a external account file type.
|
||||
ExternalAccountKey
|
||||
// GDCHServiceAccountKey represents a GDCH file type.
|
||||
GDCHServiceAccountKey
|
||||
// ExternalAccountAuthorizedUserKey represents a external account authorized
|
||||
// user file type.
|
||||
ExternalAccountAuthorizedUserKey
|
||||
)
|
||||
|
||||
// parseCredentialType returns the associated filetype based on the parsed
|
||||
// typeString provided.
|
||||
func parseCredentialType(typeString string) CredentialType {
|
||||
switch typeString {
|
||||
case "service_account":
|
||||
return ServiceAccountKey
|
||||
case "authorized_user":
|
||||
return UserCredentialsKey
|
||||
case "impersonated_service_account":
|
||||
return ImpersonatedServiceAccountKey
|
||||
case "external_account":
|
||||
return ExternalAccountKey
|
||||
case "external_account_authorized_user":
|
||||
return ExternalAccountAuthorizedUserKey
|
||||
case "gdch_service_account":
|
||||
return GDCHServiceAccountKey
|
||||
default:
|
||||
return UnknownCredType
|
||||
}
|
||||
}
|
||||
|
||||
// GetFileNameFromEnv returns the override if provided or detects a filename
|
||||
// from the environment.
|
||||
func GetFileNameFromEnv(override string) string {
|
||||
if override != "" {
|
||||
return override
|
||||
}
|
||||
return os.Getenv(GoogleAppCredsEnvVar)
|
||||
}
|
||||
|
||||
// GetWellKnownFileName tries to locate the filepath for the user credential
|
||||
// file based on the environment.
|
||||
func GetWellKnownFileName() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(os.Getenv("APPDATA"), "gcloud", userCredsFilename)
|
||||
}
|
||||
return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", userCredsFilename)
|
||||
}
|
||||
|
||||
// guessUnixHomeDir default to checking for HOME, but not all unix systems have
|
||||
// this set, do have a fallback.
|
||||
func guessUnixHomeDir() string {
|
||||
if v := os.Getenv("HOME"); v != "" {
|
||||
return v
|
||||
}
|
||||
if u, err := user.Current(); err == nil {
|
||||
return u.HomeDir
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package credsfile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Config3LO is the internals of a client creds file.
|
||||
type Config3LO struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
RedirectURIs []string `json:"redirect_uris"`
|
||||
AuthURI string `json:"auth_uri"`
|
||||
TokenURI string `json:"token_uri"`
|
||||
}
|
||||
|
||||
// ClientCredentialsFile representation.
|
||||
type ClientCredentialsFile struct {
|
||||
Web *Config3LO `json:"web"`
|
||||
Installed *Config3LO `json:"installed"`
|
||||
UniverseDomain string `json:"universe_domain"`
|
||||
}
|
||||
|
||||
// ServiceAccountFile representation.
|
||||
type ServiceAccountFile struct {
|
||||
Type string `json:"type"`
|
||||
ProjectID string `json:"project_id"`
|
||||
PrivateKeyID string `json:"private_key_id"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
ClientEmail string `json:"client_email"`
|
||||
ClientID string `json:"client_id"`
|
||||
AuthURL string `json:"auth_uri"`
|
||||
TokenURL string `json:"token_uri"`
|
||||
UniverseDomain string `json:"universe_domain"`
|
||||
}
|
||||
|
||||
// UserCredentialsFile representation.
|
||||
type UserCredentialsFile struct {
|
||||
Type string `json:"type"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
QuotaProjectID string `json:"quota_project_id"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
UniverseDomain string `json:"universe_domain"`
|
||||
}
|
||||
|
||||
// ExternalAccountFile representation.
|
||||
type ExternalAccountFile struct {
|
||||
Type string `json:"type"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Audience string `json:"audience"`
|
||||
SubjectTokenType string `json:"subject_token_type"`
|
||||
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
|
||||
TokenURL string `json:"token_url"`
|
||||
CredentialSource *CredentialSource `json:"credential_source,omitempty"`
|
||||
TokenInfoURL string `json:"token_info_url"`
|
||||
ServiceAccountImpersonation *ServiceAccountImpersonationInfo `json:"service_account_impersonation,omitempty"`
|
||||
QuotaProjectID string `json:"quota_project_id"`
|
||||
WorkforcePoolUserProject string `json:"workforce_pool_user_project"`
|
||||
UniverseDomain string `json:"universe_domain"`
|
||||
}
|
||||
|
||||
// ExternalAccountAuthorizedUserFile representation.
|
||||
type ExternalAccountAuthorizedUserFile struct {
|
||||
Type string `json:"type"`
|
||||
Audience string `json:"audience"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
TokenURL string `json:"token_url"`
|
||||
TokenInfoURL string `json:"token_info_url"`
|
||||
RevokeURL string `json:"revoke_url"`
|
||||
QuotaProjectID string `json:"quota_project_id"`
|
||||
UniverseDomain string `json:"universe_domain"`
|
||||
}
|
||||
|
||||
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
|
||||
//
|
||||
// One field amongst File, URL, Certificate, and Executable should be filled, depending on the kind of credential in question.
|
||||
// The EnvironmentID should start with AWS if being used for an AWS credential.
|
||||
type CredentialSource struct {
|
||||
File string `json:"file"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Executable *ExecutableConfig `json:"executable,omitempty"`
|
||||
Certificate *CertificateConfig `json:"certificate"`
|
||||
EnvironmentID string `json:"environment_id"` // TODO: Make type for this
|
||||
RegionURL string `json:"region_url"`
|
||||
RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
|
||||
CredVerificationURL string `json:"cred_verification_url"`
|
||||
IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
|
||||
Format *Format `json:"format,omitempty"`
|
||||
}
|
||||
|
||||
// Format describes the format of a [CredentialSource].
|
||||
type Format struct {
|
||||
// Type is either "text" or "json". When not provided "text" type is assumed.
|
||||
Type string `json:"type"`
|
||||
// SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
|
||||
SubjectTokenFieldName string `json:"subject_token_field_name"`
|
||||
}
|
||||
|
||||
// ExecutableConfig represents the command to run for an executable
|
||||
// [CredentialSource].
|
||||
type ExecutableConfig struct {
|
||||
Command string `json:"command"`
|
||||
TimeoutMillis int `json:"timeout_millis"`
|
||||
OutputFile string `json:"output_file"`
|
||||
}
|
||||
|
||||
// CertificateConfig represents the options used to set up X509 based workload
|
||||
// [CredentialSource]
|
||||
type CertificateConfig struct {
|
||||
UseDefaultCertificateConfig bool `json:"use_default_certificate_config"`
|
||||
CertificateConfigLocation string `json:"certificate_config_location"`
|
||||
}
|
||||
|
||||
// ServiceAccountImpersonationInfo has impersonation configuration.
|
||||
type ServiceAccountImpersonationInfo struct {
|
||||
TokenLifetimeSeconds int `json:"token_lifetime_seconds"`
|
||||
}
|
||||
|
||||
// ImpersonatedServiceAccountFile representation.
|
||||
type ImpersonatedServiceAccountFile struct {
|
||||
Type string `json:"type"`
|
||||
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
|
||||
Delegates []string `json:"delegates"`
|
||||
CredSource json.RawMessage `json:"source_credentials"`
|
||||
UniverseDomain string `json:"universe_domain"`
|
||||
}
|
||||
|
||||
// GDCHServiceAccountFile represents the Google Distributed Cloud Hosted (GDCH) service identity file.
|
||||
type GDCHServiceAccountFile struct {
|
||||
Type string `json:"type"`
|
||||
FormatVersion string `json:"format_version"`
|
||||
Project string `json:"project"`
|
||||
Name string `json:"name"`
|
||||
CertPath string `json:"ca_cert_path"`
|
||||
PrivateKeyID string `json:"private_key_id"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
TokenURL string `json:"token_uri"`
|
||||
UniverseDomain string `json:"universe_domain"`
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package credsfile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// ParseServiceAccount parses bytes into a [ServiceAccountFile].
|
||||
func ParseServiceAccount(b []byte) (*ServiceAccountFile, error) {
|
||||
var f *ServiceAccountFile
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ParseClientCredentials parses bytes into a
|
||||
// [credsfile.ClientCredentialsFile].
|
||||
func ParseClientCredentials(b []byte) (*ClientCredentialsFile, error) {
|
||||
var f *ClientCredentialsFile
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ParseUserCredentials parses bytes into a [UserCredentialsFile].
|
||||
func ParseUserCredentials(b []byte) (*UserCredentialsFile, error) {
|
||||
var f *UserCredentialsFile
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ParseExternalAccount parses bytes into a [ExternalAccountFile].
|
||||
func ParseExternalAccount(b []byte) (*ExternalAccountFile, error) {
|
||||
var f *ExternalAccountFile
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ParseExternalAccountAuthorizedUser parses bytes into a
|
||||
// [ExternalAccountAuthorizedUserFile].
|
||||
func ParseExternalAccountAuthorizedUser(b []byte) (*ExternalAccountAuthorizedUserFile, error) {
|
||||
var f *ExternalAccountAuthorizedUserFile
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ParseImpersonatedServiceAccount parses bytes into a
|
||||
// [ImpersonatedServiceAccountFile].
|
||||
func ParseImpersonatedServiceAccount(b []byte) (*ImpersonatedServiceAccountFile, error) {
|
||||
var f *ImpersonatedServiceAccountFile
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ParseGDCHServiceAccount parses bytes into a [GDCHServiceAccountFile].
|
||||
func ParseGDCHServiceAccount(b []byte) (*GDCHServiceAccountFile, error) {
|
||||
var f *GDCHServiceAccountFile
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
type fileTypeChecker struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ParseFileType determines the [CredentialType] based on bytes provided.
|
||||
func ParseFileType(b []byte) (CredentialType, error) {
|
||||
var f fileTypeChecker
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return parseCredentialType(f.Type), nil
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
// TokenTypeBearer is the auth header prefix for bearer tokens.
|
||||
TokenTypeBearer = "Bearer"
|
||||
|
||||
// QuotaProjectEnvVar is the environment variable for setting the quota
|
||||
// project.
|
||||
QuotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
|
||||
// UniverseDomainEnvVar is the environment variable for setting the default
|
||||
// service domain for a given Cloud universe.
|
||||
UniverseDomainEnvVar = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
|
||||
projectEnvVar = "GOOGLE_CLOUD_PROJECT"
|
||||
maxBodySize = 1 << 20
|
||||
|
||||
// DefaultUniverseDomain is the default value for universe domain.
|
||||
// Universe domain is the default service domain for a given Cloud universe.
|
||||
DefaultUniverseDomain = "googleapis.com"
|
||||
)
|
||||
|
||||
type clonableTransport interface {
|
||||
Clone() *http.Transport
|
||||
}
|
||||
|
||||
// DefaultClient returns an [http.Client] with some defaults set. If
|
||||
// the current [http.DefaultTransport] is a [clonableTransport], as
|
||||
// is the case for an [*http.Transport], the clone will be used.
|
||||
// Otherwise the [http.DefaultTransport] is used directly.
|
||||
func DefaultClient() *http.Client {
|
||||
if transport, ok := http.DefaultTransport.(clonableTransport); ok {
|
||||
return &http.Client{
|
||||
Transport: transport.Clone(),
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: http.DefaultTransport,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseKey converts the binary contents of a private key file
|
||||
// to an crypto.Signer. It detects whether the private key is in a
|
||||
// PEM container or not. If so, it extracts the the private key
|
||||
// from PEM container before conversion. It only supports PEM
|
||||
// containers with no passphrase.
|
||||
func ParseKey(key []byte) (crypto.Signer, error) {
|
||||
block, _ := pem.Decode(key)
|
||||
if block != nil {
|
||||
key = block.Bytes
|
||||
}
|
||||
var parsedKey crypto.PrivateKey
|
||||
var err error
|
||||
parsedKey, err = x509.ParsePKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
parsedKey, err = x509.ParsePKCS1PrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8: %w", err)
|
||||
}
|
||||
}
|
||||
parsed, ok := parsedKey.(crypto.Signer)
|
||||
if !ok {
|
||||
return nil, errors.New("private key is not a signer")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
// GetQuotaProject retrieves quota project with precedence being: override,
|
||||
// environment variable, creds json file.
|
||||
func GetQuotaProject(b []byte, override string) string {
|
||||
if override != "" {
|
||||
return override
|
||||
}
|
||||
if env := os.Getenv(QuotaProjectEnvVar); env != "" {
|
||||
return env
|
||||
}
|
||||
if b == nil {
|
||||
return ""
|
||||
}
|
||||
var v struct {
|
||||
QuotaProject string `json:"quota_project_id"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &v); err != nil {
|
||||
return ""
|
||||
}
|
||||
return v.QuotaProject
|
||||
}
|
||||
|
||||
// GetProjectID retrieves project with precedence being: override,
|
||||
// environment variable, creds json file.
|
||||
func GetProjectID(b []byte, override string) string {
|
||||
if override != "" {
|
||||
return override
|
||||
}
|
||||
if env := os.Getenv(projectEnvVar); env != "" {
|
||||
return env
|
||||
}
|
||||
if b == nil {
|
||||
return ""
|
||||
}
|
||||
var v struct {
|
||||
ProjectID string `json:"project_id"` // standard service account key
|
||||
Project string `json:"project"` // gdch key
|
||||
}
|
||||
if err := json.Unmarshal(b, &v); err != nil {
|
||||
return ""
|
||||
}
|
||||
if v.ProjectID != "" {
|
||||
return v.ProjectID
|
||||
}
|
||||
return v.Project
|
||||
}
|
||||
|
||||
// DoRequest executes the provided req with the client. It reads the response
|
||||
// body, closes it, and returns it.
|
||||
func DoRequest(client *http.Client, req *http.Request) (*http.Response, []byte, error) {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ReadAll(io.LimitReader(resp.Body, maxBodySize))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return resp, body, nil
|
||||
}
|
||||
|
||||
// ReadAll consumes the whole reader and safely reads the content of its body
|
||||
// with some overflow protection.
|
||||
func ReadAll(r io.Reader) ([]byte, error) {
|
||||
return io.ReadAll(io.LimitReader(r, maxBodySize))
|
||||
}
|
||||
|
||||
// StaticCredentialsProperty is a helper for creating static credentials
|
||||
// properties.
|
||||
func StaticCredentialsProperty(s string) StaticProperty {
|
||||
return StaticProperty(s)
|
||||
}
|
||||
|
||||
// StaticProperty always returns that value of the underlying string.
|
||||
type StaticProperty string
|
||||
|
||||
// GetProperty loads the properly value provided the given context.
|
||||
func (p StaticProperty) GetProperty(context.Context) (string, error) {
|
||||
return string(p), nil
|
||||
}
|
||||
|
||||
// ComputeUniverseDomainProvider fetches the credentials universe domain from
|
||||
// the google cloud metadata service.
|
||||
type ComputeUniverseDomainProvider struct {
|
||||
MetadataClient *metadata.Client
|
||||
universeDomainOnce sync.Once
|
||||
universeDomain string
|
||||
universeDomainErr error
|
||||
}
|
||||
|
||||
// GetProperty fetches the credentials universe domain from the google cloud
|
||||
// metadata service.
|
||||
func (c *ComputeUniverseDomainProvider) GetProperty(ctx context.Context) (string, error) {
|
||||
c.universeDomainOnce.Do(func() {
|
||||
c.universeDomain, c.universeDomainErr = getMetadataUniverseDomain(ctx, c.MetadataClient)
|
||||
})
|
||||
if c.universeDomainErr != nil {
|
||||
return "", c.universeDomainErr
|
||||
}
|
||||
return c.universeDomain, nil
|
||||
}
|
||||
|
||||
// httpGetMetadataUniverseDomain is a package var for unit test substitution.
|
||||
var httpGetMetadataUniverseDomain = func(ctx context.Context, client *metadata.Client) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
defer cancel()
|
||||
return client.GetWithContext(ctx, "universe/universe-domain")
|
||||
}
|
||||
|
||||
func getMetadataUniverseDomain(ctx context.Context, client *metadata.Client) (string, error) {
|
||||
universeDomain, err := httpGetMetadataUniverseDomain(ctx, client)
|
||||
if err == nil {
|
||||
return universeDomain, nil
|
||||
}
|
||||
if _, ok := err.(metadata.NotDefinedError); ok {
|
||||
// http.StatusNotFound (404)
|
||||
return DefaultUniverseDomain, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// HeaderAlgRSA256 is the RS256 [Header.Algorithm].
|
||||
HeaderAlgRSA256 = "RS256"
|
||||
// HeaderAlgES256 is the ES256 [Header.Algorithm].
|
||||
HeaderAlgES256 = "ES256"
|
||||
// HeaderType is the standard [Header.Type].
|
||||
HeaderType = "JWT"
|
||||
)
|
||||
|
||||
// Header represents a JWT header.
|
||||
type Header struct {
|
||||
Algorithm string `json:"alg"`
|
||||
Type string `json:"typ"`
|
||||
KeyID string `json:"kid"`
|
||||
}
|
||||
|
||||
func (h *Header) encode() (string, error) {
|
||||
b, err := json.Marshal(h)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// Claims represents the claims set of a JWT.
|
||||
type Claims struct {
|
||||
// Iss is the issuer JWT claim.
|
||||
Iss string `json:"iss"`
|
||||
// Scope is the scope JWT claim.
|
||||
Scope string `json:"scope,omitempty"`
|
||||
// Exp is the expiry JWT claim. If unset, default is in one hour from now.
|
||||
Exp int64 `json:"exp"`
|
||||
// Iat is the subject issued at claim. If unset, default is now.
|
||||
Iat int64 `json:"iat"`
|
||||
// Aud is the audience JWT claim. Optional.
|
||||
Aud string `json:"aud"`
|
||||
// Sub is the subject JWT claim. Optional.
|
||||
Sub string `json:"sub,omitempty"`
|
||||
// AdditionalClaims contains any additional non-standard JWT claims. Optional.
|
||||
AdditionalClaims map[string]interface{} `json:"-"`
|
||||
}
|
||||
|
||||
func (c *Claims) encode() (string, error) {
|
||||
// Compensate for skew
|
||||
now := time.Now().Add(-10 * time.Second)
|
||||
if c.Iat == 0 {
|
||||
c.Iat = now.Unix()
|
||||
}
|
||||
if c.Exp == 0 {
|
||||
c.Exp = now.Add(time.Hour).Unix()
|
||||
}
|
||||
if c.Exp < c.Iat {
|
||||
return "", fmt.Errorf("jwt: invalid Exp = %d; must be later than Iat = %d", c.Exp, c.Iat)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(c.AdditionalClaims) == 0 {
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// Marshal private claim set and then append it to b.
|
||||
prv, err := json.Marshal(c.AdditionalClaims)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid map of additional claims %v: %w", c.AdditionalClaims, err)
|
||||
}
|
||||
|
||||
// Concatenate public and private claim JSON objects.
|
||||
if !bytes.HasSuffix(b, []byte{'}'}) {
|
||||
return "", fmt.Errorf("invalid JSON %s", b)
|
||||
}
|
||||
if !bytes.HasPrefix(prv, []byte{'{'}) {
|
||||
return "", fmt.Errorf("invalid JSON %s", prv)
|
||||
}
|
||||
b[len(b)-1] = ',' // Replace closing curly brace with a comma.
|
||||
b = append(b, prv[1:]...) // Append private claims.
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// EncodeJWS encodes the data using the provided key as a JSON web signature.
|
||||
func EncodeJWS(header *Header, c *Claims, signer crypto.Signer) (string, error) {
|
||||
head, err := header.encode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
claims, err := c.encode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ss := fmt.Sprintf("%s.%s", head, claims)
|
||||
h := sha256.New()
|
||||
h.Write([]byte(ss))
|
||||
sig, err := signer.Sign(rand.Reader, h.Sum(nil), crypto.SHA256)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(sig)), nil
|
||||
}
|
||||
|
||||
// DecodeJWS decodes a claim set from a JWS payload.
|
||||
func DecodeJWS(payload string) (*Claims, error) {
|
||||
// decode returned id token to get expiry
|
||||
s := strings.Split(payload, ".")
|
||||
if len(s) < 2 {
|
||||
return nil, errors.New("invalid token received")
|
||||
}
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(s[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &Claims{}
|
||||
if err := json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.NewDecoder(bytes.NewBuffer(decoded)).Decode(&c.AdditionalClaims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
// VerifyJWS tests whether the provided JWT token's signature was produced by
|
||||
// the private key associated with the provided public key.
|
||||
func VerifyJWS(token string, key *rsa.PublicKey) error {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return errors.New("jwt: invalid token received, token must have 3 parts")
|
||||
}
|
||||
|
||||
signedContent := parts[0] + "." + parts[1]
|
||||
signatureString, err := base64.RawURLEncoding.DecodeString(parts[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(signedContent))
|
||||
return rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), signatureString)
|
||||
}
|
||||
+368
@@ -0,0 +1,368 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/transport/cert"
|
||||
"github.com/google/s2a-go"
|
||||
"github.com/google/s2a-go/fallback"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
mTLSModeAlways = "always"
|
||||
mTLSModeNever = "never"
|
||||
mTLSModeAuto = "auto"
|
||||
|
||||
// Experimental: if true, the code will try MTLS with S2A as the default for transport security. Default value is false.
|
||||
googleAPIUseS2AEnv = "EXPERIMENTAL_GOOGLE_API_USE_S2A"
|
||||
googleAPIUseCertSource = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
|
||||
googleAPIUseMTLS = "GOOGLE_API_USE_MTLS_ENDPOINT"
|
||||
googleAPIUseMTLSOld = "GOOGLE_API_USE_MTLS"
|
||||
|
||||
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
|
||||
|
||||
mtlsMDSRoot = "/run/google-mds-mtls/root.crt"
|
||||
mtlsMDSKey = "/run/google-mds-mtls/client.key"
|
||||
)
|
||||
|
||||
// Options is a struct that is duplicated information from the individual
|
||||
// transport packages in order to avoid cyclic deps. It correlates 1:1 with
|
||||
// fields on httptransport.Options and grpctransport.Options.
|
||||
type Options struct {
|
||||
Endpoint string
|
||||
DefaultEndpointTemplate string
|
||||
DefaultMTLSEndpoint string
|
||||
ClientCertProvider cert.Provider
|
||||
Client *http.Client
|
||||
UniverseDomain string
|
||||
EnableDirectPath bool
|
||||
EnableDirectPathXds bool
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
// getUniverseDomain returns the default service domain for a given Cloud
|
||||
// universe.
|
||||
func (o *Options) getUniverseDomain() string {
|
||||
if o.UniverseDomain == "" {
|
||||
return internal.DefaultUniverseDomain
|
||||
}
|
||||
return o.UniverseDomain
|
||||
}
|
||||
|
||||
// isUniverseDomainGDU returns true if the universe domain is the default Google
|
||||
// universe.
|
||||
func (o *Options) isUniverseDomainGDU() bool {
|
||||
return o.getUniverseDomain() == internal.DefaultUniverseDomain
|
||||
}
|
||||
|
||||
// defaultEndpoint returns the DefaultEndpointTemplate merged with the
|
||||
// universe domain if the DefaultEndpointTemplate is set, otherwise returns an
|
||||
// empty string.
|
||||
func (o *Options) defaultEndpoint() string {
|
||||
if o.DefaultEndpointTemplate == "" {
|
||||
return ""
|
||||
}
|
||||
return strings.Replace(o.DefaultEndpointTemplate, universeDomainPlaceholder, o.getUniverseDomain(), 1)
|
||||
}
|
||||
|
||||
// defaultMTLSEndpoint returns the DefaultMTLSEndpointTemplate merged with the
|
||||
// universe domain if the DefaultMTLSEndpointTemplate is set, otherwise returns an
|
||||
// empty string.
|
||||
func (o *Options) defaultMTLSEndpoint() string {
|
||||
if o.DefaultMTLSEndpoint == "" {
|
||||
return ""
|
||||
}
|
||||
return strings.Replace(o.DefaultMTLSEndpoint, universeDomainPlaceholder, o.getUniverseDomain(), 1)
|
||||
}
|
||||
|
||||
// mergedEndpoint merges a user-provided Endpoint of format host[:port] with the
|
||||
// default endpoint.
|
||||
func (o *Options) mergedEndpoint() (string, error) {
|
||||
defaultEndpoint := o.defaultEndpoint()
|
||||
u, err := url.Parse(fixScheme(defaultEndpoint))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.Replace(defaultEndpoint, u.Host, o.Endpoint, 1), nil
|
||||
}
|
||||
|
||||
func fixScheme(baseURL string) string {
|
||||
if !strings.Contains(baseURL, "://") {
|
||||
baseURL = "https://" + baseURL
|
||||
}
|
||||
return baseURL
|
||||
}
|
||||
|
||||
// GetGRPCTransportCredsAndEndpoint returns an instance of
|
||||
// [google.golang.org/grpc/credentials.TransportCredentials], and the
|
||||
// corresponding endpoint to use for GRPC client.
|
||||
func GetGRPCTransportCredsAndEndpoint(opts *Options) (credentials.TransportCredentials, string, error) {
|
||||
config, err := getTransportConfig(opts)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
defaultTransportCreds := credentials.NewTLS(&tls.Config{
|
||||
GetClientCertificate: config.clientCertSource,
|
||||
})
|
||||
|
||||
var s2aAddr string
|
||||
var transportCredsForS2A credentials.TransportCredentials
|
||||
|
||||
if config.mtlsS2AAddress != "" {
|
||||
s2aAddr = config.mtlsS2AAddress
|
||||
transportCredsForS2A, err = loadMTLSMDSTransportCreds(mtlsMDSRoot, mtlsMDSKey)
|
||||
if err != nil {
|
||||
log.Printf("Loading MTLS MDS credentials failed: %v", err)
|
||||
if config.s2aAddress != "" {
|
||||
s2aAddr = config.s2aAddress
|
||||
} else {
|
||||
return defaultTransportCreds, config.endpoint, nil
|
||||
}
|
||||
}
|
||||
} else if config.s2aAddress != "" {
|
||||
s2aAddr = config.s2aAddress
|
||||
} else {
|
||||
return defaultTransportCreds, config.endpoint, nil
|
||||
}
|
||||
|
||||
var fallbackOpts *s2a.FallbackOptions
|
||||
// In case of S2A failure, fall back to the endpoint that would've been used without S2A.
|
||||
if fallbackHandshake, err := fallback.DefaultFallbackClientHandshakeFunc(config.endpoint); err == nil {
|
||||
fallbackOpts = &s2a.FallbackOptions{
|
||||
FallbackClientHandshakeFunc: fallbackHandshake,
|
||||
}
|
||||
}
|
||||
|
||||
s2aTransportCreds, err := s2a.NewClientCreds(&s2a.ClientOptions{
|
||||
S2AAddress: s2aAddr,
|
||||
TransportCreds: transportCredsForS2A,
|
||||
FallbackOpts: fallbackOpts,
|
||||
})
|
||||
if err != nil {
|
||||
// Use default if we cannot initialize S2A client transport credentials.
|
||||
return defaultTransportCreds, config.endpoint, nil
|
||||
}
|
||||
return s2aTransportCreds, config.s2aMTLSEndpoint, nil
|
||||
}
|
||||
|
||||
// GetHTTPTransportConfig returns a client certificate source and a function for
|
||||
// dialing MTLS with S2A.
|
||||
func GetHTTPTransportConfig(opts *Options) (cert.Provider, func(context.Context, string, string) (net.Conn, error), error) {
|
||||
config, err := getTransportConfig(opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var s2aAddr string
|
||||
var transportCredsForS2A credentials.TransportCredentials
|
||||
|
||||
if config.mtlsS2AAddress != "" {
|
||||
s2aAddr = config.mtlsS2AAddress
|
||||
transportCredsForS2A, err = loadMTLSMDSTransportCreds(mtlsMDSRoot, mtlsMDSKey)
|
||||
if err != nil {
|
||||
log.Printf("Loading MTLS MDS credentials failed: %v", err)
|
||||
if config.s2aAddress != "" {
|
||||
s2aAddr = config.s2aAddress
|
||||
} else {
|
||||
return config.clientCertSource, nil, nil
|
||||
}
|
||||
}
|
||||
} else if config.s2aAddress != "" {
|
||||
s2aAddr = config.s2aAddress
|
||||
} else {
|
||||
return config.clientCertSource, nil, nil
|
||||
}
|
||||
|
||||
var fallbackOpts *s2a.FallbackOptions
|
||||
// In case of S2A failure, fall back to the endpoint that would've been used without S2A.
|
||||
if fallbackURL, err := url.Parse(config.endpoint); err == nil {
|
||||
if fallbackDialer, fallbackServerAddr, err := fallback.DefaultFallbackDialerAndAddress(fallbackURL.Hostname()); err == nil {
|
||||
fallbackOpts = &s2a.FallbackOptions{
|
||||
FallbackDialer: &s2a.FallbackDialer{
|
||||
Dialer: fallbackDialer,
|
||||
ServerAddr: fallbackServerAddr,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialTLSContextFunc := s2a.NewS2ADialTLSContextFunc(&s2a.ClientOptions{
|
||||
S2AAddress: s2aAddr,
|
||||
TransportCreds: transportCredsForS2A,
|
||||
FallbackOpts: fallbackOpts,
|
||||
})
|
||||
return nil, dialTLSContextFunc, nil
|
||||
}
|
||||
|
||||
func loadMTLSMDSTransportCreds(mtlsMDSRootFile, mtlsMDSKeyFile string) (credentials.TransportCredentials, error) {
|
||||
rootPEM, err := os.ReadFile(mtlsMDSRootFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
ok := caCertPool.AppendCertsFromPEM(rootPEM)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to load MTLS MDS root certificate")
|
||||
}
|
||||
// The mTLS MDS credentials are formatted as the concatenation of a PEM-encoded certificate chain
|
||||
// followed by a PEM-encoded private key. For this reason, the concatenation is passed in to the
|
||||
// tls.X509KeyPair function as both the certificate chain and private key arguments.
|
||||
cert, err := tls.LoadX509KeyPair(mtlsMDSKeyFile, mtlsMDSKeyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig := tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
return credentials.NewTLS(&tlsConfig), nil
|
||||
}
|
||||
|
||||
func getTransportConfig(opts *Options) (*transportConfig, error) {
|
||||
clientCertSource, err := GetClientCertificateProvider(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoint, err := getEndpoint(opts, clientCertSource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defaultTransportConfig := transportConfig{
|
||||
clientCertSource: clientCertSource,
|
||||
endpoint: endpoint,
|
||||
}
|
||||
|
||||
if !shouldUseS2A(clientCertSource, opts) {
|
||||
return &defaultTransportConfig, nil
|
||||
}
|
||||
|
||||
s2aAddress := GetS2AAddress(opts.Logger)
|
||||
mtlsS2AAddress := GetMTLSS2AAddress(opts.Logger)
|
||||
if s2aAddress == "" && mtlsS2AAddress == "" {
|
||||
return &defaultTransportConfig, nil
|
||||
}
|
||||
return &transportConfig{
|
||||
clientCertSource: clientCertSource,
|
||||
endpoint: endpoint,
|
||||
s2aAddress: s2aAddress,
|
||||
mtlsS2AAddress: mtlsS2AAddress,
|
||||
s2aMTLSEndpoint: opts.defaultMTLSEndpoint(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetClientCertificateProvider returns a default client certificate source, if
|
||||
// not provided by the user.
|
||||
//
|
||||
// A nil default source can be returned if the source does not exist. Any exceptions
|
||||
// encountered while initializing the default source will be reported as client
|
||||
// error (ex. corrupt metadata file).
|
||||
func GetClientCertificateProvider(opts *Options) (cert.Provider, error) {
|
||||
if !isClientCertificateEnabled(opts) {
|
||||
return nil, nil
|
||||
} else if opts.ClientCertProvider != nil {
|
||||
return opts.ClientCertProvider, nil
|
||||
}
|
||||
return cert.DefaultProvider()
|
||||
|
||||
}
|
||||
|
||||
// isClientCertificateEnabled returns true by default for all GDU universe domain, unless explicitly overridden by env var
|
||||
func isClientCertificateEnabled(opts *Options) bool {
|
||||
if value, ok := os.LookupEnv(googleAPIUseCertSource); ok {
|
||||
// error as false is OK
|
||||
b, _ := strconv.ParseBool(value)
|
||||
return b
|
||||
}
|
||||
return opts.isUniverseDomainGDU()
|
||||
}
|
||||
|
||||
type transportConfig struct {
|
||||
// The client certificate source.
|
||||
clientCertSource cert.Provider
|
||||
// The corresponding endpoint to use based on client certificate source.
|
||||
endpoint string
|
||||
// The plaintext S2A address if it can be used, otherwise an empty string.
|
||||
s2aAddress string
|
||||
// The MTLS S2A address if it can be used, otherwise an empty string.
|
||||
mtlsS2AAddress string
|
||||
// The MTLS endpoint to use with S2A.
|
||||
s2aMTLSEndpoint string
|
||||
}
|
||||
|
||||
// getEndpoint returns the endpoint for the service, taking into account the
|
||||
// user-provided endpoint override "settings.Endpoint".
|
||||
//
|
||||
// If no endpoint override is specified, we will either return the default
|
||||
// endpoint or the default mTLS endpoint if a client certificate is available.
|
||||
//
|
||||
// You can override the default endpoint choice (mTLS vs. regular) by setting
|
||||
// the GOOGLE_API_USE_MTLS_ENDPOINT environment variable.
|
||||
//
|
||||
// If the endpoint override is an address (host:port) rather than full base
|
||||
// URL (ex. https://...), then the user-provided address will be merged into
|
||||
// the default endpoint. For example, WithEndpoint("myhost:8000") and
|
||||
// DefaultEndpointTemplate("https://UNIVERSE_DOMAIN/bar/baz") will return
|
||||
// "https://myhost:8080/bar/baz". Note that this does not apply to the mTLS
|
||||
// endpoint.
|
||||
func getEndpoint(opts *Options, clientCertSource cert.Provider) (string, error) {
|
||||
if opts.Endpoint == "" {
|
||||
mtlsMode := getMTLSMode()
|
||||
if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) {
|
||||
return opts.defaultMTLSEndpoint(), nil
|
||||
}
|
||||
return opts.defaultEndpoint(), nil
|
||||
}
|
||||
if strings.Contains(opts.Endpoint, "://") {
|
||||
// User passed in a full URL path, use it verbatim.
|
||||
return opts.Endpoint, nil
|
||||
}
|
||||
if opts.defaultEndpoint() == "" {
|
||||
// If DefaultEndpointTemplate is not configured,
|
||||
// use the user provided endpoint verbatim. This allows a naked
|
||||
// "host[:port]" URL to be used with GRPC Direct Path.
|
||||
return opts.Endpoint, nil
|
||||
}
|
||||
|
||||
// Assume user-provided endpoint is host[:port], merge it with the default endpoint.
|
||||
return opts.mergedEndpoint()
|
||||
}
|
||||
|
||||
func getMTLSMode() string {
|
||||
mode := os.Getenv(googleAPIUseMTLS)
|
||||
if mode == "" {
|
||||
mode = os.Getenv(googleAPIUseMTLSOld) // Deprecated.
|
||||
}
|
||||
if mode == "" {
|
||||
return mTLSModeAuto
|
||||
}
|
||||
return strings.ToLower(mode)
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// defaultCertData holds all the variables pertaining to
|
||||
// the default certificate provider created by [DefaultProvider].
|
||||
//
|
||||
// A singleton model is used to allow the provider to be reused
|
||||
// by the transport layer. As mentioned in [DefaultProvider] (provider nil, nil)
|
||||
// may be returned to indicate a default provider could not be found, which
|
||||
// will skip extra tls config in the transport layer .
|
||||
type defaultCertData struct {
|
||||
once sync.Once
|
||||
provider Provider
|
||||
err error
|
||||
}
|
||||
|
||||
var (
|
||||
defaultCert defaultCertData
|
||||
)
|
||||
|
||||
// Provider is a function that can be passed into crypto/tls.Config.GetClientCertificate.
|
||||
type Provider func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
|
||||
|
||||
// errSourceUnavailable is a sentinel error to indicate certificate source is unavailable.
|
||||
var errSourceUnavailable = errors.New("certificate source is unavailable")
|
||||
|
||||
// DefaultProvider returns a certificate source using the preferred EnterpriseCertificateProxySource.
|
||||
// If EnterpriseCertificateProxySource is not available, fall back to the legacy SecureConnectSource.
|
||||
//
|
||||
// If neither source is available (due to missing configurations), a nil Source and a nil Error are
|
||||
// returned to indicate that a default certificate source is unavailable.
|
||||
func DefaultProvider() (Provider, error) {
|
||||
defaultCert.once.Do(func() {
|
||||
defaultCert.provider, defaultCert.err = NewWorkloadX509CertProvider("")
|
||||
if errors.Is(defaultCert.err, errSourceUnavailable) {
|
||||
defaultCert.provider, defaultCert.err = NewEnterpriseCertificateProxyProvider("")
|
||||
if errors.Is(defaultCert.err, errSourceUnavailable) {
|
||||
defaultCert.provider, defaultCert.err = NewSecureConnectProvider("")
|
||||
if errors.Is(defaultCert.err, errSourceUnavailable) {
|
||||
defaultCert.provider, defaultCert.err = nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return defaultCert.provider, defaultCert.err
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/googleapis/enterprise-certificate-proxy/client"
|
||||
)
|
||||
|
||||
type ecpSource struct {
|
||||
key *client.Key
|
||||
}
|
||||
|
||||
// NewEnterpriseCertificateProxyProvider creates a certificate source
|
||||
// using the Enterprise Certificate Proxy client, which delegates
|
||||
// certifcate related operations to an OS-specific "signer binary"
|
||||
// that communicates with the native keystore (ex. keychain on MacOS).
|
||||
//
|
||||
// The configFilePath points to a config file containing relevant parameters
|
||||
// such as the certificate issuer and the location of the signer binary.
|
||||
// If configFilePath is empty, the client will attempt to load the config from
|
||||
// a well-known gcloud location.
|
||||
func NewEnterpriseCertificateProxyProvider(configFilePath string) (Provider, error) {
|
||||
key, err := client.Cred(configFilePath)
|
||||
if err != nil {
|
||||
// TODO(codyoss): once this is fixed upstream can handle this error a
|
||||
// little better here. But be safe for now and assume unavailable.
|
||||
return nil, errSourceUnavailable
|
||||
}
|
||||
|
||||
return (&ecpSource{
|
||||
key: key,
|
||||
}).getClientCertificate, nil
|
||||
}
|
||||
|
||||
func (s *ecpSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
||||
var cert tls.Certificate
|
||||
cert.PrivateKey = s.key
|
||||
cert.Certificate = s.key.CertificateChain()
|
||||
return &cert, nil
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
metadataPath = ".secureConnect"
|
||||
metadataFile = "context_aware_metadata.json"
|
||||
)
|
||||
|
||||
type secureConnectSource struct {
|
||||
metadata secureConnectMetadata
|
||||
|
||||
// Cache the cert to avoid executing helper command repeatedly.
|
||||
cachedCertMutex sync.Mutex
|
||||
cachedCert *tls.Certificate
|
||||
}
|
||||
|
||||
type secureConnectMetadata struct {
|
||||
Cmd []string `json:"cert_provider_command"`
|
||||
}
|
||||
|
||||
// NewSecureConnectProvider creates a certificate source using
|
||||
// the Secure Connect Helper and its associated metadata file.
|
||||
//
|
||||
// The configFilePath points to the location of the context aware metadata file.
|
||||
// If configFilePath is empty, use the default context aware metadata location.
|
||||
func NewSecureConnectProvider(configFilePath string) (Provider, error) {
|
||||
if configFilePath == "" {
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
// Error locating the default config means Secure Connect is not supported.
|
||||
return nil, errSourceUnavailable
|
||||
}
|
||||
configFilePath = filepath.Join(user.HomeDir, metadataPath, metadataFile)
|
||||
}
|
||||
|
||||
file, err := os.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
// Config file missing means Secure Connect is not supported.
|
||||
// There are non-os.ErrNotExist errors that may be returned.
|
||||
// (e.g. if the home directory is /dev/null, *nix systems will
|
||||
// return ENOTDIR instead of ENOENT)
|
||||
return nil, errSourceUnavailable
|
||||
}
|
||||
|
||||
var metadata secureConnectMetadata
|
||||
if err := json.Unmarshal(file, &metadata); err != nil {
|
||||
return nil, fmt.Errorf("cert: could not parse JSON in %q: %w", configFilePath, err)
|
||||
}
|
||||
if err := validateMetadata(metadata); err != nil {
|
||||
return nil, fmt.Errorf("cert: invalid config in %q: %w", configFilePath, err)
|
||||
}
|
||||
return (&secureConnectSource{
|
||||
metadata: metadata,
|
||||
}).getClientCertificate, nil
|
||||
}
|
||||
|
||||
func validateMetadata(metadata secureConnectMetadata) error {
|
||||
if len(metadata.Cmd) == 0 {
|
||||
return errors.New("empty cert_provider_command")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *secureConnectSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
||||
s.cachedCertMutex.Lock()
|
||||
defer s.cachedCertMutex.Unlock()
|
||||
if s.cachedCert != nil && !isCertificateExpired(s.cachedCert) {
|
||||
return s.cachedCert, nil
|
||||
}
|
||||
// Expand OS environment variables in the cert provider command such as "$HOME".
|
||||
for i := 0; i < len(s.metadata.Cmd); i++ {
|
||||
s.metadata.Cmd[i] = os.ExpandEnv(s.metadata.Cmd[i])
|
||||
}
|
||||
command := s.metadata.Cmd
|
||||
data, err := exec.Command(command[0], command[1:]...).Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair(data, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.cachedCert = &cert
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
// isCertificateExpired returns true if the given cert is expired or invalid.
|
||||
func isCertificateExpired(cert *tls.Certificate) bool {
|
||||
if len(cert.Certificate) == 0 {
|
||||
return true
|
||||
}
|
||||
parsed, err := x509.ParseCertificate(cert.Certificate[0])
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return time.Now().After(parsed.NotAfter)
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/googleapis/enterprise-certificate-proxy/client/util"
|
||||
)
|
||||
|
||||
type certConfigs struct {
|
||||
Workload *workloadSource `json:"workload"`
|
||||
}
|
||||
|
||||
type workloadSource struct {
|
||||
CertPath string `json:"cert_path"`
|
||||
KeyPath string `json:"key_path"`
|
||||
}
|
||||
|
||||
type certificateConfig struct {
|
||||
CertConfigs certConfigs `json:"cert_configs"`
|
||||
}
|
||||
|
||||
// NewWorkloadX509CertProvider creates a certificate source
|
||||
// that reads a certificate and private key file from the local file system.
|
||||
// This is intended to be used for workload identity federation.
|
||||
//
|
||||
// The configFilePath points to a config file containing relevant parameters
|
||||
// such as the certificate and key file paths.
|
||||
// If configFilePath is empty, the client will attempt to load the config from
|
||||
// a well-known gcloud location.
|
||||
func NewWorkloadX509CertProvider(configFilePath string) (Provider, error) {
|
||||
if configFilePath == "" {
|
||||
envFilePath := util.GetConfigFilePathFromEnv()
|
||||
if envFilePath != "" {
|
||||
configFilePath = envFilePath
|
||||
} else {
|
||||
configFilePath = util.GetDefaultConfigFilePath()
|
||||
}
|
||||
}
|
||||
|
||||
certFile, keyFile, err := getCertAndKeyFiles(configFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
source := &workloadSource{
|
||||
CertPath: certFile,
|
||||
KeyPath: keyFile,
|
||||
}
|
||||
return source.getClientCertificate, nil
|
||||
}
|
||||
|
||||
// getClientCertificate attempts to load the certificate and key from the files specified in the
|
||||
// certificate config.
|
||||
func (s *workloadSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
||||
cert, err := tls.LoadX509KeyPair(s.CertPath, s.KeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
// getCertAndKeyFiles attempts to read the provided config file and return the certificate and private
|
||||
// key file paths.
|
||||
func getCertAndKeyFiles(configFilePath string) (string, string, error) {
|
||||
jsonFile, err := os.Open(configFilePath)
|
||||
if err != nil {
|
||||
return "", "", errSourceUnavailable
|
||||
}
|
||||
|
||||
byteValue, err := io.ReadAll(jsonFile)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var config certificateConfig
|
||||
if err := json.Unmarshal(byteValue, &config); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if config.CertConfigs.Workload == nil {
|
||||
return "", "", errSourceUnavailable
|
||||
}
|
||||
|
||||
certFile := config.CertConfigs.Workload.CertPath
|
||||
keyFile := config.CertConfigs.Workload.KeyPath
|
||||
|
||||
if certFile == "" {
|
||||
return "", "", errors.New("certificate configuration is missing the certificate file location")
|
||||
}
|
||||
|
||||
if keyFile == "" {
|
||||
return "", "", errors.New("certificate configuration is missing the key file location")
|
||||
}
|
||||
|
||||
return certFile, keyFile, nil
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"cloud.google.com/go/auth/internal/transport/cert"
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
configEndpointSuffix = "instance/platform-security/auto-mtls-configuration"
|
||||
)
|
||||
|
||||
var (
|
||||
mtlsConfiguration *mtlsConfig
|
||||
|
||||
mtlsOnce sync.Once
|
||||
)
|
||||
|
||||
// GetS2AAddress returns the S2A address to be reached via plaintext connection.
|
||||
// Returns empty string if not set or invalid.
|
||||
func GetS2AAddress(logger *slog.Logger) string {
|
||||
getMetadataMTLSAutoConfig(logger)
|
||||
if !mtlsConfiguration.valid() {
|
||||
return ""
|
||||
}
|
||||
return mtlsConfiguration.S2A.PlaintextAddress
|
||||
}
|
||||
|
||||
// GetMTLSS2AAddress returns the S2A address to be reached via MTLS connection.
|
||||
// Returns empty string if not set or invalid.
|
||||
func GetMTLSS2AAddress(logger *slog.Logger) string {
|
||||
getMetadataMTLSAutoConfig(logger)
|
||||
if !mtlsConfiguration.valid() {
|
||||
return ""
|
||||
}
|
||||
return mtlsConfiguration.S2A.MTLSAddress
|
||||
}
|
||||
|
||||
// mtlsConfig contains the configuration for establishing MTLS connections with Google APIs.
|
||||
type mtlsConfig struct {
|
||||
S2A *s2aAddresses `json:"s2a"`
|
||||
}
|
||||
|
||||
func (c *mtlsConfig) valid() bool {
|
||||
return c != nil && c.S2A != nil
|
||||
}
|
||||
|
||||
// s2aAddresses contains the plaintext and/or MTLS S2A addresses.
|
||||
type s2aAddresses struct {
|
||||
// PlaintextAddress is the plaintext address to reach S2A
|
||||
PlaintextAddress string `json:"plaintext_address"`
|
||||
// MTLSAddress is the MTLS address to reach S2A
|
||||
MTLSAddress string `json:"mtls_address"`
|
||||
}
|
||||
|
||||
func getMetadataMTLSAutoConfig(logger *slog.Logger) {
|
||||
var err error
|
||||
mtlsOnce.Do(func() {
|
||||
mtlsConfiguration, err = queryConfig(logger)
|
||||
if err != nil {
|
||||
log.Printf("Getting MTLS config failed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var httpGetMetadataMTLSConfig = func(logger *slog.Logger) (string, error) {
|
||||
metadataClient := metadata.NewWithOptions(&metadata.Options{
|
||||
Logger: logger,
|
||||
})
|
||||
return metadataClient.GetWithContext(context.Background(), configEndpointSuffix)
|
||||
}
|
||||
|
||||
func queryConfig(logger *slog.Logger) (*mtlsConfig, error) {
|
||||
resp, err := httpGetMetadataMTLSConfig(logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying MTLS config from MDS endpoint failed: %w", err)
|
||||
}
|
||||
var config mtlsConfig
|
||||
err = json.Unmarshal([]byte(resp), &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling MTLS config from MDS endpoint failed: %w", err)
|
||||
}
|
||||
if config.S2A == nil {
|
||||
return nil, fmt.Errorf("returned MTLS config from MDS endpoint is invalid: %v", config)
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func shouldUseS2A(clientCertSource cert.Provider, opts *Options) bool {
|
||||
// If client cert is found, use that over S2A.
|
||||
if clientCertSource != nil {
|
||||
return false
|
||||
}
|
||||
// If EXPERIMENTAL_GOOGLE_API_USE_S2A is not set to true, skip S2A.
|
||||
if !isGoogleS2AEnabled() {
|
||||
return false
|
||||
}
|
||||
// If DefaultMTLSEndpoint is not set or has endpoint override, skip S2A.
|
||||
if opts.DefaultMTLSEndpoint == "" || opts.Endpoint != "" {
|
||||
return false
|
||||
}
|
||||
// If custom HTTP client is provided, skip S2A.
|
||||
if opts.Client != nil {
|
||||
return false
|
||||
}
|
||||
// If directPath is enabled, skip S2A.
|
||||
return !opts.EnableDirectPath && !opts.EnableDirectPathXds
|
||||
}
|
||||
|
||||
func isGoogleS2AEnabled() bool {
|
||||
b, err := strconv.ParseBool(os.Getenv(googleAPIUseS2AEnv))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return b
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package transport provided internal helpers for the two transport packages
|
||||
// (grpctransport and httptransport).
|
||||
package transport
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth/credentials"
|
||||
)
|
||||
|
||||
// CloneDetectOptions clones a user set detect option into some new memory that
|
||||
// we can internally manipulate before sending onto the detect package.
|
||||
func CloneDetectOptions(oldDo *credentials.DetectOptions) *credentials.DetectOptions {
|
||||
if oldDo == nil {
|
||||
// it is valid for users not to set this, but we will need to to default
|
||||
// some options for them in this case so return some initialized memory
|
||||
// to work with.
|
||||
return &credentials.DetectOptions{}
|
||||
}
|
||||
newDo := &credentials.DetectOptions{
|
||||
// Simple types
|
||||
Audience: oldDo.Audience,
|
||||
Subject: oldDo.Subject,
|
||||
EarlyTokenRefresh: oldDo.EarlyTokenRefresh,
|
||||
TokenURL: oldDo.TokenURL,
|
||||
STSAudience: oldDo.STSAudience,
|
||||
CredentialsFile: oldDo.CredentialsFile,
|
||||
UseSelfSignedJWT: oldDo.UseSelfSignedJWT,
|
||||
UniverseDomain: oldDo.UniverseDomain,
|
||||
|
||||
// These fields are are pointer types that we just want to use exactly
|
||||
// as the user set, copy the ref
|
||||
Client: oldDo.Client,
|
||||
Logger: oldDo.Logger,
|
||||
AuthHandlerOptions: oldDo.AuthHandlerOptions,
|
||||
}
|
||||
|
||||
// Smartly size this memory and copy below.
|
||||
if len(oldDo.CredentialsJSON) > 0 {
|
||||
newDo.CredentialsJSON = make([]byte, len(oldDo.CredentialsJSON))
|
||||
copy(newDo.CredentialsJSON, oldDo.CredentialsJSON)
|
||||
}
|
||||
if len(oldDo.Scopes) > 0 {
|
||||
newDo.Scopes = make([]string, len(oldDo.Scopes))
|
||||
copy(newDo.Scopes, oldDo.Scopes)
|
||||
}
|
||||
|
||||
return newDo
|
||||
}
|
||||
|
||||
// ValidateUniverseDomain verifies that the universe domain configured for the
|
||||
// client matches the universe domain configured for the credentials.
|
||||
func ValidateUniverseDomain(clientUniverseDomain, credentialsUniverseDomain string) error {
|
||||
if clientUniverseDomain != credentialsUniverseDomain {
|
||||
return fmt.Errorf(
|
||||
"the configured universe domain (%q) does not match the universe "+
|
||||
"domain found in the credentials (%q). If you haven't configured "+
|
||||
"the universe domain explicitly, \"googleapis.com\" is the default",
|
||||
clientUniverseDomain,
|
||||
credentialsUniverseDomain)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultHTTPClientWithTLS constructs an HTTPClient using the provided tlsConfig, to support mTLS.
|
||||
func DefaultHTTPClientWithTLS(tlsConfig *tls.Config) *http.Client {
|
||||
trans := BaseTransport()
|
||||
trans.TLSClientConfig = tlsConfig
|
||||
return &http.Client{Transport: trans}
|
||||
}
|
||||
|
||||
// BaseTransport returns a default [http.Transport] which can be used if
|
||||
// [http.DefaultTransport] has been overwritten.
|
||||
func BaseTransport() *http.Transport {
|
||||
return &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
# Changelog
|
||||
|
||||
## [0.2.6](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.5...auth/oauth2adapt/v0.2.6) (2024-11-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/oauth2adapt:** Copy map in tokenSourceAdapter.Token ([#11164](https://github.com/googleapis/google-cloud-go/issues/11164)) ([8cb0cbc](https://github.com/googleapis/google-cloud-go/commit/8cb0cbccdc32886dfb3af49fee04012937d114d2)), refs [#11161](https://github.com/googleapis/google-cloud-go/issues/11161)
|
||||
|
||||
## [0.2.5](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.4...auth/oauth2adapt/v0.2.5) (2024-10-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/oauth2adapt:** Convert token metadata where possible ([#11062](https://github.com/googleapis/google-cloud-go/issues/11062)) ([34bf1c1](https://github.com/googleapis/google-cloud-go/commit/34bf1c164465d66745c0cfdf7cd10a8e2da92e52))
|
||||
|
||||
## [0.2.4](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.3...auth/oauth2adapt/v0.2.4) (2024-08-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/oauth2adapt:** Update dependencies ([257c40b](https://github.com/googleapis/google-cloud-go/commit/257c40bd6d7e59730017cf32bda8823d7a232758))
|
||||
|
||||
## [0.2.3](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.2...auth/oauth2adapt/v0.2.3) (2024-07-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/oauth2adapt:** Bump google.golang.org/api@v0.187.0 ([8fa9e39](https://github.com/googleapis/google-cloud-go/commit/8fa9e398e512fd8533fd49060371e61b5725a85b))
|
||||
|
||||
## [0.2.2](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.1...auth/oauth2adapt/v0.2.2) (2024-04-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/oauth2adapt:** Bump x/net to v0.24.0 ([ba31ed5](https://github.com/googleapis/google-cloud-go/commit/ba31ed5fda2c9664f2e1cf972469295e63deb5b4))
|
||||
|
||||
## [0.2.1](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.2.0...auth/oauth2adapt/v0.2.1) (2024-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/oauth2adapt:** Adapt Token Types to be translated ([#9801](https://github.com/googleapis/google-cloud-go/issues/9801)) ([70f4115](https://github.com/googleapis/google-cloud-go/commit/70f411555ebbf2b71e6d425cc8d2030644c6b438)), refs [#9800](https://github.com/googleapis/google-cloud-go/issues/9800)
|
||||
|
||||
## [0.2.0](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.1.0...auth/oauth2adapt/v0.2.0) (2024-04-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth/oauth2adapt:** Add helpers for working with credentials types ([#9694](https://github.com/googleapis/google-cloud-go/issues/9694)) ([cf33b55](https://github.com/googleapis/google-cloud-go/commit/cf33b5514423a2ac5c2a323a1cd99aac34fd4233))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/oauth2adapt:** Update protobuf dep to v1.33.0 ([30b038d](https://github.com/googleapis/google-cloud-go/commit/30b038d8cac0b8cd5dd4761c87f3f298760dd33a))
|
||||
|
||||
## 0.1.0 (2023-10-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth/oauth2adapt:** Adds a new module to translate types ([#8595](https://github.com/googleapis/google-cloud-go/issues/8595)) ([6933c5a](https://github.com/googleapis/google-cloud-go/commit/6933c5a0c1fc8e58cbfff8bbca439d671b94672f))
|
||||
* **auth/oauth2adapt:** Fixup deps for release ([#8747](https://github.com/googleapis/google-cloud-go/issues/8747)) ([749d243](https://github.com/googleapis/google-cloud-go/commit/749d243862b025a6487a4d2d339219889b4cfe70))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth/oauth2adapt:** Update golang.org/x/net to v0.17.0 ([174da47](https://github.com/googleapis/google-cloud-go/commit/174da47254fefb12921bbfc65b7829a453af6f5d))
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package oauth2adapt helps converts types used in [cloud.google.com/go/auth]
|
||||
// and [golang.org/x/oauth2].
|
||||
package oauth2adapt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
const (
|
||||
oauth2TokenSourceKey = "oauth2.google.tokenSource"
|
||||
oauth2ServiceAccountKey = "oauth2.google.serviceAccount"
|
||||
authTokenSourceKey = "auth.google.tokenSource"
|
||||
authServiceAccountKey = "auth.google.serviceAccount"
|
||||
)
|
||||
|
||||
// TokenProviderFromTokenSource converts any [golang.org/x/oauth2.TokenSource]
|
||||
// into a [cloud.google.com/go/auth.TokenProvider].
|
||||
func TokenProviderFromTokenSource(ts oauth2.TokenSource) auth.TokenProvider {
|
||||
return &tokenProviderAdapter{ts: ts}
|
||||
}
|
||||
|
||||
type tokenProviderAdapter struct {
|
||||
ts oauth2.TokenSource
|
||||
}
|
||||
|
||||
// Token fulfills the [cloud.google.com/go/auth.TokenProvider] interface. It
|
||||
// is a light wrapper around the underlying TokenSource.
|
||||
func (tp *tokenProviderAdapter) Token(context.Context) (*auth.Token, error) {
|
||||
tok, err := tp.ts.Token()
|
||||
if err != nil {
|
||||
var err2 *oauth2.RetrieveError
|
||||
if ok := errors.As(err, &err2); ok {
|
||||
return nil, AuthErrorFromRetrieveError(err2)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// Preserve compute token metadata, for both types of tokens.
|
||||
metadata := map[string]interface{}{}
|
||||
if val, ok := tok.Extra(oauth2TokenSourceKey).(string); ok {
|
||||
metadata[authTokenSourceKey] = val
|
||||
metadata[oauth2TokenSourceKey] = val
|
||||
}
|
||||
if val, ok := tok.Extra(oauth2ServiceAccountKey).(string); ok {
|
||||
metadata[authServiceAccountKey] = val
|
||||
metadata[oauth2ServiceAccountKey] = val
|
||||
}
|
||||
return &auth.Token{
|
||||
Value: tok.AccessToken,
|
||||
Type: tok.Type(),
|
||||
Expiry: tok.Expiry,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TokenSourceFromTokenProvider converts any
|
||||
// [cloud.google.com/go/auth.TokenProvider] into a
|
||||
// [golang.org/x/oauth2.TokenSource].
|
||||
func TokenSourceFromTokenProvider(tp auth.TokenProvider) oauth2.TokenSource {
|
||||
return &tokenSourceAdapter{tp: tp}
|
||||
}
|
||||
|
||||
type tokenSourceAdapter struct {
|
||||
tp auth.TokenProvider
|
||||
}
|
||||
|
||||
// Token fulfills the [golang.org/x/oauth2.TokenSource] interface. It
|
||||
// is a light wrapper around the underlying TokenProvider.
|
||||
func (ts *tokenSourceAdapter) Token() (*oauth2.Token, error) {
|
||||
tok, err := ts.tp.Token(context.Background())
|
||||
if err != nil {
|
||||
var err2 *auth.Error
|
||||
if ok := errors.As(err, &err2); ok {
|
||||
return nil, AddRetrieveErrorToAuthError(err2)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
tok2 := &oauth2.Token{
|
||||
AccessToken: tok.Value,
|
||||
TokenType: tok.Type,
|
||||
Expiry: tok.Expiry,
|
||||
}
|
||||
// Preserve token metadata.
|
||||
m := tok.Metadata
|
||||
if m != nil {
|
||||
// Copy map to avoid concurrent map writes error (#11161).
|
||||
metadata := make(map[string]interface{}, len(m)+2)
|
||||
for k, v := range m {
|
||||
metadata[k] = v
|
||||
}
|
||||
// Append compute token metadata in converted form.
|
||||
if val, ok := metadata[authTokenSourceKey].(string); ok && val != "" {
|
||||
metadata[oauth2TokenSourceKey] = val
|
||||
}
|
||||
if val, ok := metadata[authServiceAccountKey].(string); ok && val != "" {
|
||||
metadata[oauth2ServiceAccountKey] = val
|
||||
}
|
||||
tok2 = tok2.WithExtra(metadata)
|
||||
}
|
||||
return tok2, nil
|
||||
}
|
||||
|
||||
// AuthCredentialsFromOauth2Credentials converts a [golang.org/x/oauth2/google.Credentials]
|
||||
// to a [cloud.google.com/go/auth.Credentials].
|
||||
func AuthCredentialsFromOauth2Credentials(creds *google.Credentials) *auth.Credentials {
|
||||
if creds == nil {
|
||||
return nil
|
||||
}
|
||||
return auth.NewCredentials(&auth.CredentialsOptions{
|
||||
TokenProvider: TokenProviderFromTokenSource(creds.TokenSource),
|
||||
JSON: creds.JSON,
|
||||
ProjectIDProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
|
||||
return creds.ProjectID, nil
|
||||
}),
|
||||
UniverseDomainProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
|
||||
return creds.GetUniverseDomain()
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
// Oauth2CredentialsFromAuthCredentials converts a [cloud.google.com/go/auth.Credentials]
|
||||
// to a [golang.org/x/oauth2/google.Credentials].
|
||||
func Oauth2CredentialsFromAuthCredentials(creds *auth.Credentials) *google.Credentials {
|
||||
if creds == nil {
|
||||
return nil
|
||||
}
|
||||
// Throw away errors as old credentials are not request aware. Also, no
|
||||
// network requests are currently happening for this use case.
|
||||
projectID, _ := creds.ProjectID(context.Background())
|
||||
|
||||
return &google.Credentials{
|
||||
TokenSource: TokenSourceFromTokenProvider(creds.TokenProvider),
|
||||
ProjectID: projectID,
|
||||
JSON: creds.JSON(),
|
||||
UniverseDomainProvider: func() (string, error) {
|
||||
return creds.UniverseDomain(context.Background())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type oauth2Error struct {
|
||||
ErrorCode string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
ErrorURI string `json:"error_uri"`
|
||||
}
|
||||
|
||||
// AddRetrieveErrorToAuthError returns the same error provided and adds a
|
||||
// [golang.org/x/oauth2.RetrieveError] to the error chain by setting the `Err` field on the
|
||||
// [cloud.google.com/go/auth.Error].
|
||||
func AddRetrieveErrorToAuthError(err *auth.Error) *auth.Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
e := &oauth2.RetrieveError{
|
||||
Response: err.Response,
|
||||
Body: err.Body,
|
||||
}
|
||||
err.Err = e
|
||||
if len(err.Body) > 0 {
|
||||
var oErr oauth2Error
|
||||
// ignore the error as it only fills in extra details
|
||||
json.Unmarshal(err.Body, &oErr)
|
||||
e.ErrorCode = oErr.ErrorCode
|
||||
e.ErrorDescription = oErr.ErrorDescription
|
||||
e.ErrorURI = oErr.ErrorURI
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// AuthErrorFromRetrieveError returns an [cloud.google.com/go/auth.Error] that
|
||||
// wraps the provided [golang.org/x/oauth2.RetrieveError].
|
||||
func AuthErrorFromRetrieveError(err *oauth2.RetrieveError) *auth.Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &auth.Error{
|
||||
Response: err.Response,
|
||||
Body: err.Body,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
)
|
||||
|
||||
// AuthorizationHandler is a 3-legged-OAuth helper that prompts the user for
|
||||
// OAuth consent at the specified auth code URL and returns an auth code and
|
||||
// state upon approval.
|
||||
type AuthorizationHandler func(authCodeURL string) (code string, state string, err error)
|
||||
|
||||
// Options3LO are the options for doing a 3-legged OAuth2 flow.
|
||||
type Options3LO struct {
|
||||
// ClientID is the application's ID.
|
||||
ClientID string
|
||||
// ClientSecret is the application's secret. Not required if AuthHandlerOpts
|
||||
// is set.
|
||||
ClientSecret string
|
||||
// AuthURL is the URL for authenticating.
|
||||
AuthURL string
|
||||
// TokenURL is the URL for retrieving a token.
|
||||
TokenURL string
|
||||
// AuthStyle is used to describe how to client info in the token request.
|
||||
AuthStyle Style
|
||||
// RefreshToken is the token used to refresh the credential. Not required
|
||||
// if AuthHandlerOpts is set.
|
||||
RefreshToken string
|
||||
// RedirectURL is the URL to redirect users to. Optional.
|
||||
RedirectURL string
|
||||
// Scopes specifies requested permissions for the Token. Optional.
|
||||
Scopes []string
|
||||
|
||||
// URLParams are the set of values to apply to the token exchange. Optional.
|
||||
URLParams url.Values
|
||||
// Client is the client to be used to make the underlying token requests.
|
||||
// Optional.
|
||||
Client *http.Client
|
||||
// EarlyTokenExpiry is the time before the token expires that it should be
|
||||
// refreshed. If not set the default value is 3 minutes and 45 seconds.
|
||||
// Optional.
|
||||
EarlyTokenExpiry time.Duration
|
||||
|
||||
// AuthHandlerOpts provides a set of options for doing a
|
||||
// 3-legged OAuth2 flow with a custom [AuthorizationHandler]. Optional.
|
||||
AuthHandlerOpts *AuthorizationHandlerOptions
|
||||
// Logger is used for debug logging. If provided, logging will be enabled
|
||||
// at the loggers configured level. By default logging is disabled unless
|
||||
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
|
||||
// logger will be used. Optional.
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
func (o *Options3LO) validate() error {
|
||||
if o == nil {
|
||||
return errors.New("auth: options must be provided")
|
||||
}
|
||||
if o.ClientID == "" {
|
||||
return errors.New("auth: client ID must be provided")
|
||||
}
|
||||
if o.AuthHandlerOpts == nil && o.ClientSecret == "" {
|
||||
return errors.New("auth: client secret must be provided")
|
||||
}
|
||||
if o.AuthURL == "" {
|
||||
return errors.New("auth: auth URL must be provided")
|
||||
}
|
||||
if o.TokenURL == "" {
|
||||
return errors.New("auth: token URL must be provided")
|
||||
}
|
||||
if o.AuthStyle == StyleUnknown {
|
||||
return errors.New("auth: auth style must be provided")
|
||||
}
|
||||
if o.AuthHandlerOpts == nil && o.RefreshToken == "" {
|
||||
return errors.New("auth: refresh token must be provided")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Options3LO) logger() *slog.Logger {
|
||||
return internallog.New(o.Logger)
|
||||
}
|
||||
|
||||
// PKCEOptions holds parameters to support PKCE.
|
||||
type PKCEOptions struct {
|
||||
// Challenge is the un-padded, base64-url-encoded string of the encrypted code verifier.
|
||||
Challenge string // The un-padded, base64-url-encoded string of the encrypted code verifier.
|
||||
// ChallengeMethod is the encryption method (ex. S256).
|
||||
ChallengeMethod string
|
||||
// Verifier is the original, non-encrypted secret.
|
||||
Verifier string // The original, non-encrypted secret.
|
||||
}
|
||||
|
||||
type tokenJSON struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
// error fields
|
||||
ErrorCode string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
ErrorURI string `json:"error_uri"`
|
||||
}
|
||||
|
||||
func (e *tokenJSON) expiry() (t time.Time) {
|
||||
if v := e.ExpiresIn; v != 0 {
|
||||
return time.Now().Add(time.Duration(v) * time.Second)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *Options3LO) client() *http.Client {
|
||||
if o.Client != nil {
|
||||
return o.Client
|
||||
}
|
||||
return internal.DefaultClient()
|
||||
}
|
||||
|
||||
// authCodeURL returns a URL that points to a OAuth2 consent page.
|
||||
func (o *Options3LO) authCodeURL(state string, values url.Values) string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(o.AuthURL)
|
||||
v := url.Values{
|
||||
"response_type": {"code"},
|
||||
"client_id": {o.ClientID},
|
||||
}
|
||||
if o.RedirectURL != "" {
|
||||
v.Set("redirect_uri", o.RedirectURL)
|
||||
}
|
||||
if len(o.Scopes) > 0 {
|
||||
v.Set("scope", strings.Join(o.Scopes, " "))
|
||||
}
|
||||
if state != "" {
|
||||
v.Set("state", state)
|
||||
}
|
||||
if o.AuthHandlerOpts != nil {
|
||||
if o.AuthHandlerOpts.PKCEOpts != nil &&
|
||||
o.AuthHandlerOpts.PKCEOpts.Challenge != "" {
|
||||
v.Set(codeChallengeKey, o.AuthHandlerOpts.PKCEOpts.Challenge)
|
||||
}
|
||||
if o.AuthHandlerOpts.PKCEOpts != nil &&
|
||||
o.AuthHandlerOpts.PKCEOpts.ChallengeMethod != "" {
|
||||
v.Set(codeChallengeMethodKey, o.AuthHandlerOpts.PKCEOpts.ChallengeMethod)
|
||||
}
|
||||
}
|
||||
for k := range values {
|
||||
v.Set(k, v.Get(k))
|
||||
}
|
||||
if strings.Contains(o.AuthURL, "?") {
|
||||
buf.WriteByte('&')
|
||||
} else {
|
||||
buf.WriteByte('?')
|
||||
}
|
||||
buf.WriteString(v.Encode())
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// New3LOTokenProvider returns a [TokenProvider] based on the 3-legged OAuth2
|
||||
// configuration. The TokenProvider is caches and auto-refreshes tokens by
|
||||
// default.
|
||||
func New3LOTokenProvider(opts *Options3LO) (TokenProvider, error) {
|
||||
if err := opts.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts.AuthHandlerOpts != nil {
|
||||
return new3LOTokenProviderWithAuthHandler(opts), nil
|
||||
}
|
||||
return NewCachedTokenProvider(&tokenProvider3LO{opts: opts, refreshToken: opts.RefreshToken, client: opts.client()}, &CachedTokenProviderOptions{
|
||||
ExpireEarly: opts.EarlyTokenExpiry,
|
||||
}), nil
|
||||
}
|
||||
|
||||
// AuthorizationHandlerOptions provides a set of options to specify for doing a
|
||||
// 3-legged OAuth2 flow with a custom [AuthorizationHandler].
|
||||
type AuthorizationHandlerOptions struct {
|
||||
// AuthorizationHandler specifies the handler used to for the authorization
|
||||
// part of the flow.
|
||||
Handler AuthorizationHandler
|
||||
// State is used verify that the "state" is identical in the request and
|
||||
// response before exchanging the auth code for OAuth2 token.
|
||||
State string
|
||||
// PKCEOpts allows setting configurations for PKCE. Optional.
|
||||
PKCEOpts *PKCEOptions
|
||||
}
|
||||
|
||||
func new3LOTokenProviderWithAuthHandler(opts *Options3LO) TokenProvider {
|
||||
return NewCachedTokenProvider(&tokenProviderWithHandler{opts: opts, state: opts.AuthHandlerOpts.State}, &CachedTokenProviderOptions{
|
||||
ExpireEarly: opts.EarlyTokenExpiry,
|
||||
})
|
||||
}
|
||||
|
||||
// exchange handles the final exchange portion of the 3lo flow. Returns a Token,
|
||||
// refreshToken, and error.
|
||||
func (o *Options3LO) exchange(ctx context.Context, code string) (*Token, string, error) {
|
||||
// Build request
|
||||
v := url.Values{
|
||||
"grant_type": {"authorization_code"},
|
||||
"code": {code},
|
||||
}
|
||||
if o.RedirectURL != "" {
|
||||
v.Set("redirect_uri", o.RedirectURL)
|
||||
}
|
||||
if o.AuthHandlerOpts != nil &&
|
||||
o.AuthHandlerOpts.PKCEOpts != nil &&
|
||||
o.AuthHandlerOpts.PKCEOpts.Verifier != "" {
|
||||
v.Set(codeVerifierKey, o.AuthHandlerOpts.PKCEOpts.Verifier)
|
||||
}
|
||||
for k := range o.URLParams {
|
||||
v.Set(k, o.URLParams.Get(k))
|
||||
}
|
||||
return fetchToken(ctx, o, v)
|
||||
}
|
||||
|
||||
// This struct is not safe for concurrent access alone, but the way it is used
|
||||
// in this package by wrapping it with a cachedTokenProvider makes it so.
|
||||
type tokenProvider3LO struct {
|
||||
opts *Options3LO
|
||||
client *http.Client
|
||||
refreshToken string
|
||||
}
|
||||
|
||||
func (tp *tokenProvider3LO) Token(ctx context.Context) (*Token, error) {
|
||||
if tp.refreshToken == "" {
|
||||
return nil, errors.New("auth: token expired and refresh token is not set")
|
||||
}
|
||||
v := url.Values{
|
||||
"grant_type": {"refresh_token"},
|
||||
"refresh_token": {tp.refreshToken},
|
||||
}
|
||||
for k := range tp.opts.URLParams {
|
||||
v.Set(k, tp.opts.URLParams.Get(k))
|
||||
}
|
||||
|
||||
tk, rt, err := fetchToken(ctx, tp.opts, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tp.refreshToken != rt && rt != "" {
|
||||
tp.refreshToken = rt
|
||||
}
|
||||
return tk, err
|
||||
}
|
||||
|
||||
type tokenProviderWithHandler struct {
|
||||
opts *Options3LO
|
||||
state string
|
||||
}
|
||||
|
||||
func (tp tokenProviderWithHandler) Token(ctx context.Context) (*Token, error) {
|
||||
url := tp.opts.authCodeURL(tp.state, nil)
|
||||
code, state, err := tp.opts.AuthHandlerOpts.Handler(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if state != tp.state {
|
||||
return nil, errors.New("auth: state mismatch in 3-legged-OAuth flow")
|
||||
}
|
||||
tok, _, err := tp.opts.exchange(ctx, code)
|
||||
return tok, err
|
||||
}
|
||||
|
||||
// fetchToken returns a Token, refresh token, and/or an error.
|
||||
func fetchToken(ctx context.Context, o *Options3LO, v url.Values) (*Token, string, error) {
|
||||
var refreshToken string
|
||||
if o.AuthStyle == StyleInParams {
|
||||
if o.ClientID != "" {
|
||||
v.Set("client_id", o.ClientID)
|
||||
}
|
||||
if o.ClientSecret != "" {
|
||||
v.Set("client_secret", o.ClientSecret)
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", o.TokenURL, strings.NewReader(v.Encode()))
|
||||
if err != nil {
|
||||
return nil, refreshToken, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if o.AuthStyle == StyleInHeader {
|
||||
req.SetBasicAuth(url.QueryEscape(o.ClientID), url.QueryEscape(o.ClientSecret))
|
||||
}
|
||||
logger := o.logger()
|
||||
|
||||
logger.DebugContext(ctx, "3LO token request", "request", internallog.HTTPRequest(req, []byte(v.Encode())))
|
||||
// Make request
|
||||
resp, body, err := internal.DoRequest(o.client(), req)
|
||||
if err != nil {
|
||||
return nil, refreshToken, err
|
||||
}
|
||||
logger.DebugContext(ctx, "3LO token response", "response", internallog.HTTPResponse(resp, body))
|
||||
failureStatus := resp.StatusCode < 200 || resp.StatusCode > 299
|
||||
tokError := &Error{
|
||||
Response: resp,
|
||||
Body: body,
|
||||
}
|
||||
|
||||
var token *Token
|
||||
// errors ignored because of default switch on content
|
||||
content, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
switch content {
|
||||
case "application/x-www-form-urlencoded", "text/plain":
|
||||
// some endpoints return a query string
|
||||
vals, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
if failureStatus {
|
||||
return nil, refreshToken, tokError
|
||||
}
|
||||
return nil, refreshToken, fmt.Errorf("auth: cannot parse response: %w", err)
|
||||
}
|
||||
tokError.code = vals.Get("error")
|
||||
tokError.description = vals.Get("error_description")
|
||||
tokError.uri = vals.Get("error_uri")
|
||||
token = &Token{
|
||||
Value: vals.Get("access_token"),
|
||||
Type: vals.Get("token_type"),
|
||||
Metadata: make(map[string]interface{}, len(vals)),
|
||||
}
|
||||
for k, v := range vals {
|
||||
token.Metadata[k] = v
|
||||
}
|
||||
refreshToken = vals.Get("refresh_token")
|
||||
e := vals.Get("expires_in")
|
||||
expires, _ := strconv.Atoi(e)
|
||||
if expires != 0 {
|
||||
token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
|
||||
}
|
||||
default:
|
||||
var tj tokenJSON
|
||||
if err = json.Unmarshal(body, &tj); err != nil {
|
||||
if failureStatus {
|
||||
return nil, refreshToken, tokError
|
||||
}
|
||||
return nil, refreshToken, fmt.Errorf("auth: cannot parse json: %w", err)
|
||||
}
|
||||
tokError.code = tj.ErrorCode
|
||||
tokError.description = tj.ErrorDescription
|
||||
tokError.uri = tj.ErrorURI
|
||||
token = &Token{
|
||||
Value: tj.AccessToken,
|
||||
Type: tj.TokenType,
|
||||
Expiry: tj.expiry(),
|
||||
Metadata: make(map[string]interface{}),
|
||||
}
|
||||
json.Unmarshal(body, &token.Metadata) // optional field, skip err check
|
||||
refreshToken = tj.RefreshToken
|
||||
}
|
||||
// according to spec, servers should respond status 400 in error case
|
||||
// https://www.rfc-editor.org/rfc/rfc6749#section-5.2
|
||||
// but some unorthodox servers respond 200 in error case
|
||||
if failureStatus || tokError.code != "" {
|
||||
return nil, refreshToken, tokError
|
||||
}
|
||||
if token.Value == "" {
|
||||
return nil, refreshToken, errors.New("auth: server response missing access_token")
|
||||
}
|
||||
return token, refreshToken, nil
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
# Changes
|
||||
|
||||
## [0.6.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.5.2...compute/metadata/v0.6.0) (2024-12-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compute/metadata:** Add debug logging ([#11078](https://github.com/googleapis/google-cloud-go/issues/11078)) ([a816814](https://github.com/googleapis/google-cloud-go/commit/a81681463906e4473570a2f426eb0dc2de64e53f))
|
||||
|
||||
## [0.5.2](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.5.1...compute/metadata/v0.5.2) (2024-09-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compute/metadata:** Close Response Body for failed request ([#10891](https://github.com/googleapis/google-cloud-go/issues/10891)) ([e91d45e](https://github.com/googleapis/google-cloud-go/commit/e91d45e4757a9e354114509ba9800085d9e0ff1f))
|
||||
|
||||
## [0.5.1](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.5.0...compute/metadata/v0.5.1) (2024-09-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compute/metadata:** Check error chain for retryable error ([#10840](https://github.com/googleapis/google-cloud-go/issues/10840)) ([2bdedef](https://github.com/googleapis/google-cloud-go/commit/2bdedeff621b223d63cebc4355fcf83bc68412cd))
|
||||
|
||||
## [0.5.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.4.0...compute/metadata/v0.5.0) (2024-07-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compute/metadata:** Add sys check for windows OnGCE ([#10521](https://github.com/googleapis/google-cloud-go/issues/10521)) ([3b9a830](https://github.com/googleapis/google-cloud-go/commit/3b9a83063960d2a2ac20beb47cc15818a68bd302))
|
||||
|
||||
## [0.4.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.3.0...compute/metadata/v0.4.0) (2024-07-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compute/metadata:** Add context for all functions/methods ([#10370](https://github.com/googleapis/google-cloud-go/issues/10370)) ([66b8efe](https://github.com/googleapis/google-cloud-go/commit/66b8efe7ad877e052b2987bb4475477e38c67bb3))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* **compute/metadata:** Update OnGCE description ([#10408](https://github.com/googleapis/google-cloud-go/issues/10408)) ([6a46dca](https://github.com/googleapis/google-cloud-go/commit/6a46dca4eae4f88ec6f88822e01e5bf8aeca787f))
|
||||
|
||||
## [0.3.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.2.3...compute/metadata/v0.3.0) (2024-04-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compute/metadata:** Add context aware functions ([#9733](https://github.com/googleapis/google-cloud-go/issues/9733)) ([e4eb5b4](https://github.com/googleapis/google-cloud-go/commit/e4eb5b46ee2aec9d2fc18300bfd66015e25a0510))
|
||||
|
||||
## [0.2.3](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.2.2...compute/metadata/v0.2.3) (2022-12-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compute/metadata:** Switch DNS lookup to an absolute lookup ([119b410](https://github.com/googleapis/google-cloud-go/commit/119b41060c7895e45e48aee5621ad35607c4d021)), refs [#7165](https://github.com/googleapis/google-cloud-go/issues/7165)
|
||||
|
||||
## [0.2.2](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.2.1...compute/metadata/v0.2.2) (2022-12-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compute/metadata:** Set IdleConnTimeout for http.Client ([#7084](https://github.com/googleapis/google-cloud-go/issues/7084)) ([766516a](https://github.com/googleapis/google-cloud-go/commit/766516aaf3816bfb3159efeea65aa3d1d205a3e2)), refs [#5430](https://github.com/googleapis/google-cloud-go/issues/5430)
|
||||
|
||||
## [0.1.0] (2022-10-26)
|
||||
|
||||
Initial release of metadata being it's own module.
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Compute API
|
||||
|
||||
[](https://pkg.go.dev/cloud.google.com/go/compute/metadata)
|
||||
|
||||
This is a utility library for communicating with Google Cloud metadata service
|
||||
on Google Cloud.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get cloud.google.com/go/compute/metadata
|
||||
```
|
||||
|
||||
## Go Version Support
|
||||
|
||||
See the [Go Versions Supported](https://github.com/googleapis/google-cloud-go#go-versions-supported)
|
||||
section in the root directory's README.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome. Please, see the [CONTRIBUTING](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md)
|
||||
document for details.
|
||||
|
||||
Please note that this project is released with a Contributor Code of Conduct.
|
||||
By participating in this project you agree to abide by its terms. See
|
||||
[Contributor Code of Conduct](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md#contributor-code-of-conduct)
|
||||
for more information.
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Code below this point is copied from github.com/googleapis/gax-go/v2/internallog
|
||||
// to avoid the dependency. The compute/metadata module is used by too many
|
||||
// non-client library modules that can't justify the dependency.
|
||||
|
||||
// The handler returned if logging is not enabled.
|
||||
type noOpHandler struct{}
|
||||
|
||||
func (h noOpHandler) Enabled(_ context.Context, _ slog.Level) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (h noOpHandler) Handle(_ context.Context, _ slog.Record) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h noOpHandler) WithAttrs(_ []slog.Attr) slog.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
func (h noOpHandler) WithGroup(_ string) slog.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
// httpRequest returns a lazily evaluated [slog.LogValuer] for a
|
||||
// [http.Request] and the associated body.
|
||||
func httpRequest(req *http.Request, body []byte) slog.LogValuer {
|
||||
return &request{
|
||||
req: req,
|
||||
payload: body,
|
||||
}
|
||||
}
|
||||
|
||||
type request struct {
|
||||
req *http.Request
|
||||
payload []byte
|
||||
}
|
||||
|
||||
func (r *request) LogValue() slog.Value {
|
||||
if r == nil || r.req == nil {
|
||||
return slog.Value{}
|
||||
}
|
||||
var groupValueAttrs []slog.Attr
|
||||
groupValueAttrs = append(groupValueAttrs, slog.String("method", r.req.Method))
|
||||
groupValueAttrs = append(groupValueAttrs, slog.String("url", r.req.URL.String()))
|
||||
|
||||
var headerAttr []slog.Attr
|
||||
for k, val := range r.req.Header {
|
||||
headerAttr = append(headerAttr, slog.String(k, strings.Join(val, ",")))
|
||||
}
|
||||
if len(headerAttr) > 0 {
|
||||
groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr))
|
||||
}
|
||||
|
||||
if len(r.payload) > 0 {
|
||||
if attr, ok := processPayload(r.payload); ok {
|
||||
groupValueAttrs = append(groupValueAttrs, attr)
|
||||
}
|
||||
}
|
||||
return slog.GroupValue(groupValueAttrs...)
|
||||
}
|
||||
|
||||
// httpResponse returns a lazily evaluated [slog.LogValuer] for a
|
||||
// [http.Response] and the associated body.
|
||||
func httpResponse(resp *http.Response, body []byte) slog.LogValuer {
|
||||
return &response{
|
||||
resp: resp,
|
||||
payload: body,
|
||||
}
|
||||
}
|
||||
|
||||
type response struct {
|
||||
resp *http.Response
|
||||
payload []byte
|
||||
}
|
||||
|
||||
func (r *response) LogValue() slog.Value {
|
||||
if r == nil {
|
||||
return slog.Value{}
|
||||
}
|
||||
var groupValueAttrs []slog.Attr
|
||||
groupValueAttrs = append(groupValueAttrs, slog.String("status", fmt.Sprint(r.resp.StatusCode)))
|
||||
|
||||
var headerAttr []slog.Attr
|
||||
for k, val := range r.resp.Header {
|
||||
headerAttr = append(headerAttr, slog.String(k, strings.Join(val, ",")))
|
||||
}
|
||||
if len(headerAttr) > 0 {
|
||||
groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr))
|
||||
}
|
||||
|
||||
if len(r.payload) > 0 {
|
||||
if attr, ok := processPayload(r.payload); ok {
|
||||
groupValueAttrs = append(groupValueAttrs, attr)
|
||||
}
|
||||
}
|
||||
return slog.GroupValue(groupValueAttrs...)
|
||||
}
|
||||
|
||||
func processPayload(payload []byte) (slog.Attr, bool) {
|
||||
peekChar := payload[0]
|
||||
if peekChar == '{' {
|
||||
// JSON object
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(payload, &m); err == nil {
|
||||
return slog.Any("payload", m), true
|
||||
}
|
||||
} else if peekChar == '[' {
|
||||
// JSON array
|
||||
var m []any
|
||||
if err := json.Unmarshal(payload, &m); err == nil {
|
||||
return slog.Any("payload", m), true
|
||||
}
|
||||
} else {
|
||||
// Everything else
|
||||
buf := &bytes.Buffer{}
|
||||
if err := json.Compact(buf, payload); err != nil {
|
||||
// Write raw payload incase of error
|
||||
buf.Write(payload)
|
||||
}
|
||||
return slog.String("payload", buf.String()), true
|
||||
}
|
||||
return slog.Attr{}, false
|
||||
}
|
||||
+872
@@ -0,0 +1,872 @@
|
||||
// Copyright 2014 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package metadata provides access to Google Compute Engine (GCE)
|
||||
// metadata and API service accounts.
|
||||
//
|
||||
// This package is a wrapper around the GCE metadata service,
|
||||
// as documented at https://cloud.google.com/compute/docs/metadata/overview.
|
||||
package metadata // import "cloud.google.com/go/compute/metadata"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// metadataIP is the documented metadata server IP address.
|
||||
metadataIP = "169.254.169.254"
|
||||
|
||||
// metadataHostEnv is the environment variable specifying the
|
||||
// GCE metadata hostname. If empty, the default value of
|
||||
// metadataIP ("169.254.169.254") is used instead.
|
||||
// This is variable name is not defined by any spec, as far as
|
||||
// I know; it was made up for the Go package.
|
||||
metadataHostEnv = "GCE_METADATA_HOST"
|
||||
|
||||
userAgent = "gcloud-golang/0.1"
|
||||
)
|
||||
|
||||
type cachedValue struct {
|
||||
k string
|
||||
trim bool
|
||||
mu sync.Mutex
|
||||
v string
|
||||
}
|
||||
|
||||
var (
|
||||
projID = &cachedValue{k: "project/project-id", trim: true}
|
||||
projNum = &cachedValue{k: "project/numeric-project-id", trim: true}
|
||||
instID = &cachedValue{k: "instance/id", trim: true}
|
||||
)
|
||||
|
||||
var defaultClient = &Client{
|
||||
hc: newDefaultHTTPClient(),
|
||||
logger: slog.New(noOpHandler{}),
|
||||
}
|
||||
|
||||
func newDefaultHTTPClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 2 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
IdleConnTimeout: 60 * time.Second,
|
||||
},
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// NotDefinedError is returned when requested metadata is not defined.
|
||||
//
|
||||
// The underlying string is the suffix after "/computeMetadata/v1/".
|
||||
//
|
||||
// This error is not returned if the value is defined to be the empty
|
||||
// string.
|
||||
type NotDefinedError string
|
||||
|
||||
func (suffix NotDefinedError) Error() string {
|
||||
return fmt.Sprintf("metadata: GCE metadata %q not defined", string(suffix))
|
||||
}
|
||||
|
||||
func (c *cachedValue) get(ctx context.Context, cl *Client) (v string, err error) {
|
||||
defer c.mu.Unlock()
|
||||
c.mu.Lock()
|
||||
if c.v != "" {
|
||||
return c.v, nil
|
||||
}
|
||||
if c.trim {
|
||||
v, err = cl.getTrimmed(ctx, c.k)
|
||||
} else {
|
||||
v, err = cl.GetWithContext(ctx, c.k)
|
||||
}
|
||||
if err == nil {
|
||||
c.v = v
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
onGCEOnce sync.Once
|
||||
onGCE bool
|
||||
)
|
||||
|
||||
// OnGCE reports whether this process is running on Google Compute Platforms.
|
||||
// NOTE: True returned from `OnGCE` does not guarantee that the metadata server
|
||||
// is accessible from this process and have all the metadata defined.
|
||||
func OnGCE() bool {
|
||||
onGCEOnce.Do(initOnGCE)
|
||||
return onGCE
|
||||
}
|
||||
|
||||
func initOnGCE() {
|
||||
onGCE = testOnGCE()
|
||||
}
|
||||
|
||||
func testOnGCE() bool {
|
||||
// The user explicitly said they're on GCE, so trust them.
|
||||
if os.Getenv(metadataHostEnv) != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
resc := make(chan bool, 2)
|
||||
|
||||
// Try two strategies in parallel.
|
||||
// See https://github.com/googleapis/google-cloud-go/issues/194
|
||||
go func() {
|
||||
req, _ := http.NewRequest("GET", "http://"+metadataIP, nil)
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
res, err := newDefaultHTTPClient().Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
resc <- false
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
resc <- res.Header.Get("Metadata-Flavor") == "Google"
|
||||
}()
|
||||
|
||||
go func() {
|
||||
resolver := &net.Resolver{}
|
||||
addrs, err := resolver.LookupHost(ctx, "metadata.google.internal.")
|
||||
if err != nil || len(addrs) == 0 {
|
||||
resc <- false
|
||||
return
|
||||
}
|
||||
resc <- strsContains(addrs, metadataIP)
|
||||
}()
|
||||
|
||||
tryHarder := systemInfoSuggestsGCE()
|
||||
if tryHarder {
|
||||
res := <-resc
|
||||
if res {
|
||||
// The first strategy succeeded, so let's use it.
|
||||
return true
|
||||
}
|
||||
// Wait for either the DNS or metadata server probe to
|
||||
// contradict the other one and say we are running on
|
||||
// GCE. Give it a lot of time to do so, since the system
|
||||
// info already suggests we're running on a GCE BIOS.
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case res = <-resc:
|
||||
return res
|
||||
case <-timer.C:
|
||||
// Too slow. Who knows what this system is.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// There's no hint from the system info that we're running on
|
||||
// GCE, so use the first probe's result as truth, whether it's
|
||||
// true or false. The goal here is to optimize for speed for
|
||||
// users who are NOT running on GCE. We can't assume that
|
||||
// either a DNS lookup or an HTTP request to a blackholed IP
|
||||
// address is fast. Worst case this should return when the
|
||||
// metaClient's Transport.ResponseHeaderTimeout or
|
||||
// Transport.Dial.Timeout fires (in two seconds).
|
||||
return <-resc
|
||||
}
|
||||
|
||||
// Subscribe calls Client.SubscribeWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [SubscribeWithContext].
|
||||
func Subscribe(suffix string, fn func(v string, ok bool) error) error {
|
||||
return defaultClient.SubscribeWithContext(context.Background(), suffix, func(ctx context.Context, v string, ok bool) error { return fn(v, ok) })
|
||||
}
|
||||
|
||||
// SubscribeWithContext calls Client.SubscribeWithContext on the default client.
|
||||
func SubscribeWithContext(ctx context.Context, suffix string, fn func(ctx context.Context, v string, ok bool) error) error {
|
||||
return defaultClient.SubscribeWithContext(ctx, suffix, fn)
|
||||
}
|
||||
|
||||
// Get calls Client.GetWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [GetWithContext].
|
||||
func Get(suffix string) (string, error) {
|
||||
return defaultClient.GetWithContext(context.Background(), suffix)
|
||||
}
|
||||
|
||||
// GetWithContext calls Client.GetWithContext on the default client.
|
||||
func GetWithContext(ctx context.Context, suffix string) (string, error) {
|
||||
return defaultClient.GetWithContext(ctx, suffix)
|
||||
}
|
||||
|
||||
// ProjectID returns the current instance's project ID string.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [ProjectIDWithContext].
|
||||
func ProjectID() (string, error) {
|
||||
return defaultClient.ProjectIDWithContext(context.Background())
|
||||
}
|
||||
|
||||
// ProjectIDWithContext returns the current instance's project ID string.
|
||||
func ProjectIDWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.ProjectIDWithContext(ctx)
|
||||
}
|
||||
|
||||
// NumericProjectID returns the current instance's numeric project ID.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [NumericProjectIDWithContext].
|
||||
func NumericProjectID() (string, error) {
|
||||
return defaultClient.NumericProjectIDWithContext(context.Background())
|
||||
}
|
||||
|
||||
// NumericProjectIDWithContext returns the current instance's numeric project ID.
|
||||
func NumericProjectIDWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.NumericProjectIDWithContext(ctx)
|
||||
}
|
||||
|
||||
// InternalIP returns the instance's primary internal IP address.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [InternalIPWithContext].
|
||||
func InternalIP() (string, error) {
|
||||
return defaultClient.InternalIPWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InternalIPWithContext returns the instance's primary internal IP address.
|
||||
func InternalIPWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.InternalIPWithContext(ctx)
|
||||
}
|
||||
|
||||
// ExternalIP returns the instance's primary external (public) IP address.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [ExternalIPWithContext].
|
||||
func ExternalIP() (string, error) {
|
||||
return defaultClient.ExternalIPWithContext(context.Background())
|
||||
}
|
||||
|
||||
// ExternalIPWithContext returns the instance's primary external (public) IP address.
|
||||
func ExternalIPWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.ExternalIPWithContext(ctx)
|
||||
}
|
||||
|
||||
// Email calls Client.EmailWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [EmailWithContext].
|
||||
func Email(serviceAccount string) (string, error) {
|
||||
return defaultClient.EmailWithContext(context.Background(), serviceAccount)
|
||||
}
|
||||
|
||||
// EmailWithContext calls Client.EmailWithContext on the default client.
|
||||
func EmailWithContext(ctx context.Context, serviceAccount string) (string, error) {
|
||||
return defaultClient.EmailWithContext(ctx, serviceAccount)
|
||||
}
|
||||
|
||||
// Hostname returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [HostnameWithContext].
|
||||
func Hostname() (string, error) {
|
||||
return defaultClient.HostnameWithContext(context.Background())
|
||||
}
|
||||
|
||||
// HostnameWithContext returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
func HostnameWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.HostnameWithContext(ctx)
|
||||
}
|
||||
|
||||
// InstanceTags returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [InstanceTagsWithContext].
|
||||
func InstanceTags() ([]string, error) {
|
||||
return defaultClient.InstanceTagsWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceTagsWithContext returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
func InstanceTagsWithContext(ctx context.Context) ([]string, error) {
|
||||
return defaultClient.InstanceTagsWithContext(ctx)
|
||||
}
|
||||
|
||||
// InstanceID returns the current VM's numeric instance ID.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [InstanceIDWithContext].
|
||||
func InstanceID() (string, error) {
|
||||
return defaultClient.InstanceIDWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceIDWithContext returns the current VM's numeric instance ID.
|
||||
func InstanceIDWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.InstanceIDWithContext(ctx)
|
||||
}
|
||||
|
||||
// InstanceName returns the current VM's instance ID string.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [InstanceNameWithContext].
|
||||
func InstanceName() (string, error) {
|
||||
return defaultClient.InstanceNameWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceNameWithContext returns the current VM's instance ID string.
|
||||
func InstanceNameWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.InstanceNameWithContext(ctx)
|
||||
}
|
||||
|
||||
// Zone returns the current VM's zone, such as "us-central1-b".
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [ZoneWithContext].
|
||||
func Zone() (string, error) {
|
||||
return defaultClient.ZoneWithContext(context.Background())
|
||||
}
|
||||
|
||||
// ZoneWithContext returns the current VM's zone, such as "us-central1-b".
|
||||
func ZoneWithContext(ctx context.Context) (string, error) {
|
||||
return defaultClient.ZoneWithContext(ctx)
|
||||
}
|
||||
|
||||
// InstanceAttributes calls Client.InstanceAttributesWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [InstanceAttributesWithContext.
|
||||
func InstanceAttributes() ([]string, error) {
|
||||
return defaultClient.InstanceAttributesWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceAttributesWithContext calls Client.ProjectAttributesWithContext on the default client.
|
||||
func InstanceAttributesWithContext(ctx context.Context) ([]string, error) {
|
||||
return defaultClient.InstanceAttributesWithContext(ctx)
|
||||
}
|
||||
|
||||
// ProjectAttributes calls Client.ProjectAttributesWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [ProjectAttributesWithContext].
|
||||
func ProjectAttributes() ([]string, error) {
|
||||
return defaultClient.ProjectAttributesWithContext(context.Background())
|
||||
}
|
||||
|
||||
// ProjectAttributesWithContext calls Client.ProjectAttributesWithContext on the default client.
|
||||
func ProjectAttributesWithContext(ctx context.Context) ([]string, error) {
|
||||
return defaultClient.ProjectAttributesWithContext(ctx)
|
||||
}
|
||||
|
||||
// InstanceAttributeValue calls Client.InstanceAttributeValueWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [InstanceAttributeValueWithContext].
|
||||
func InstanceAttributeValue(attr string) (string, error) {
|
||||
return defaultClient.InstanceAttributeValueWithContext(context.Background(), attr)
|
||||
}
|
||||
|
||||
// InstanceAttributeValueWithContext calls Client.InstanceAttributeValueWithContext on the default client.
|
||||
func InstanceAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
|
||||
return defaultClient.InstanceAttributeValueWithContext(ctx, attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValue calls Client.ProjectAttributeValueWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [ProjectAttributeValueWithContext].
|
||||
func ProjectAttributeValue(attr string) (string, error) {
|
||||
return defaultClient.ProjectAttributeValueWithContext(context.Background(), attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValueWithContext calls Client.ProjectAttributeValueWithContext on the default client.
|
||||
func ProjectAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
|
||||
return defaultClient.ProjectAttributeValueWithContext(ctx, attr)
|
||||
}
|
||||
|
||||
// Scopes calls Client.ScopesWithContext on the default client.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [ScopesWithContext].
|
||||
func Scopes(serviceAccount string) ([]string, error) {
|
||||
return defaultClient.ScopesWithContext(context.Background(), serviceAccount)
|
||||
}
|
||||
|
||||
// ScopesWithContext calls Client.ScopesWithContext on the default client.
|
||||
func ScopesWithContext(ctx context.Context, serviceAccount string) ([]string, error) {
|
||||
return defaultClient.ScopesWithContext(ctx, serviceAccount)
|
||||
}
|
||||
|
||||
func strsContains(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A Client provides metadata.
|
||||
type Client struct {
|
||||
hc *http.Client
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// Options for configuring a [Client].
|
||||
type Options struct {
|
||||
// Client is the HTTP client used to make requests. Optional.
|
||||
Client *http.Client
|
||||
// Logger is used to log information about HTTP request and responses.
|
||||
// If not provided, nothing will be logged. Optional.
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewClient returns a Client that can be used to fetch metadata.
|
||||
// Returns the client that uses the specified http.Client for HTTP requests.
|
||||
// If nil is specified, returns the default client.
|
||||
func NewClient(c *http.Client) *Client {
|
||||
return NewWithOptions(&Options{
|
||||
Client: c,
|
||||
})
|
||||
}
|
||||
|
||||
// NewWithOptions returns a Client that is configured with the provided Options.
|
||||
func NewWithOptions(opts *Options) *Client {
|
||||
if opts == nil {
|
||||
return defaultClient
|
||||
}
|
||||
client := opts.Client
|
||||
if client == nil {
|
||||
client = newDefaultHTTPClient()
|
||||
}
|
||||
logger := opts.Logger
|
||||
if logger == nil {
|
||||
logger = slog.New(noOpHandler{})
|
||||
}
|
||||
return &Client{hc: client, logger: logger}
|
||||
}
|
||||
|
||||
// getETag returns a value from the metadata service as well as the associated ETag.
|
||||
// This func is otherwise equivalent to Get.
|
||||
func (c *Client) getETag(ctx context.Context, suffix string) (value, etag string, err error) {
|
||||
// Using a fixed IP makes it very difficult to spoof the metadata service in
|
||||
// a container, which is an important use-case for local testing of cloud
|
||||
// deployments. To enable spoofing of the metadata service, the environment
|
||||
// variable GCE_METADATA_HOST is first inspected to decide where metadata
|
||||
// requests shall go.
|
||||
host := os.Getenv(metadataHostEnv)
|
||||
if host == "" {
|
||||
// Using 169.254.169.254 instead of "metadata" here because Go
|
||||
// binaries built with the "netgo" tag and without cgo won't
|
||||
// know the search suffix for "metadata" is
|
||||
// ".google.internal", and this IP address is documented as
|
||||
// being stable anyway.
|
||||
host = metadataIP
|
||||
}
|
||||
suffix = strings.TrimLeft(suffix, "/")
|
||||
u := "http://" + host + "/computeMetadata/v1/" + suffix
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
req.Header.Set("Metadata-Flavor", "Google")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
var res *http.Response
|
||||
var reqErr error
|
||||
var body []byte
|
||||
retryer := newRetryer()
|
||||
for {
|
||||
c.logger.DebugContext(ctx, "metadata request", "request", httpRequest(req, nil))
|
||||
res, reqErr = c.hc.Do(req)
|
||||
var code int
|
||||
if res != nil {
|
||||
code = res.StatusCode
|
||||
body, err = io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
res.Body.Close()
|
||||
return "", "", err
|
||||
}
|
||||
c.logger.DebugContext(ctx, "metadata response", "response", httpResponse(res, body))
|
||||
res.Body.Close()
|
||||
}
|
||||
if delay, shouldRetry := retryer.Retry(code, reqErr); shouldRetry {
|
||||
if res != nil && res.Body != nil {
|
||||
res.Body.Close()
|
||||
}
|
||||
if err := sleep(ctx, delay); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if reqErr != nil {
|
||||
return "", "", reqErr
|
||||
}
|
||||
if res.StatusCode == http.StatusNotFound {
|
||||
return "", "", NotDefinedError(suffix)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return "", "", &Error{Code: res.StatusCode, Message: string(body)}
|
||||
}
|
||||
return string(body), res.Header.Get("Etag"), nil
|
||||
}
|
||||
|
||||
// Get returns a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
//
|
||||
// If the GCE_METADATA_HOST environment variable is not defined, a default of
|
||||
// 169.254.169.254 will be used instead.
|
||||
//
|
||||
// If the requested metadata is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.GetWithContext].
|
||||
func (c *Client) Get(suffix string) (string, error) {
|
||||
return c.GetWithContext(context.Background(), suffix)
|
||||
}
|
||||
|
||||
// GetWithContext returns a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
//
|
||||
// If the GCE_METADATA_HOST environment variable is not defined, a default of
|
||||
// 169.254.169.254 will be used instead.
|
||||
//
|
||||
// If the requested metadata is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// NOTE: Without an extra deadline in the context this call can take in the
|
||||
// worst case, with internal backoff retries, up to 15 seconds (e.g. when server
|
||||
// is responding slowly). Pass context with additional timeouts when needed.
|
||||
func (c *Client) GetWithContext(ctx context.Context, suffix string) (string, error) {
|
||||
val, _, err := c.getETag(ctx, suffix)
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (c *Client) getTrimmed(ctx context.Context, suffix string) (s string, err error) {
|
||||
s, err = c.GetWithContext(ctx, suffix)
|
||||
s = strings.TrimSpace(s)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) lines(ctx context.Context, suffix string) ([]string, error) {
|
||||
j, err := c.GetWithContext(ctx, suffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := strings.Split(strings.TrimSpace(j), "\n")
|
||||
for i := range s {
|
||||
s[i] = strings.TrimSpace(s[i])
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ProjectID returns the current instance's project ID string.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.ProjectIDWithContext].
|
||||
func (c *Client) ProjectID() (string, error) { return c.ProjectIDWithContext(context.Background()) }
|
||||
|
||||
// ProjectIDWithContext returns the current instance's project ID string.
|
||||
func (c *Client) ProjectIDWithContext(ctx context.Context) (string, error) { return projID.get(ctx, c) }
|
||||
|
||||
// NumericProjectID returns the current instance's numeric project ID.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.NumericProjectIDWithContext].
|
||||
func (c *Client) NumericProjectID() (string, error) {
|
||||
return c.NumericProjectIDWithContext(context.Background())
|
||||
}
|
||||
|
||||
// NumericProjectIDWithContext returns the current instance's numeric project ID.
|
||||
func (c *Client) NumericProjectIDWithContext(ctx context.Context) (string, error) {
|
||||
return projNum.get(ctx, c)
|
||||
}
|
||||
|
||||
// InstanceID returns the current VM's numeric instance ID.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.InstanceIDWithContext].
|
||||
func (c *Client) InstanceID() (string, error) {
|
||||
return c.InstanceIDWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceIDWithContext returns the current VM's numeric instance ID.
|
||||
func (c *Client) InstanceIDWithContext(ctx context.Context) (string, error) {
|
||||
return instID.get(ctx, c)
|
||||
}
|
||||
|
||||
// InternalIP returns the instance's primary internal IP address.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.InternalIPWithContext].
|
||||
func (c *Client) InternalIP() (string, error) {
|
||||
return c.InternalIPWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InternalIPWithContext returns the instance's primary internal IP address.
|
||||
func (c *Client) InternalIPWithContext(ctx context.Context) (string, error) {
|
||||
return c.getTrimmed(ctx, "instance/network-interfaces/0/ip")
|
||||
}
|
||||
|
||||
// Email returns the email address associated with the service account.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.EmailWithContext].
|
||||
func (c *Client) Email(serviceAccount string) (string, error) {
|
||||
return c.EmailWithContext(context.Background(), serviceAccount)
|
||||
}
|
||||
|
||||
// EmailWithContext returns the email address associated with the service account.
|
||||
// The serviceAccount parameter default value (empty string or "default" value)
|
||||
// will use the instance's main account.
|
||||
func (c *Client) EmailWithContext(ctx context.Context, serviceAccount string) (string, error) {
|
||||
if serviceAccount == "" {
|
||||
serviceAccount = "default"
|
||||
}
|
||||
return c.getTrimmed(ctx, "instance/service-accounts/"+serviceAccount+"/email")
|
||||
}
|
||||
|
||||
// ExternalIP returns the instance's primary external (public) IP address.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.ExternalIPWithContext].
|
||||
func (c *Client) ExternalIP() (string, error) {
|
||||
return c.ExternalIPWithContext(context.Background())
|
||||
}
|
||||
|
||||
// ExternalIPWithContext returns the instance's primary external (public) IP address.
|
||||
func (c *Client) ExternalIPWithContext(ctx context.Context) (string, error) {
|
||||
return c.getTrimmed(ctx, "instance/network-interfaces/0/access-configs/0/external-ip")
|
||||
}
|
||||
|
||||
// Hostname returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.HostnameWithContext].
|
||||
func (c *Client) Hostname() (string, error) {
|
||||
return c.HostnameWithContext(context.Background())
|
||||
}
|
||||
|
||||
// HostnameWithContext returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
func (c *Client) HostnameWithContext(ctx context.Context) (string, error) {
|
||||
return c.getTrimmed(ctx, "instance/hostname")
|
||||
}
|
||||
|
||||
// InstanceTags returns the list of user-defined instance tags.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.InstanceTagsWithContext].
|
||||
func (c *Client) InstanceTags() ([]string, error) {
|
||||
return c.InstanceTagsWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceTagsWithContext returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
func (c *Client) InstanceTagsWithContext(ctx context.Context) ([]string, error) {
|
||||
var s []string
|
||||
j, err := c.GetWithContext(ctx, "instance/tags")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.NewDecoder(strings.NewReader(j)).Decode(&s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// InstanceName returns the current VM's instance ID string.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.InstanceNameWithContext].
|
||||
func (c *Client) InstanceName() (string, error) {
|
||||
return c.InstanceNameWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceNameWithContext returns the current VM's instance ID string.
|
||||
func (c *Client) InstanceNameWithContext(ctx context.Context) (string, error) {
|
||||
return c.getTrimmed(ctx, "instance/name")
|
||||
}
|
||||
|
||||
// Zone returns the current VM's zone, such as "us-central1-b".
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.ZoneWithContext].
|
||||
func (c *Client) Zone() (string, error) {
|
||||
return c.ZoneWithContext(context.Background())
|
||||
}
|
||||
|
||||
// ZoneWithContext returns the current VM's zone, such as "us-central1-b".
|
||||
func (c *Client) ZoneWithContext(ctx context.Context) (string, error) {
|
||||
zone, err := c.getTrimmed(ctx, "instance/zone")
|
||||
// zone is of the form "projects/<projNum>/zones/<zoneName>".
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return zone[strings.LastIndex(zone, "/")+1:], nil
|
||||
}
|
||||
|
||||
// InstanceAttributes returns the list of user-defined attributes,
|
||||
// assigned when initially creating a GCE VM instance. The value of an
|
||||
// attribute can be obtained with InstanceAttributeValue.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.InstanceAttributesWithContext].
|
||||
func (c *Client) InstanceAttributes() ([]string, error) {
|
||||
return c.InstanceAttributesWithContext(context.Background())
|
||||
}
|
||||
|
||||
// InstanceAttributesWithContext returns the list of user-defined attributes,
|
||||
// assigned when initially creating a GCE VM instance. The value of an
|
||||
// attribute can be obtained with InstanceAttributeValue.
|
||||
func (c *Client) InstanceAttributesWithContext(ctx context.Context) ([]string, error) {
|
||||
return c.lines(ctx, "instance/attributes/")
|
||||
}
|
||||
|
||||
// ProjectAttributes returns the list of user-defined attributes
|
||||
// applying to the project as a whole, not just this VM. The value of
|
||||
// an attribute can be obtained with ProjectAttributeValue.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.ProjectAttributesWithContext].
|
||||
func (c *Client) ProjectAttributes() ([]string, error) {
|
||||
return c.ProjectAttributesWithContext(context.Background())
|
||||
}
|
||||
|
||||
// ProjectAttributesWithContext returns the list of user-defined attributes
|
||||
// applying to the project as a whole, not just this VM. The value of
|
||||
// an attribute can be obtained with ProjectAttributeValue.
|
||||
func (c *Client) ProjectAttributesWithContext(ctx context.Context) ([]string, error) {
|
||||
return c.lines(ctx, "project/attributes/")
|
||||
}
|
||||
|
||||
// InstanceAttributeValue returns the value of the provided VM
|
||||
// instance attribute.
|
||||
//
|
||||
// If the requested attribute is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// InstanceAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.InstanceAttributeValueWithContext].
|
||||
func (c *Client) InstanceAttributeValue(attr string) (string, error) {
|
||||
return c.InstanceAttributeValueWithContext(context.Background(), attr)
|
||||
}
|
||||
|
||||
// InstanceAttributeValueWithContext returns the value of the provided VM
|
||||
// instance attribute.
|
||||
//
|
||||
// If the requested attribute is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// InstanceAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
func (c *Client) InstanceAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
|
||||
return c.GetWithContext(ctx, "instance/attributes/"+attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValue returns the value of the provided
|
||||
// project attribute.
|
||||
//
|
||||
// If the requested attribute is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// ProjectAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.ProjectAttributeValueWithContext].
|
||||
func (c *Client) ProjectAttributeValue(attr string) (string, error) {
|
||||
return c.ProjectAttributeValueWithContext(context.Background(), attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValueWithContext returns the value of the provided
|
||||
// project attribute.
|
||||
//
|
||||
// If the requested attribute is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// ProjectAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
func (c *Client) ProjectAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
|
||||
return c.GetWithContext(ctx, "project/attributes/"+attr)
|
||||
}
|
||||
|
||||
// Scopes returns the service account scopes for the given account.
|
||||
// The account may be empty or the string "default" to use the instance's
|
||||
// main account.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.ScopesWithContext].
|
||||
func (c *Client) Scopes(serviceAccount string) ([]string, error) {
|
||||
return c.ScopesWithContext(context.Background(), serviceAccount)
|
||||
}
|
||||
|
||||
// ScopesWithContext returns the service account scopes for the given account.
|
||||
// The account may be empty or the string "default" to use the instance's
|
||||
// main account.
|
||||
func (c *Client) ScopesWithContext(ctx context.Context, serviceAccount string) ([]string, error) {
|
||||
if serviceAccount == "" {
|
||||
serviceAccount = "default"
|
||||
}
|
||||
return c.lines(ctx, "instance/service-accounts/"+serviceAccount+"/scopes")
|
||||
}
|
||||
|
||||
// Subscribe subscribes to a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
// The suffix may contain query parameters.
|
||||
//
|
||||
// Deprecated: Please use the context aware variant [Client.SubscribeWithContext].
|
||||
func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) error {
|
||||
return c.SubscribeWithContext(context.Background(), suffix, func(ctx context.Context, v string, ok bool) error { return fn(v, ok) })
|
||||
}
|
||||
|
||||
// SubscribeWithContext subscribes to a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
// The suffix may contain query parameters.
|
||||
//
|
||||
// SubscribeWithContext calls fn with the latest metadata value indicated by the
|
||||
// provided suffix. If the metadata value is deleted, fn is called with the
|
||||
// empty string and ok false. Subscribe blocks until fn returns a non-nil error
|
||||
// or the value is deleted. Subscribe returns the error value returned from the
|
||||
// last call to fn, which may be nil when ok == false.
|
||||
func (c *Client) SubscribeWithContext(ctx context.Context, suffix string, fn func(ctx context.Context, v string, ok bool) error) error {
|
||||
const failedSubscribeSleep = time.Second * 5
|
||||
|
||||
// First check to see if the metadata value exists at all.
|
||||
val, lastETag, err := c.getETag(ctx, suffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fn(ctx, val, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ok := true
|
||||
if strings.ContainsRune(suffix, '?') {
|
||||
suffix += "&wait_for_change=true&last_etag="
|
||||
} else {
|
||||
suffix += "?wait_for_change=true&last_etag="
|
||||
}
|
||||
for {
|
||||
val, etag, err := c.getETag(ctx, suffix+url.QueryEscape(lastETag))
|
||||
if err != nil {
|
||||
if _, deleted := err.(NotDefinedError); !deleted {
|
||||
time.Sleep(failedSubscribeSleep)
|
||||
continue // Retry on other errors.
|
||||
}
|
||||
ok = false
|
||||
}
|
||||
lastETag = etag
|
||||
|
||||
if err := fn(ctx, val, ok); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error contains an error response from the server.
|
||||
type Error struct {
|
||||
// Code is the HTTP response status code.
|
||||
Code int
|
||||
// Message is the server response message.
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("compute: Received %d `%s`", e.Code, e.Message)
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright 2021 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
maxRetryAttempts = 5
|
||||
)
|
||||
|
||||
var (
|
||||
syscallRetryable = func(error) bool { return false }
|
||||
)
|
||||
|
||||
// defaultBackoff is basically equivalent to gax.Backoff without the need for
|
||||
// the dependency.
|
||||
type defaultBackoff struct {
|
||||
max time.Duration
|
||||
mul float64
|
||||
cur time.Duration
|
||||
}
|
||||
|
||||
func (b *defaultBackoff) Pause() time.Duration {
|
||||
d := time.Duration(1 + rand.Int63n(int64(b.cur)))
|
||||
b.cur = time.Duration(float64(b.cur) * b.mul)
|
||||
if b.cur > b.max {
|
||||
b.cur = b.max
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// sleep is the equivalent of gax.Sleep without the need for the dependency.
|
||||
func sleep(ctx context.Context, d time.Duration) error {
|
||||
t := time.NewTimer(d)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
return ctx.Err()
|
||||
case <-t.C:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func newRetryer() *metadataRetryer {
|
||||
return &metadataRetryer{bo: &defaultBackoff{
|
||||
cur: 100 * time.Millisecond,
|
||||
max: 30 * time.Second,
|
||||
mul: 2,
|
||||
}}
|
||||
}
|
||||
|
||||
type backoff interface {
|
||||
Pause() time.Duration
|
||||
}
|
||||
|
||||
type metadataRetryer struct {
|
||||
bo backoff
|
||||
attempts int
|
||||
}
|
||||
|
||||
func (r *metadataRetryer) Retry(status int, err error) (time.Duration, bool) {
|
||||
if status == http.StatusOK {
|
||||
return 0, false
|
||||
}
|
||||
retryOk := shouldRetry(status, err)
|
||||
if !retryOk {
|
||||
return 0, false
|
||||
}
|
||||
if r.attempts == maxRetryAttempts {
|
||||
return 0, false
|
||||
}
|
||||
r.attempts++
|
||||
return r.bo.Pause(), true
|
||||
}
|
||||
|
||||
func shouldRetry(status int, err error) bool {
|
||||
if 500 <= status && status <= 599 {
|
||||
return true
|
||||
}
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
return true
|
||||
}
|
||||
// Transient network errors should be retried.
|
||||
if syscallRetryable(err) {
|
||||
return true
|
||||
}
|
||||
if err, ok := err.(interface{ Temporary() bool }); ok {
|
||||
if err.Temporary() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if err, ok := err.(interface{ Unwrap() error }); ok {
|
||||
return shouldRetry(status, err.Unwrap())
|
||||
}
|
||||
return false
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright 2021 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize syscallRetryable to return true on transient socket-level
|
||||
// errors. These errors are specific to Linux.
|
||||
syscallRetryable = func(err error) bool {
|
||||
return errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.ECONNREFUSED)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !windows && !linux
|
||||
|
||||
package metadata
|
||||
|
||||
// systemInfoSuggestsGCE reports whether the local system (without
|
||||
// doing network requests) suggests that we're running on GCE. If this
|
||||
// returns true, testOnGCE tries a bit harder to reach its metadata
|
||||
// server.
|
||||
func systemInfoSuggestsGCE() bool {
|
||||
// We don't currently have checks for other GOOS
|
||||
return false
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build linux
|
||||
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func systemInfoSuggestsGCE() bool {
|
||||
b, _ := os.ReadFile("/sys/class/dmi/id/product_name")
|
||||
name := strings.TrimSpace(string(b))
|
||||
return name == "Google" || name == "Google Compute Engine"
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
func systemInfoSuggestsGCE() bool {
|
||||
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\HardwareConfig\Current`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
s, _, err := k.GetStringValue("SystemProductName")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
return strings.HasPrefix(s, "Google")
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
# Changes
|
||||
|
||||
## [1.18.0](https://github.com/googleapis/google-cloud-go/compare/firestore/v1.17.0...firestore/v1.18.0) (2025-01-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore:** Add String method for Update struct ([#11355](https://github.com/googleapis/google-cloud-go/issues/11355)) ([2320c35](https://github.com/googleapis/google-cloud-go/commit/2320c35ad9a7244c992bfe528e8d49fdc4089369))
|
||||
* **firestore:** Add WithCommitResponseTo TransactionOption ([#6967](https://github.com/googleapis/google-cloud-go/issues/6967)) ([eb25266](https://github.com/googleapis/google-cloud-go/commit/eb252663ad0bdabbd5de1767b42a69fd2aee54b2))
|
||||
* **firestore:** Surfacing the error returned from the service in Bulkwriter ([#10826](https://github.com/googleapis/google-cloud-go/issues/10826)) ([9ae039a](https://github.com/googleapis/google-cloud-go/commit/9ae039a38856133a2bde4c3bd70155d17538c974))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **firestore:** Add UTF-8 validation ([#10881](https://github.com/googleapis/google-cloud-go/issues/10881)) ([9199843](https://github.com/googleapis/google-cloud-go/commit/9199843947bc3a0fa415dba50ba2221850e0fbad))
|
||||
* **firestore:** Allow using != with nil ([#11112](https://github.com/googleapis/google-cloud-go/issues/11112)) ([5b59819](https://github.com/googleapis/google-cloud-go/commit/5b59819e2d603ef55c4cf056b70af6a08d335373))
|
||||
* **firestore:** Update golang.org/x/net to v0.33.0 ([e9b0b69](https://github.com/googleapis/google-cloud-go/commit/e9b0b69644ea5b276cacff0a707e8a5e87efafc9))
|
||||
* **firestore:** Update google.golang.org/api to v0.203.0 ([8bb87d5](https://github.com/googleapis/google-cloud-go/commit/8bb87d56af1cba736e0fe243979723e747e5e11e))
|
||||
* **firestore:** WARNING: On approximately Dec 1, 2024, an update to Protobuf will change service registration function signatures to use an interface instead of a concrete type in generated .pb.go files. This change is expected to affect very few if any users of this client library. For more information, see https://togithub.com/googleapis/google-cloud-go/issues/11020. ([8bb87d5](https://github.com/googleapis/google-cloud-go/commit/8bb87d56af1cba736e0fe243979723e747e5e11e))
|
||||
|
||||
## [1.17.0](https://github.com/googleapis/google-cloud-go/compare/firestore/v1.16.0...firestore/v1.17.0) (2024-09-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore/apiv1:** Add Database.CmekConfig and Database.cmek_config (information about CMEK enablement) ([2d5a9f9](https://github.com/googleapis/google-cloud-go/commit/2d5a9f9ea9a31e341f9a380ae50a650d48c29e99))
|
||||
* **firestore/apiv1:** Add Database.delete_time (the time a database was deleted, if it ever was) ([2d5a9f9](https://github.com/googleapis/google-cloud-go/commit/2d5a9f9ea9a31e341f9a380ae50a650d48c29e99))
|
||||
* **firestore/apiv1:** Add Database.previous_id (if a database was deleted, what ID it was using beforehand) ([2d5a9f9](https://github.com/googleapis/google-cloud-go/commit/2d5a9f9ea9a31e341f9a380ae50a650d48c29e99))
|
||||
* **firestore/apiv1:** Add Database.SourceInfo and Database.source_info (information about database provenance, specifically for restored databases) ([2d5a9f9](https://github.com/googleapis/google-cloud-go/commit/2d5a9f9ea9a31e341f9a380ae50a650d48c29e99))
|
||||
* **firestore/apiv1:** Allow specifying an encryption_config when restoring a database ([2d5a9f9](https://github.com/googleapis/google-cloud-go/commit/2d5a9f9ea9a31e341f9a380ae50a650d48c29e99))
|
||||
* **firestore:** Add support for Go 1.23 iterators ([84461c0](https://github.com/googleapis/google-cloud-go/commit/84461c0ba464ec2f951987ba60030e37c8a8fc18))
|
||||
* **firestore:** Expose the `FindNearest.distance_result_field` parameter ([9a5144e](https://github.com/googleapis/google-cloud-go/commit/9a5144e7d30c6f058b13fdf3fd9436904e77dff0))
|
||||
* **firestore:** Expose the `FindNearest.distance_threshold` parameter ([9a5144e](https://github.com/googleapis/google-cloud-go/commit/9a5144e7d30c6f058b13fdf3fd9436904e77dff0))
|
||||
* **firestore:** Query profiling ([#10164](https://github.com/googleapis/google-cloud-go/issues/10164)) ([58052a2](https://github.com/googleapis/google-cloud-go/commit/58052a2eefd56b3129e04f177398b3ffb688d4d7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **firestore:** Bump dependencies ([2ddeb15](https://github.com/googleapis/google-cloud-go/commit/2ddeb1544a53188a7592046b98913982f1b0cf04))
|
||||
* **firestore:** Retry batchwrite only on retryable errors ([#10603](https://github.com/googleapis/google-cloud-go/issues/10603)) ([23e5df5](https://github.com/googleapis/google-cloud-go/commit/23e5df5b8ee40317ab0d1ac6bb2b92ccc054426c))
|
||||
* **firestore:** Update google.golang.org/api to v0.191.0 ([5b32644](https://github.com/googleapis/google-cloud-go/commit/5b32644eb82eb6bd6021f80b4fad471c60fb9d73))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* **firestore/apiv1:** Clarify maximum retention of backups (max 14 weeks) ([2710d0f](https://github.com/googleapis/google-cloud-go/commit/2710d0f8c66c17f1ddb1d4cc287f7aeb701c0f72))
|
||||
* **firestore/apiv1:** Clarify restore details ([2d5a9f9](https://github.com/googleapis/google-cloud-go/commit/2d5a9f9ea9a31e341f9a380ae50a650d48c29e99))
|
||||
* **firestore/apiv1:** Fix assorted capitalization issues with the word "ID" ([2d5a9f9](https://github.com/googleapis/google-cloud-go/commit/2d5a9f9ea9a31e341f9a380ae50a650d48c29e99))
|
||||
* **firestore/apiv1:** Remove note about backups running at a specific time ([2710d0f](https://github.com/googleapis/google-cloud-go/commit/2710d0f8c66c17f1ddb1d4cc287f7aeb701c0f72))
|
||||
* **firestore/apiv1:** Standardize on the capitalization of "ID" ([2710d0f](https://github.com/googleapis/google-cloud-go/commit/2710d0f8c66c17f1ddb1d4cc287f7aeb701c0f72))
|
||||
* **firestore:** Minor documentation clarifications on FindNearest DistanceMeasure options ([5b4b0f7](https://github.com/googleapis/google-cloud-go/commit/5b4b0f7878276ab5709011778b1b4a6ffd30a60b))
|
||||
|
||||
## [1.16.0](https://github.com/googleapis/google-cloud-go/compare/firestore/v1.15.0...firestore/v1.16.0) (2024-07-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore/apiv1:** Add bulk delete api ([#10369](https://github.com/googleapis/google-cloud-go/issues/10369)) ([134f567](https://github.com/googleapis/google-cloud-go/commit/134f567c18892d6050f60ae875a3de7738104da0))
|
||||
* **firestore/apiv1:** Add Vector Index API ([f8ff971](https://github.com/googleapis/google-cloud-go/commit/f8ff971366999aefb5eb5189c6c9e2bd76a05d9e))
|
||||
* **firestore:** Adding vector search ([#10548](https://github.com/googleapis/google-cloud-go/issues/10548)) ([5c0d6df](https://github.com/googleapis/google-cloud-go/commit/5c0d6df5cc28659c5fbd54329f8b6c134cf95730))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **firestore:** Bump google.golang.org/api@v0.187.0 ([8fa9e39](https://github.com/googleapis/google-cloud-go/commit/8fa9e398e512fd8533fd49060371e61b5725a85b))
|
||||
* **firestore:** Bump google.golang.org/grpc@v1.64.1 ([8ecc4e9](https://github.com/googleapis/google-cloud-go/commit/8ecc4e9622e5bbe9b90384d5848ab816027226c5))
|
||||
* **firestore:** Bump x/net to v0.24.0 ([ba31ed5](https://github.com/googleapis/google-cloud-go/commit/ba31ed5fda2c9664f2e1cf972469295e63deb5b4))
|
||||
* **firestore:** Move createIndexes calls ([#9714](https://github.com/googleapis/google-cloud-go/issues/9714)) ([d931626](https://github.com/googleapis/google-cloud-go/commit/d9316263ca4ad0667d4a0f886a4977b36585b572))
|
||||
* **firestore:** Update dependencies ([257c40b](https://github.com/googleapis/google-cloud-go/commit/257c40bd6d7e59730017cf32bda8823d7a232758))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* **firestore/apiv1:** Allow 14 week backup retention for Firestore daily backups ([#9685](https://github.com/googleapis/google-cloud-go/issues/9685)) ([2cdc40a](https://github.com/googleapis/google-cloud-go/commit/2cdc40a0b4288f5ab5f2b2b8f5c1d6453a9c81ec))
|
||||
* **firestore/apiv1:** Correct BackupSchedule recurrence docs that mentioned specific time of day ([fe85be0](https://github.com/googleapis/google-cloud-go/commit/fe85be03d1e6ba69182ff1045a3faed15aa00128))
|
||||
* **firestore/apiv1:** Update field api description ([134f567](https://github.com/googleapis/google-cloud-go/commit/134f567c18892d6050f60ae875a3de7738104da0))
|
||||
|
||||
## [1.15.0](https://github.com/googleapis/google-cloud-go/compare/firestore/v1.14.0...firestore/v1.15.0) (2024-03-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore/apiv1:** Add DeleteDatabase API and delete protection ([#9185](https://github.com/googleapis/google-cloud-go/issues/9185)) ([ec9b526](https://github.com/googleapis/google-cloud-go/commit/ec9b5268627734c40efd15353cf4bc83a837ff3a))
|
||||
* **firestore/apiv1:** Expose Firestore PITR fields in Database to stable ([5132d0f](https://github.com/googleapis/google-cloud-go/commit/5132d0fea3a5ac902a2c9eee865241ed4509a5f4))
|
||||
* **firestore:** Add new types QueryMode, QueryPlan, ResultSetStats ([82054d0](https://github.com/googleapis/google-cloud-go/commit/82054d0e6905358e48517cbc8ea844dfb624082c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **firestore:** Bump google.golang.org/api to v0.149.0 ([8d2ab9f](https://github.com/googleapis/google-cloud-go/commit/8d2ab9f320a86c1c0fab90513fc05861561d0880))
|
||||
* **firestore:** Correct the cursors when LimitToLast is used ([#9413](https://github.com/googleapis/google-cloud-go/issues/9413)) ([2090651](https://github.com/googleapis/google-cloud-go/commit/2090651b4a7a1dc3be5af4e7ac4607fbc3ffccac))
|
||||
* **firestore:** Enable universe domain resolution options ([fd1d569](https://github.com/googleapis/google-cloud-go/commit/fd1d56930fa8a747be35a224611f4797b8aeb698))
|
||||
* **firestore:** Remove types QueryMode, QueryPlan, ResultSetStats ([97d62c7](https://github.com/googleapis/google-cloud-go/commit/97d62c7a6a305c47670ea9c147edc444f4bf8620))
|
||||
* **firestore:** Return status code from bulkwriter results ([#9030](https://github.com/googleapis/google-cloud-go/issues/9030)) ([e8223c6](https://github.com/googleapis/google-cloud-go/commit/e8223c6ee544237b54b351e421b7092dc3b237a6))
|
||||
* **firestore:** Update grpc-go to v1.56.3 ([343cea8](https://github.com/googleapis/google-cloud-go/commit/343cea8c43b1e31ae21ad50ad31d3b0b60143f8c))
|
||||
* **firestore:** Update grpc-go to v1.59.0 ([81a97b0](https://github.com/googleapis/google-cloud-go/commit/81a97b06cb28b25432e4ece595c55a9857e960b7))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* **firestore/apiv1:** Fix formatting due to unclosed backtick ([0500c7a](https://github.com/googleapis/google-cloud-go/commit/0500c7a7f9a9e8629a091558fa258ca7c5028474))
|
||||
|
||||
## [1.14.0](https://github.com/googleapis/google-cloud-go/compare/firestore/v1.13.0...firestore/v1.14.0) (2023-10-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore:** SUM and AVG aggregations ([#8293](https://github.com/googleapis/google-cloud-go/issues/8293)) ([011f9ff](https://github.com/googleapis/google-cloud-go/commit/011f9ff083bebad5c30443b3b0fd9df68579a65b))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **firestore:** Update golang.org/x/net to v0.17.0 ([174da47](https://github.com/googleapis/google-cloud-go/commit/174da47254fefb12921bbfc65b7829a453af6f5d))
|
||||
|
||||
## [1.13.0](https://github.com/googleapis/google-cloud-go/compare/firestore/v1.12.0...firestore/v1.13.0) (2023-09-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore:** Support for multiple databases ([#5331](https://github.com/googleapis/google-cloud-go/issues/5331)) ([94d4b1b](https://github.com/googleapis/google-cloud-go/commit/94d4b1b58d2c8f3dac18e7efb0be641b6311c775))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **firestore:** Compare full resource path when docs ordered by __name__ ([#8409](https://github.com/googleapis/google-cloud-go/issues/8409)) ([5ef93de](https://github.com/googleapis/google-cloud-go/commit/5ef93de226b854bdf6277b7f906b86755a07d229))
|
||||
* **firestore:** Correcting EndBefore with LimitToLast behaviour ([#8370](https://github.com/googleapis/google-cloud-go/issues/8370)) ([350f7ad](https://github.com/googleapis/google-cloud-go/commit/350f7adb2a087811a70f1c05bf71014022aefeb4))
|
||||
|
||||
## [1.12.0](https://github.com/googleapis/google-cloud-go/compare/firestore/v1.11.0...firestore/v1.12.0) (2023-08-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore:** Publish proto definitions for SUM/AVG in Firestore ([e3f8c89](https://github.com/googleapis/google-cloud-go/commit/e3f8c89429a207c05fee36d5d93efe76f9e29efe))
|
||||
|
||||
## [1.11.0](https://github.com/googleapis/google-cloud-go/compare/firestore/v1.10.0...firestore/v1.11.0) (2023-06-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore:** Update all direct dependencies ([b340d03](https://github.com/googleapis/google-cloud-go/commit/b340d030f2b52a4ce48846ce63984b28583abde6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **firestore:** Cleanup integration test resources ([#8057](https://github.com/googleapis/google-cloud-go/issues/8057)) ([210584d](https://github.com/googleapis/google-cloud-go/commit/210584df494e9627dd13c24138fcbebe85048647))
|
||||
* **firestore:** Do not trace iterator.Done error ([#8082](https://github.com/googleapis/google-cloud-go/issues/8082)) ([5f24d17](https://github.com/googleapis/google-cloud-go/commit/5f24d173db35358d241de186953cd094dae312c9)), refs [#7711](https://github.com/googleapis/google-cloud-go/issues/7711)
|
||||
* **firestore:** REST query UpdateMask bug ([df52820](https://github.com/googleapis/google-cloud-go/commit/df52820b0e7721954809a8aa8700b93c5662dc9b))
|
||||
|
||||
## [1.10.0](https://github.com/googleapis/google-cloud-go/compare/firestore/v1.9.0...firestore/v1.10.0) (2023-05-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore:** Add `OR` query support docs: Improve the API documentation for the `Firestore.ListDocuments` RPC docs: Minor documentation formatting and cleanup ([aeb6fec](https://github.com/googleapis/google-cloud-go/commit/aeb6fecc7fd3f088ff461a0c068ceb9a7ae7b2a3))
|
||||
* **firestore:** Add bloom filter related proto fields PiperOrigin-RevId: 529511263 ([31c3766](https://github.com/googleapis/google-cloud-go/commit/31c3766c9c4cab411669c14fc1a30bd6d2e3f2dd))
|
||||
* **firestore:** Add REST client ([06a54a1](https://github.com/googleapis/google-cloud-go/commit/06a54a16a5866cce966547c51e203b9e09a25bc0))
|
||||
* **firestore:** Added support for REST transport ([aeb6fec](https://github.com/googleapis/google-cloud-go/commit/aeb6fecc7fd3f088ff461a0c068ceb9a7ae7b2a3))
|
||||
* **firestore:** EntityFilter for AND/OR queries ([#7757](https://github.com/googleapis/google-cloud-go/issues/7757)) ([ae37793](https://github.com/googleapis/google-cloud-go/commit/ae377932de20d99f31766ca1cccd2d1cfa18a1c0))
|
||||
* **firestore:** Rewrite signatures and type in terms of new location ([620e6d8](https://github.com/googleapis/google-cloud-go/commit/620e6d828ad8641663ae351bfccfe46281e817ad))
|
||||
* **firestore:** Update iam and longrunning deps ([91a1f78](https://github.com/googleapis/google-cloud-go/commit/91a1f784a109da70f63b96414bba8a9b4254cddd))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **firestore:** Enable rest_numeric_enums for PHP client ([2fef56f](https://github.com/googleapis/google-cloud-go/commit/2fef56f75a63dc4ff6e0eea56c7b26d4831c8e27))
|
||||
* **firestore:** Replace usage of transform with update_transform in batch write ([#7864](https://github.com/googleapis/google-cloud-go/issues/7864)) ([949e4d8](https://github.com/googleapis/google-cloud-go/commit/949e4d8001040e78f2ad9b1e5cbf5b9113d8f3ef))
|
||||
* **firestore:** Update grpc to v1.55.0 ([1147ce0](https://github.com/googleapis/google-cloud-go/commit/1147ce02a990276ca4f8ab7a1ab65c14da4450ef))
|
||||
|
||||
## [1.9.0](https://github.com/googleapis/google-cloud-go/compare/firestore/v1.8.0...firestore/v1.9.0) (2022-11-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore:** start generating proto stubs ([eed371e](https://github.com/googleapis/google-cloud-go/commit/eed371e9b1639c81663c6858db119fb87a126454))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* **firestore:** Adds emulator snippet ([#6926](https://github.com/googleapis/google-cloud-go/issues/6926)) ([456afab](https://github.com/googleapis/google-cloud-go/commit/456afab76f078ef58b7e5b3409acc6b3f71c5b79))
|
||||
|
||||
## [1.8.0](https://github.com/googleapis/google-cloud-go/compare/firestore/v1.7.0...firestore/v1.8.0) (2022-10-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore:** Adds COUNT aggregation query ([#6692](https://github.com/googleapis/google-cloud-go/issues/6692)) ([31ac692](https://github.com/googleapis/google-cloud-go/commit/31ac692d925065981a695266d1e4e22e5374725e))
|
||||
* **firestore:** Adds snapshot reads impl. ([#6718](https://github.com/googleapis/google-cloud-go/issues/6718)) ([43cc5bc](https://github.com/googleapis/google-cloud-go/commit/43cc5bc068d2f3abdde6c65beaac349218fc1a02))
|
||||
|
||||
## [1.7.0](https://github.com/googleapis/google-cloud-go/compare/firestore/v1.6.1...firestore/v1.7.0) (2022-10-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore/apiv1:** add firestore aggregation query apis to the stable googleapis branch ([ec1a190](https://github.com/googleapis/google-cloud-go/commit/ec1a190abbc4436fcaeaa1421c7d9df624042752))
|
||||
* **firestore:** Adds Bulkwriter support to Firestore client ([#5946](https://github.com/googleapis/google-cloud-go/issues/5946)) ([20b6c1b](https://github.com/googleapis/google-cloud-go/commit/20b6c1bbbc28311f4388e163cd9358d1ac0e94d4))
|
||||
* **firestore:** expose read_time fields in Firestore PartitionQuery and ListCollectionIds, currently only available in private preview ([90489b1](https://github.com/googleapis/google-cloud-go/commit/90489b10fd7da4cfafe326e00d1f4d81570147f7))
|
||||
|
||||
### [1.6.1](https://www.github.com/googleapis/google-cloud-go/compare/firestore/v1.6.0...firestore/v1.6.1) (2021-10-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **firestore:** prefer exact matches when reflecting fields ([#4908](https://www.github.com/googleapis/google-cloud-go/issues/4908)) ([d3d9420](https://www.github.com/googleapis/google-cloud-go/commit/d3d94205995ad910bd277f1f930cef4ac86c8040))
|
||||
|
||||
## [1.6.0](https://www.github.com/googleapis/google-cloud-go/compare/firestore/v1.5.0...firestore/v1.6.0) (2021-09-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore:** Add support for PartitionQuery ([#4206](https://www.github.com/googleapis/google-cloud-go/issues/4206)) ([b34783a](https://www.github.com/googleapis/google-cloud-go/commit/b34783a4d7a8c88204e0f44bd411795d8267d811))
|
||||
* **firestore:** Support DocumentRefs in OrderBy, Add Query.Serialize, Query.Deserialize for cross machine serialization ([#4347](https://www.github.com/googleapis/google-cloud-go/issues/4347)) ([a0f7a02](https://www.github.com/googleapis/google-cloud-go/commit/a0f7a02bd8db90fa2297c6e84658868901ef9566))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **firestore:** correct an issue with returning empty paritions from GetPartionedQueries ([#4346](https://www.github.com/googleapis/google-cloud-go/issues/4346)) ([b2a6171](https://www.github.com/googleapis/google-cloud-go/commit/b2a61719b3caf43b095fc290b23de245a2135512))
|
||||
* **firestore:** remove excessive spans on iterator ([#4163](https://www.github.com/googleapis/google-cloud-go/issues/4163)) ([812ef1f](https://www.github.com/googleapis/google-cloud-go/commit/812ef1ffdce2e87570660b58f0e725ad51f68546))
|
||||
* **firestore:** retry RESOURCE_EXHAUSTED errors docs: various documentation improvements ([9a459d5](https://www.github.com/googleapis/google-cloud-go/commit/9a459d5d149b9c3b02a35d4245d164b899ff09b3))
|
||||
|
||||
## [1.5.0](https://www.github.com/googleapis/google-cloud-go/compare/v1.4.0...v1.5.0) (2021-02-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore:** add opencensus tracing support ([#2942](https://www.github.com/googleapis/google-cloud-go/issues/2942)) ([257f322](https://www.github.com/googleapis/google-cloud-go/commit/257f322e68b75765bd316ccefed5461d4df538a0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **firestore:** address a missing branch in watch.stop() error remapping ([#3643](https://www.github.com/googleapis/google-cloud-go/issues/3643)) ([89ad55d](https://www.github.com/googleapis/google-cloud-go/commit/89ad55d72f79995a68f9c2ed1cd9b5ba50009d6d))
|
||||
|
||||
## [1.4.0](https://www.github.com/googleapis/google-cloud-go/compare/firestore/v1.3.0...v1.4.0) (2020-12-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **firestore:** support "!=" and "not-in" query operators ([#3207](https://www.github.com/googleapis/google-cloud-go/issues/3207)) ([5c44019](https://www.github.com/googleapis/google-cloud-go/commit/5c440192105fe3e9b5dd1b584118b309113935e3)), closes [/firebase.google.com/support/release-notes/js#version_7210_-_september_17_2020](https://www.github.com/googleapis//firebase.google.com/support/release-notes/js/issues/version_7210_-_september_17_2020)
|
||||
|
||||
## v1.3.0
|
||||
|
||||
- Add support for LimitToLast feature for queries. This allows
|
||||
a query to return the final N results. See docs
|
||||
[here](https://firebase.google.com/docs/reference/js/firebase.database.Query#limittolast).
|
||||
- Add support for FieldTransformMinimum and FieldTransformMaximum.
|
||||
- Add exported SetGoogleClientInfo method.
|
||||
- Various updates to autogenerated clients.
|
||||
|
||||
## v1.2.0
|
||||
|
||||
- Deprecate v1beta1 client.
|
||||
- Fix serverTimestamp docs.
|
||||
- Add missing operators to query docs.
|
||||
- Make document IDs 20 alpha-numeric characters. Previously, there could be more
|
||||
than 20 non-alphanumeric characters, which broke some users. See
|
||||
https://github.com/googleapis/google-cloud-go/issues/1715.
|
||||
- Various updates to autogenerated clients.
|
||||
|
||||
## v1.1.1
|
||||
|
||||
- Fix bug in CollectionGroup query validation.
|
||||
|
||||
## v1.1.0
|
||||
|
||||
- Add support for `in` and `array-contains-any` query operators.
|
||||
|
||||
## v1.0.0
|
||||
|
||||
This is the first tag to carve out firestore as its own module. See:
|
||||
https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository.
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Copy textproto files in this directory from the source of truth.
|
||||
|
||||
SRC=$(GOPATH)/src/github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore
|
||||
|
||||
.PHONY: refresh-tests
|
||||
|
||||
refresh-tests:
|
||||
-rm genproto/*.pb.go
|
||||
cp $(SRC)/genproto/*.pb.go genproto
|
||||
-rm testdata/*.textproto
|
||||
cp $(SRC)/testdata/*.textproto testdata
|
||||
openssl dgst -sha1 $(SRC)/testdata/test-suite.binproto > testdata/VERSION
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
// Copyright 2024 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.
|
||||
|
||||
// Code generated by protoc-gen-go_gapic. DO NOT EDIT.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
firestorepb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
longrunningpb "cloud.google.com/go/longrunning/autogen/longrunningpb"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
// CursorIterator manages a stream of *firestorepb.Cursor.
|
||||
type CursorIterator struct {
|
||||
items []*firestorepb.Cursor
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
|
||||
// Response is the raw response for the current page.
|
||||
// It must be cast to the RPC response type.
|
||||
// Calling Next() or InternalFetch() updates this value.
|
||||
Response interface{}
|
||||
|
||||
// InternalFetch is for use by the Google Cloud Libraries only.
|
||||
// It is not part of the stable interface of this package.
|
||||
//
|
||||
// InternalFetch returns results from a single call to the underlying RPC.
|
||||
// The number of results is no greater than pageSize.
|
||||
// If there are no more results, nextPageToken is empty and err is nil.
|
||||
InternalFetch func(pageSize int, pageToken string) (results []*firestorepb.Cursor, nextPageToken string, err error)
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the [google.golang.org/api/iterator] package for details.
|
||||
func (it *CursorIterator) PageInfo() *iterator.PageInfo {
|
||||
return it.pageInfo
|
||||
}
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there are no more
|
||||
// results. Once Next returns Done, all subsequent calls will return Done.
|
||||
func (it *CursorIterator) Next() (*firestorepb.Cursor, error) {
|
||||
var item *firestorepb.Cursor
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return item, err
|
||||
}
|
||||
item = it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *CursorIterator) bufLen() int {
|
||||
return len(it.items)
|
||||
}
|
||||
|
||||
func (it *CursorIterator) takeBuf() interface{} {
|
||||
b := it.items
|
||||
it.items = nil
|
||||
return b
|
||||
}
|
||||
|
||||
// DocumentIterator manages a stream of *firestorepb.Document.
|
||||
type DocumentIterator struct {
|
||||
items []*firestorepb.Document
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
|
||||
// Response is the raw response for the current page.
|
||||
// It must be cast to the RPC response type.
|
||||
// Calling Next() or InternalFetch() updates this value.
|
||||
Response interface{}
|
||||
|
||||
// InternalFetch is for use by the Google Cloud Libraries only.
|
||||
// It is not part of the stable interface of this package.
|
||||
//
|
||||
// InternalFetch returns results from a single call to the underlying RPC.
|
||||
// The number of results is no greater than pageSize.
|
||||
// If there are no more results, nextPageToken is empty and err is nil.
|
||||
InternalFetch func(pageSize int, pageToken string) (results []*firestorepb.Document, nextPageToken string, err error)
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the [google.golang.org/api/iterator] package for details.
|
||||
func (it *DocumentIterator) PageInfo() *iterator.PageInfo {
|
||||
return it.pageInfo
|
||||
}
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there are no more
|
||||
// results. Once Next returns Done, all subsequent calls will return Done.
|
||||
func (it *DocumentIterator) Next() (*firestorepb.Document, error) {
|
||||
var item *firestorepb.Document
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return item, err
|
||||
}
|
||||
item = it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *DocumentIterator) bufLen() int {
|
||||
return len(it.items)
|
||||
}
|
||||
|
||||
func (it *DocumentIterator) takeBuf() interface{} {
|
||||
b := it.items
|
||||
it.items = nil
|
||||
return b
|
||||
}
|
||||
|
||||
// OperationIterator manages a stream of *longrunningpb.Operation.
|
||||
type OperationIterator struct {
|
||||
items []*longrunningpb.Operation
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
|
||||
// Response is the raw response for the current page.
|
||||
// It must be cast to the RPC response type.
|
||||
// Calling Next() or InternalFetch() updates this value.
|
||||
Response interface{}
|
||||
|
||||
// InternalFetch is for use by the Google Cloud Libraries only.
|
||||
// It is not part of the stable interface of this package.
|
||||
//
|
||||
// InternalFetch returns results from a single call to the underlying RPC.
|
||||
// The number of results is no greater than pageSize.
|
||||
// If there are no more results, nextPageToken is empty and err is nil.
|
||||
InternalFetch func(pageSize int, pageToken string) (results []*longrunningpb.Operation, nextPageToken string, err error)
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the [google.golang.org/api/iterator] package for details.
|
||||
func (it *OperationIterator) PageInfo() *iterator.PageInfo {
|
||||
return it.pageInfo
|
||||
}
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there are no more
|
||||
// results. Once Next returns Done, all subsequent calls will return Done.
|
||||
func (it *OperationIterator) Next() (*longrunningpb.Operation, error) {
|
||||
var item *longrunningpb.Operation
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return item, err
|
||||
}
|
||||
item = it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *OperationIterator) bufLen() int {
|
||||
return len(it.items)
|
||||
}
|
||||
|
||||
func (it *OperationIterator) takeBuf() interface{} {
|
||||
b := it.items
|
||||
it.items = nil
|
||||
return b
|
||||
}
|
||||
|
||||
// StringIterator manages a stream of string.
|
||||
type StringIterator struct {
|
||||
items []string
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
|
||||
// Response is the raw response for the current page.
|
||||
// It must be cast to the RPC response type.
|
||||
// Calling Next() or InternalFetch() updates this value.
|
||||
Response interface{}
|
||||
|
||||
// InternalFetch is for use by the Google Cloud Libraries only.
|
||||
// It is not part of the stable interface of this package.
|
||||
//
|
||||
// InternalFetch returns results from a single call to the underlying RPC.
|
||||
// The number of results is no greater than pageSize.
|
||||
// If there are no more results, nextPageToken is empty and err is nil.
|
||||
InternalFetch func(pageSize int, pageToken string) (results []string, nextPageToken string, err error)
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the [google.golang.org/api/iterator] package for details.
|
||||
func (it *StringIterator) PageInfo() *iterator.PageInfo {
|
||||
return it.pageInfo
|
||||
}
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there are no more
|
||||
// results. Once Next returns Done, all subsequent calls will return Done.
|
||||
func (it *StringIterator) Next() (string, error) {
|
||||
var item string
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return item, err
|
||||
}
|
||||
item = it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *StringIterator) bufLen() int {
|
||||
return len(it.items)
|
||||
}
|
||||
|
||||
func (it *StringIterator) takeBuf() interface{} {
|
||||
b := it.items
|
||||
it.items = nil
|
||||
return b
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// Copyright 2024 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.
|
||||
|
||||
// Code generated by protoc-gen-go_gapic. DO NOT EDIT.
|
||||
|
||||
//go:build go1.23
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
firestorepb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
longrunningpb "cloud.google.com/go/longrunning/autogen/longrunningpb"
|
||||
"github.com/googleapis/gax-go/v2/iterator"
|
||||
)
|
||||
|
||||
// All returns an iterator. If an error is returned by the iterator, the
|
||||
// iterator will stop after that iteration.
|
||||
func (it *CursorIterator) All() iter.Seq2[*firestorepb.Cursor, error] {
|
||||
return iterator.RangeAdapter(it.Next)
|
||||
}
|
||||
|
||||
// All returns an iterator. If an error is returned by the iterator, the
|
||||
// iterator will stop after that iteration.
|
||||
func (it *DocumentIterator) All() iter.Seq2[*firestorepb.Document, error] {
|
||||
return iterator.RangeAdapter(it.Next)
|
||||
}
|
||||
|
||||
// All returns an iterator. If an error is returned by the iterator, the
|
||||
// iterator will stop after that iteration.
|
||||
func (it *OperationIterator) All() iter.Seq2[*longrunningpb.Operation, error] {
|
||||
return iterator.RangeAdapter(it.Next)
|
||||
}
|
||||
|
||||
// All returns an iterator. If an error is returned by the iterator, the
|
||||
// iterator will stop after that iteration.
|
||||
func (it *StringIterator) All() iter.Seq2[string, error] {
|
||||
return iterator.RangeAdapter(it.Next)
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright 2024 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.
|
||||
|
||||
// Code generated by protoc-gen-go_gapic. DO NOT EDIT.
|
||||
|
||||
// Package firestore is an auto-generated package for the
|
||||
// Cloud Firestore API.
|
||||
//
|
||||
// Accesses the NoSQL document database built for automatic scaling, high
|
||||
// performance, and ease of application development.
|
||||
//
|
||||
// # General documentation
|
||||
//
|
||||
// For information that is relevant for all client libraries please reference
|
||||
// https://pkg.go.dev/cloud.google.com/go#pkg-overview. Some information on this
|
||||
// page includes:
|
||||
//
|
||||
// - [Authentication and Authorization]
|
||||
// - [Timeouts and Cancellation]
|
||||
// - [Testing against Client Libraries]
|
||||
// - [Debugging Client Libraries]
|
||||
// - [Inspecting errors]
|
||||
//
|
||||
// # Example usage
|
||||
//
|
||||
// To get started with this package, create a client.
|
||||
//
|
||||
// // go get cloud.google.com/go/firestore/apiv1@latest
|
||||
// ctx := context.Background()
|
||||
// // This snippet has been automatically generated and should be regarded as a code template only.
|
||||
// // It will require modifications to work:
|
||||
// // - It may require correct/in-range values for request initialization.
|
||||
// // - It may require specifying regional endpoints when creating the service client as shown in:
|
||||
// // https://pkg.go.dev/cloud.google.com/go#hdr-Client_Options
|
||||
// c, err := firestore.NewClient(ctx)
|
||||
// if err != nil {
|
||||
// // TODO: Handle error.
|
||||
// }
|
||||
// defer c.Close()
|
||||
//
|
||||
// The client will use your default application credentials. Clients should be reused instead of created as needed.
|
||||
// The methods of Client are safe for concurrent use by multiple goroutines.
|
||||
// The returned client must be Closed when it is done being used.
|
||||
//
|
||||
// # Using the Client
|
||||
//
|
||||
// The following is an example of making an API call with the newly created client, mentioned above.
|
||||
//
|
||||
// # Use of Context
|
||||
//
|
||||
// The ctx passed to NewClient is used for authentication requests and
|
||||
// for creating the underlying connection, but is not used for subsequent calls.
|
||||
// Individual methods on the client use the ctx given to them.
|
||||
//
|
||||
// To close the open connection, use the Close() method.
|
||||
//
|
||||
// [Authentication and Authorization]: https://pkg.go.dev/cloud.google.com/go#hdr-Authentication_and_Authorization
|
||||
// [Timeouts and Cancellation]: https://pkg.go.dev/cloud.google.com/go#hdr-Timeouts_and_Cancellation
|
||||
// [Testing against Client Libraries]: https://pkg.go.dev/cloud.google.com/go#hdr-Testing
|
||||
// [Debugging Client Libraries]: https://pkg.go.dev/cloud.google.com/go#hdr-Debugging
|
||||
// [Inspecting errors]: https://pkg.go.dev/cloud.google.com/go#hdr-Inspecting_errors
|
||||
package firestore // import "cloud.google.com/go/firestore/apiv1"
|
||||
+2615
File diff suppressed because it is too large
Load Diff
+185
@@ -0,0 +1,185 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc v4.25.3
|
||||
// source: google/firestore/v1/aggregation_result.proto
|
||||
|
||||
package firestorepb
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// The result of a single bucket from a Firestore aggregation query.
|
||||
//
|
||||
// The keys of `aggregate_fields` are the same for all results in an aggregation
|
||||
// query, unlike document queries which can have different fields present for
|
||||
// each result.
|
||||
type AggregationResult struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The result of the aggregation functions, ex: `COUNT(*) AS total_docs`.
|
||||
//
|
||||
// The key is the
|
||||
// [alias][google.firestore.v1.StructuredAggregationQuery.Aggregation.alias]
|
||||
// assigned to the aggregation function on input and the size of this map
|
||||
// equals the number of aggregation functions in the query.
|
||||
AggregateFields map[string]*Value `protobuf:"bytes,2,rep,name=aggregate_fields,json=aggregateFields,proto3" json:"aggregate_fields,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *AggregationResult) Reset() {
|
||||
*x = AggregationResult{}
|
||||
mi := &file_google_firestore_v1_aggregation_result_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AggregationResult) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AggregationResult) ProtoMessage() {}
|
||||
|
||||
func (x *AggregationResult) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_aggregation_result_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AggregationResult.ProtoReflect.Descriptor instead.
|
||||
func (*AggregationResult) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_aggregation_result_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *AggregationResult) GetAggregateFields() map[string]*Value {
|
||||
if x != nil {
|
||||
return x.AggregateFields
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_google_firestore_v1_aggregation_result_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_google_firestore_v1_aggregation_result_proto_rawDesc = []byte{
|
||||
0x0a, 0x2c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x13,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65,
|
||||
0x2e, 0x76, 0x31, 0x1a, 0x22, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x66, 0x69, 0x72, 0x65,
|
||||
0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e,
|
||||
0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xdb, 0x01, 0x0a, 0x11, 0x41, 0x67, 0x67, 0x72,
|
||||
0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x66, 0x0a,
|
||||
0x10, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64,
|
||||
0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x67,
|
||||
0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x2e,
|
||||
0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x46,
|
||||
0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x5e, 0x0a, 0x14, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61,
|
||||
0x74, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a,
|
||||
0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12,
|
||||
0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
|
||||
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72,
|
||||
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0xce, 0x01, 0x0a, 0x17, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76,
|
||||
0x31, 0x42, 0x16, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65,
|
||||
0x73, 0x75, 0x6c, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3b, 0x63, 0x6c, 0x6f,
|
||||
0x75, 0x64, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f,
|
||||
0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x76, 0x31,
|
||||
0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x62, 0x3b, 0x66, 0x69, 0x72,
|
||||
0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x62, 0xa2, 0x02, 0x04, 0x47, 0x43, 0x46, 0x53, 0xaa,
|
||||
0x02, 0x19, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x46,
|
||||
0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x19, 0x47, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x5c, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x5c, 0x46, 0x69, 0x72, 0x65, 0x73,
|
||||
0x74, 0x6f, 0x72, 0x65, 0x5c, 0x56, 0x31, 0xea, 0x02, 0x1c, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x3a, 0x3a, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x3a, 0x3a, 0x46, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_google_firestore_v1_aggregation_result_proto_rawDescOnce sync.Once
|
||||
file_google_firestore_v1_aggregation_result_proto_rawDescData = file_google_firestore_v1_aggregation_result_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_google_firestore_v1_aggregation_result_proto_rawDescGZIP() []byte {
|
||||
file_google_firestore_v1_aggregation_result_proto_rawDescOnce.Do(func() {
|
||||
file_google_firestore_v1_aggregation_result_proto_rawDescData = protoimpl.X.CompressGZIP(file_google_firestore_v1_aggregation_result_proto_rawDescData)
|
||||
})
|
||||
return file_google_firestore_v1_aggregation_result_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_google_firestore_v1_aggregation_result_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_google_firestore_v1_aggregation_result_proto_goTypes = []any{
|
||||
(*AggregationResult)(nil), // 0: google.firestore.v1.AggregationResult
|
||||
nil, // 1: google.firestore.v1.AggregationResult.AggregateFieldsEntry
|
||||
(*Value)(nil), // 2: google.firestore.v1.Value
|
||||
}
|
||||
var file_google_firestore_v1_aggregation_result_proto_depIdxs = []int32{
|
||||
1, // 0: google.firestore.v1.AggregationResult.aggregate_fields:type_name -> google.firestore.v1.AggregationResult.AggregateFieldsEntry
|
||||
2, // 1: google.firestore.v1.AggregationResult.AggregateFieldsEntry.value:type_name -> google.firestore.v1.Value
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_google_firestore_v1_aggregation_result_proto_init() }
|
||||
func file_google_firestore_v1_aggregation_result_proto_init() {
|
||||
if File_google_firestore_v1_aggregation_result_proto != nil {
|
||||
return
|
||||
}
|
||||
file_google_firestore_v1_document_proto_init()
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_google_firestore_v1_aggregation_result_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_google_firestore_v1_aggregation_result_proto_goTypes,
|
||||
DependencyIndexes: file_google_firestore_v1_aggregation_result_proto_depIdxs,
|
||||
MessageInfos: file_google_firestore_v1_aggregation_result_proto_msgTypes,
|
||||
}.Build()
|
||||
File_google_firestore_v1_aggregation_result_proto = out.File
|
||||
file_google_firestore_v1_aggregation_result_proto_rawDesc = nil
|
||||
file_google_firestore_v1_aggregation_result_proto_goTypes = nil
|
||||
file_google_firestore_v1_aggregation_result_proto_depIdxs = nil
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc v4.25.3
|
||||
// source: google/firestore/v1/bloom_filter.proto
|
||||
|
||||
package firestorepb
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// A sequence of bits, encoded in a byte array.
|
||||
//
|
||||
// Each byte in the `bitmap` byte array stores 8 bits of the sequence. The only
|
||||
// exception is the last byte, which may store 8 _or fewer_ bits. The `padding`
|
||||
// defines the number of bits of the last byte to be ignored as "padding". The
|
||||
// values of these "padding" bits are unspecified and must be ignored.
|
||||
//
|
||||
// To retrieve the first bit, bit 0, calculate: `(bitmap[0] & 0x01) != 0`.
|
||||
// To retrieve the second bit, bit 1, calculate: `(bitmap[0] & 0x02) != 0`.
|
||||
// To retrieve the third bit, bit 2, calculate: `(bitmap[0] & 0x04) != 0`.
|
||||
// To retrieve the fourth bit, bit 3, calculate: `(bitmap[0] & 0x08) != 0`.
|
||||
// To retrieve bit n, calculate: `(bitmap[n / 8] & (0x01 << (n % 8))) != 0`.
|
||||
//
|
||||
// The "size" of a `BitSequence` (the number of bits it contains) is calculated
|
||||
// by this formula: `(bitmap.length * 8) - padding`.
|
||||
type BitSequence struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The bytes that encode the bit sequence.
|
||||
// May have a length of zero.
|
||||
Bitmap []byte `protobuf:"bytes,1,opt,name=bitmap,proto3" json:"bitmap,omitempty"`
|
||||
// The number of bits of the last byte in `bitmap` to ignore as "padding".
|
||||
// If the length of `bitmap` is zero, then this value must be `0`.
|
||||
// Otherwise, this value must be between 0 and 7, inclusive.
|
||||
Padding int32 `protobuf:"varint,2,opt,name=padding,proto3" json:"padding,omitempty"`
|
||||
}
|
||||
|
||||
func (x *BitSequence) Reset() {
|
||||
*x = BitSequence{}
|
||||
mi := &file_google_firestore_v1_bloom_filter_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BitSequence) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BitSequence) ProtoMessage() {}
|
||||
|
||||
func (x *BitSequence) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_bloom_filter_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BitSequence.ProtoReflect.Descriptor instead.
|
||||
func (*BitSequence) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_bloom_filter_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *BitSequence) GetBitmap() []byte {
|
||||
if x != nil {
|
||||
return x.Bitmap
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *BitSequence) GetPadding() int32 {
|
||||
if x != nil {
|
||||
return x.Padding
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// A bloom filter (https://en.wikipedia.org/wiki/Bloom_filter).
|
||||
//
|
||||
// The bloom filter hashes the entries with MD5 and treats the resulting 128-bit
|
||||
// hash as 2 distinct 64-bit hash values, interpreted as unsigned integers
|
||||
// using 2's complement encoding.
|
||||
//
|
||||
// These two hash values, named `h1` and `h2`, are then used to compute the
|
||||
// `hash_count` hash values using the formula, starting at `i=0`:
|
||||
//
|
||||
// h(i) = h1 + (i * h2)
|
||||
//
|
||||
// These resulting values are then taken modulo the number of bits in the bloom
|
||||
// filter to get the bits of the bloom filter to test for the given entry.
|
||||
type BloomFilter struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The bloom filter data.
|
||||
Bits *BitSequence `protobuf:"bytes,1,opt,name=bits,proto3" json:"bits,omitempty"`
|
||||
// The number of hashes used by the algorithm.
|
||||
HashCount int32 `protobuf:"varint,2,opt,name=hash_count,json=hashCount,proto3" json:"hash_count,omitempty"`
|
||||
}
|
||||
|
||||
func (x *BloomFilter) Reset() {
|
||||
*x = BloomFilter{}
|
||||
mi := &file_google_firestore_v1_bloom_filter_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BloomFilter) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BloomFilter) ProtoMessage() {}
|
||||
|
||||
func (x *BloomFilter) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_bloom_filter_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BloomFilter.ProtoReflect.Descriptor instead.
|
||||
func (*BloomFilter) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_bloom_filter_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *BloomFilter) GetBits() *BitSequence {
|
||||
if x != nil {
|
||||
return x.Bits
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *BloomFilter) GetHashCount() int32 {
|
||||
if x != nil {
|
||||
return x.HashCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_google_firestore_v1_bloom_filter_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_google_firestore_v1_bloom_filter_proto_rawDesc = []byte{
|
||||
0x0a, 0x26, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x6c, 0x6f, 0x6f, 0x6d, 0x5f, 0x66, 0x69, 0x6c, 0x74,
|
||||
0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x13, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x22, 0x3f, 0x0a,
|
||||
0x0b, 0x42, 0x69, 0x74, 0x53, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06,
|
||||
0x62, 0x69, 0x74, 0x6d, 0x61, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x62, 0x69,
|
||||
0x74, 0x6d, 0x61, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x22, 0x62,
|
||||
0x0a, 0x0b, 0x42, 0x6c, 0x6f, 0x6f, 0x6d, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x34, 0x0a,
|
||||
0x04, 0x62, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x42, 0x69, 0x74, 0x53, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x04, 0x62,
|
||||
0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x63, 0x6f, 0x75, 0x6e,
|
||||
0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x68, 0x61, 0x73, 0x68, 0x43, 0x6f, 0x75,
|
||||
0x6e, 0x74, 0x42, 0xc8, 0x01, 0x0a, 0x17, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x10,
|
||||
0x42, 0x6c, 0x6f, 0x6f, 0x6d, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x50, 0x01, 0x5a, 0x3b, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72,
|
||||
0x65, 0x2f, 0x61, 0x70, 0x69, 0x76, 0x31, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72,
|
||||
0x65, 0x70, 0x62, 0x3b, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x62, 0xa2,
|
||||
0x02, 0x04, 0x47, 0x43, 0x46, 0x53, 0xaa, 0x02, 0x19, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||
0x43, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e,
|
||||
0x56, 0x31, 0xca, 0x02, 0x19, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x5c, 0x43, 0x6c, 0x6f, 0x75,
|
||||
0x64, 0x5c, 0x46, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5c, 0x56, 0x31, 0xea, 0x02,
|
||||
0x1c, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x3a, 0x3a, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x3a, 0x3a,
|
||||
0x46, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_google_firestore_v1_bloom_filter_proto_rawDescOnce sync.Once
|
||||
file_google_firestore_v1_bloom_filter_proto_rawDescData = file_google_firestore_v1_bloom_filter_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_google_firestore_v1_bloom_filter_proto_rawDescGZIP() []byte {
|
||||
file_google_firestore_v1_bloom_filter_proto_rawDescOnce.Do(func() {
|
||||
file_google_firestore_v1_bloom_filter_proto_rawDescData = protoimpl.X.CompressGZIP(file_google_firestore_v1_bloom_filter_proto_rawDescData)
|
||||
})
|
||||
return file_google_firestore_v1_bloom_filter_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_google_firestore_v1_bloom_filter_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_google_firestore_v1_bloom_filter_proto_goTypes = []any{
|
||||
(*BitSequence)(nil), // 0: google.firestore.v1.BitSequence
|
||||
(*BloomFilter)(nil), // 1: google.firestore.v1.BloomFilter
|
||||
}
|
||||
var file_google_firestore_v1_bloom_filter_proto_depIdxs = []int32{
|
||||
0, // 0: google.firestore.v1.BloomFilter.bits:type_name -> google.firestore.v1.BitSequence
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_google_firestore_v1_bloom_filter_proto_init() }
|
||||
func file_google_firestore_v1_bloom_filter_proto_init() {
|
||||
if File_google_firestore_v1_bloom_filter_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_google_firestore_v1_bloom_filter_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_google_firestore_v1_bloom_filter_proto_goTypes,
|
||||
DependencyIndexes: file_google_firestore_v1_bloom_filter_proto_depIdxs,
|
||||
MessageInfos: file_google_firestore_v1_bloom_filter_proto_msgTypes,
|
||||
}.Build()
|
||||
File_google_firestore_v1_bloom_filter_proto = out.File
|
||||
file_google_firestore_v1_bloom_filter_proto_rawDesc = nil
|
||||
file_google_firestore_v1_bloom_filter_proto_goTypes = nil
|
||||
file_google_firestore_v1_bloom_filter_proto_depIdxs = nil
|
||||
}
|
||||
+510
@@ -0,0 +1,510 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc v4.25.3
|
||||
// source: google/firestore/v1/common.proto
|
||||
|
||||
package firestorepb
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// A set of field paths on a document.
|
||||
// Used to restrict a get or update operation on a document to a subset of its
|
||||
// fields.
|
||||
// This is different from standard field masks, as this is always scoped to a
|
||||
// [Document][google.firestore.v1.Document], and takes in account the dynamic
|
||||
// nature of [Value][google.firestore.v1.Value].
|
||||
type DocumentMask struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The list of field paths in the mask. See
|
||||
// [Document.fields][google.firestore.v1.Document.fields] for a field path
|
||||
// syntax reference.
|
||||
FieldPaths []string `protobuf:"bytes,1,rep,name=field_paths,json=fieldPaths,proto3" json:"field_paths,omitempty"`
|
||||
}
|
||||
|
||||
func (x *DocumentMask) Reset() {
|
||||
*x = DocumentMask{}
|
||||
mi := &file_google_firestore_v1_common_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DocumentMask) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DocumentMask) ProtoMessage() {}
|
||||
|
||||
func (x *DocumentMask) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_common_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DocumentMask.ProtoReflect.Descriptor instead.
|
||||
func (*DocumentMask) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_common_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *DocumentMask) GetFieldPaths() []string {
|
||||
if x != nil {
|
||||
return x.FieldPaths
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A precondition on a document, used for conditional operations.
|
||||
type Precondition struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The type of precondition.
|
||||
//
|
||||
// Types that are assignable to ConditionType:
|
||||
//
|
||||
// *Precondition_Exists
|
||||
// *Precondition_UpdateTime
|
||||
ConditionType isPrecondition_ConditionType `protobuf_oneof:"condition_type"`
|
||||
}
|
||||
|
||||
func (x *Precondition) Reset() {
|
||||
*x = Precondition{}
|
||||
mi := &file_google_firestore_v1_common_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Precondition) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Precondition) ProtoMessage() {}
|
||||
|
||||
func (x *Precondition) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_common_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Precondition.ProtoReflect.Descriptor instead.
|
||||
func (*Precondition) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_common_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (m *Precondition) GetConditionType() isPrecondition_ConditionType {
|
||||
if m != nil {
|
||||
return m.ConditionType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Precondition) GetExists() bool {
|
||||
if x, ok := x.GetConditionType().(*Precondition_Exists); ok {
|
||||
return x.Exists
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Precondition) GetUpdateTime() *timestamppb.Timestamp {
|
||||
if x, ok := x.GetConditionType().(*Precondition_UpdateTime); ok {
|
||||
return x.UpdateTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isPrecondition_ConditionType interface {
|
||||
isPrecondition_ConditionType()
|
||||
}
|
||||
|
||||
type Precondition_Exists struct {
|
||||
// When set to `true`, the target document must exist.
|
||||
// When set to `false`, the target document must not exist.
|
||||
Exists bool `protobuf:"varint,1,opt,name=exists,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Precondition_UpdateTime struct {
|
||||
// When set, the target document must exist and have been last updated at
|
||||
// that time. Timestamp must be microsecond aligned.
|
||||
UpdateTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=update_time,json=updateTime,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*Precondition_Exists) isPrecondition_ConditionType() {}
|
||||
|
||||
func (*Precondition_UpdateTime) isPrecondition_ConditionType() {}
|
||||
|
||||
// Options for creating a new transaction.
|
||||
type TransactionOptions struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The mode of the transaction.
|
||||
//
|
||||
// Types that are assignable to Mode:
|
||||
//
|
||||
// *TransactionOptions_ReadOnly_
|
||||
// *TransactionOptions_ReadWrite_
|
||||
Mode isTransactionOptions_Mode `protobuf_oneof:"mode"`
|
||||
}
|
||||
|
||||
func (x *TransactionOptions) Reset() {
|
||||
*x = TransactionOptions{}
|
||||
mi := &file_google_firestore_v1_common_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TransactionOptions) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TransactionOptions) ProtoMessage() {}
|
||||
|
||||
func (x *TransactionOptions) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_common_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TransactionOptions.ProtoReflect.Descriptor instead.
|
||||
func (*TransactionOptions) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_common_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (m *TransactionOptions) GetMode() isTransactionOptions_Mode {
|
||||
if m != nil {
|
||||
return m.Mode
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TransactionOptions) GetReadOnly() *TransactionOptions_ReadOnly {
|
||||
if x, ok := x.GetMode().(*TransactionOptions_ReadOnly_); ok {
|
||||
return x.ReadOnly
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TransactionOptions) GetReadWrite() *TransactionOptions_ReadWrite {
|
||||
if x, ok := x.GetMode().(*TransactionOptions_ReadWrite_); ok {
|
||||
return x.ReadWrite
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isTransactionOptions_Mode interface {
|
||||
isTransactionOptions_Mode()
|
||||
}
|
||||
|
||||
type TransactionOptions_ReadOnly_ struct {
|
||||
// The transaction can only be used for read operations.
|
||||
ReadOnly *TransactionOptions_ReadOnly `protobuf:"bytes,2,opt,name=read_only,json=readOnly,proto3,oneof"`
|
||||
}
|
||||
|
||||
type TransactionOptions_ReadWrite_ struct {
|
||||
// The transaction can be used for both read and write operations.
|
||||
ReadWrite *TransactionOptions_ReadWrite `protobuf:"bytes,3,opt,name=read_write,json=readWrite,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*TransactionOptions_ReadOnly_) isTransactionOptions_Mode() {}
|
||||
|
||||
func (*TransactionOptions_ReadWrite_) isTransactionOptions_Mode() {}
|
||||
|
||||
// Options for a transaction that can be used to read and write documents.
|
||||
//
|
||||
// Firestore does not allow 3rd party auth requests to create read-write.
|
||||
// transactions.
|
||||
type TransactionOptions_ReadWrite struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// An optional transaction to retry.
|
||||
RetryTransaction []byte `protobuf:"bytes,1,opt,name=retry_transaction,json=retryTransaction,proto3" json:"retry_transaction,omitempty"`
|
||||
}
|
||||
|
||||
func (x *TransactionOptions_ReadWrite) Reset() {
|
||||
*x = TransactionOptions_ReadWrite{}
|
||||
mi := &file_google_firestore_v1_common_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TransactionOptions_ReadWrite) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TransactionOptions_ReadWrite) ProtoMessage() {}
|
||||
|
||||
func (x *TransactionOptions_ReadWrite) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_common_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TransactionOptions_ReadWrite.ProtoReflect.Descriptor instead.
|
||||
func (*TransactionOptions_ReadWrite) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_common_proto_rawDescGZIP(), []int{2, 0}
|
||||
}
|
||||
|
||||
func (x *TransactionOptions_ReadWrite) GetRetryTransaction() []byte {
|
||||
if x != nil {
|
||||
return x.RetryTransaction
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Options for a transaction that can only be used to read documents.
|
||||
type TransactionOptions_ReadOnly struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The consistency mode for this transaction. If not set, defaults to strong
|
||||
// consistency.
|
||||
//
|
||||
// Types that are assignable to ConsistencySelector:
|
||||
//
|
||||
// *TransactionOptions_ReadOnly_ReadTime
|
||||
ConsistencySelector isTransactionOptions_ReadOnly_ConsistencySelector `protobuf_oneof:"consistency_selector"`
|
||||
}
|
||||
|
||||
func (x *TransactionOptions_ReadOnly) Reset() {
|
||||
*x = TransactionOptions_ReadOnly{}
|
||||
mi := &file_google_firestore_v1_common_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TransactionOptions_ReadOnly) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TransactionOptions_ReadOnly) ProtoMessage() {}
|
||||
|
||||
func (x *TransactionOptions_ReadOnly) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_common_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TransactionOptions_ReadOnly.ProtoReflect.Descriptor instead.
|
||||
func (*TransactionOptions_ReadOnly) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_common_proto_rawDescGZIP(), []int{2, 1}
|
||||
}
|
||||
|
||||
func (m *TransactionOptions_ReadOnly) GetConsistencySelector() isTransactionOptions_ReadOnly_ConsistencySelector {
|
||||
if m != nil {
|
||||
return m.ConsistencySelector
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TransactionOptions_ReadOnly) GetReadTime() *timestamppb.Timestamp {
|
||||
if x, ok := x.GetConsistencySelector().(*TransactionOptions_ReadOnly_ReadTime); ok {
|
||||
return x.ReadTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isTransactionOptions_ReadOnly_ConsistencySelector interface {
|
||||
isTransactionOptions_ReadOnly_ConsistencySelector()
|
||||
}
|
||||
|
||||
type TransactionOptions_ReadOnly_ReadTime struct {
|
||||
// Reads documents at the given time.
|
||||
//
|
||||
// This must be a microsecond precision timestamp within the past one
|
||||
// hour, or if Point-in-Time Recovery is enabled, can additionally be a
|
||||
// whole minute timestamp within the past 7 days.
|
||||
ReadTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=read_time,json=readTime,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*TransactionOptions_ReadOnly_ReadTime) isTransactionOptions_ReadOnly_ConsistencySelector() {}
|
||||
|
||||
var File_google_firestore_v1_common_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_google_firestore_v1_common_proto_rawDesc = []byte{
|
||||
0x0a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x12, 0x13, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73,
|
||||
0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
|
||||
0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2f, 0x0a, 0x0c, 0x44, 0x6f, 0x63, 0x75,
|
||||
0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x61, 0x73, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x69, 0x65, 0x6c,
|
||||
0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x66,
|
||||
0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x73, 0x22, 0x79, 0x0a, 0x0c, 0x50, 0x72, 0x65,
|
||||
0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x06, 0x65, 0x78, 0x69,
|
||||
0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x06, 0x65, 0x78, 0x69,
|
||||
0x73, 0x74, 0x73, 0x12, 0x3d, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69,
|
||||
0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
|
||||
0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69,
|
||||
0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
|
||||
0x74, 0x79, 0x70, 0x65, 0x22, 0xda, 0x02, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4f, 0x0a, 0x09, 0x72,
|
||||
0x65, 0x61, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30,
|
||||
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72,
|
||||
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79,
|
||||
0x48, 0x00, 0x52, 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x52, 0x0a, 0x0a,
|
||||
0x72, 0x65, 0x61, 0x64, 0x5f, 0x77, 0x72, 0x69, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x31, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74,
|
||||
0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x57, 0x72,
|
||||
0x69, 0x74, 0x65, 0x48, 0x00, 0x52, 0x09, 0x72, 0x65, 0x61, 0x64, 0x57, 0x72, 0x69, 0x74, 0x65,
|
||||
0x1a, 0x38, 0x0a, 0x09, 0x52, 0x65, 0x61, 0x64, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x2b, 0x0a,
|
||||
0x11, 0x72, 0x65, 0x74, 0x72, 0x79, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x72, 0x65, 0x74, 0x72, 0x79, 0x54,
|
||||
0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x5d, 0x0a, 0x08, 0x52, 0x65,
|
||||
0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x39, 0x0a, 0x09, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x74,
|
||||
0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
|
||||
0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x08, 0x72, 0x65, 0x61, 0x64, 0x54, 0x69, 0x6d,
|
||||
0x65, 0x42, 0x16, 0x0a, 0x14, 0x63, 0x6f, 0x6e, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x79,
|
||||
0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x06, 0x0a, 0x04, 0x6d, 0x6f, 0x64,
|
||||
0x65, 0x42, 0xc3, 0x01, 0x0a, 0x17, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0b, 0x43,
|
||||
0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3b, 0x63, 0x6c,
|
||||
0x6f, 0x75, 0x64, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67,
|
||||
0x6f, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x76,
|
||||
0x31, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x62, 0x3b, 0x66, 0x69,
|
||||
0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x62, 0xa2, 0x02, 0x04, 0x47, 0x43, 0x46, 0x53,
|
||||
0xaa, 0x02, 0x19, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x2e,
|
||||
0x46, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x19, 0x47,
|
||||
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x5c, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x5c, 0x46, 0x69, 0x72, 0x65,
|
||||
0x73, 0x74, 0x6f, 0x72, 0x65, 0x5c, 0x56, 0x31, 0xea, 0x02, 0x1c, 0x47, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x3a, 0x3a, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x3a, 0x3a, 0x46, 0x69, 0x72, 0x65, 0x73, 0x74,
|
||||
0x6f, 0x72, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_google_firestore_v1_common_proto_rawDescOnce sync.Once
|
||||
file_google_firestore_v1_common_proto_rawDescData = file_google_firestore_v1_common_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_google_firestore_v1_common_proto_rawDescGZIP() []byte {
|
||||
file_google_firestore_v1_common_proto_rawDescOnce.Do(func() {
|
||||
file_google_firestore_v1_common_proto_rawDescData = protoimpl.X.CompressGZIP(file_google_firestore_v1_common_proto_rawDescData)
|
||||
})
|
||||
return file_google_firestore_v1_common_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_google_firestore_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||
var file_google_firestore_v1_common_proto_goTypes = []any{
|
||||
(*DocumentMask)(nil), // 0: google.firestore.v1.DocumentMask
|
||||
(*Precondition)(nil), // 1: google.firestore.v1.Precondition
|
||||
(*TransactionOptions)(nil), // 2: google.firestore.v1.TransactionOptions
|
||||
(*TransactionOptions_ReadWrite)(nil), // 3: google.firestore.v1.TransactionOptions.ReadWrite
|
||||
(*TransactionOptions_ReadOnly)(nil), // 4: google.firestore.v1.TransactionOptions.ReadOnly
|
||||
(*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp
|
||||
}
|
||||
var file_google_firestore_v1_common_proto_depIdxs = []int32{
|
||||
5, // 0: google.firestore.v1.Precondition.update_time:type_name -> google.protobuf.Timestamp
|
||||
4, // 1: google.firestore.v1.TransactionOptions.read_only:type_name -> google.firestore.v1.TransactionOptions.ReadOnly
|
||||
3, // 2: google.firestore.v1.TransactionOptions.read_write:type_name -> google.firestore.v1.TransactionOptions.ReadWrite
|
||||
5, // 3: google.firestore.v1.TransactionOptions.ReadOnly.read_time:type_name -> google.protobuf.Timestamp
|
||||
4, // [4:4] is the sub-list for method output_type
|
||||
4, // [4:4] is the sub-list for method input_type
|
||||
4, // [4:4] is the sub-list for extension type_name
|
||||
4, // [4:4] is the sub-list for extension extendee
|
||||
0, // [0:4] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_google_firestore_v1_common_proto_init() }
|
||||
func file_google_firestore_v1_common_proto_init() {
|
||||
if File_google_firestore_v1_common_proto != nil {
|
||||
return
|
||||
}
|
||||
file_google_firestore_v1_common_proto_msgTypes[1].OneofWrappers = []any{
|
||||
(*Precondition_Exists)(nil),
|
||||
(*Precondition_UpdateTime)(nil),
|
||||
}
|
||||
file_google_firestore_v1_common_proto_msgTypes[2].OneofWrappers = []any{
|
||||
(*TransactionOptions_ReadOnly_)(nil),
|
||||
(*TransactionOptions_ReadWrite_)(nil),
|
||||
}
|
||||
file_google_firestore_v1_common_proto_msgTypes[4].OneofWrappers = []any{
|
||||
(*TransactionOptions_ReadOnly_ReadTime)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_google_firestore_v1_common_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 5,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_google_firestore_v1_common_proto_goTypes,
|
||||
DependencyIndexes: file_google_firestore_v1_common_proto_depIdxs,
|
||||
MessageInfos: file_google_firestore_v1_common_proto_msgTypes,
|
||||
}.Build()
|
||||
File_google_firestore_v1_common_proto = out.File
|
||||
file_google_firestore_v1_common_proto_rawDesc = nil
|
||||
file_google_firestore_v1_common_proto_goTypes = nil
|
||||
file_google_firestore_v1_common_proto_depIdxs = nil
|
||||
}
|
||||
+662
@@ -0,0 +1,662 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc v4.25.3
|
||||
// source: google/firestore/v1/document.proto
|
||||
|
||||
package firestorepb
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
_ "google.golang.org/genproto/googleapis/api/annotations"
|
||||
latlng "google.golang.org/genproto/googleapis/type/latlng"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
structpb "google.golang.org/protobuf/types/known/structpb"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// A Firestore document.
|
||||
//
|
||||
// Must not exceed 1 MiB - 4 bytes.
|
||||
type Document struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The resource name of the document, for example
|
||||
// `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
// The document's fields.
|
||||
//
|
||||
// The map keys represent field names.
|
||||
//
|
||||
// Field names matching the regular expression `__.*__` are reserved. Reserved
|
||||
// field names are forbidden except in certain documented contexts. The field
|
||||
// names, represented as UTF-8, must not exceed 1,500 bytes and cannot be
|
||||
// empty.
|
||||
//
|
||||
// Field paths may be used in other contexts to refer to structured fields
|
||||
// defined here. For `map_value`, the field path is represented by a
|
||||
// dot-delimited (`.`) string of segments. Each segment is either a simple
|
||||
// field name (defined below) or a quoted field name. For example, the
|
||||
// structured field `"foo" : { map_value: { "x&y" : { string_value: "hello"
|
||||
// }}}` would be represented by the field path “ foo.`x&y` “.
|
||||
//
|
||||
// A simple field name contains only characters `a` to `z`, `A` to `Z`,
|
||||
// `0` to `9`, or `_`, and must not start with `0` to `9`. For example,
|
||||
// `foo_bar_17`.
|
||||
//
|
||||
// A quoted field name starts and ends with “ ` “ and
|
||||
// may contain any character. Some characters, including “ ` “, must be
|
||||
// escaped using a `\`. For example, “ `x&y` “ represents `x&y` and
|
||||
// “ `bak\`tik` “ represents “ bak`tik “.
|
||||
Fields map[string]*Value `protobuf:"bytes,2,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
// Output only. The time at which the document was created.
|
||||
//
|
||||
// This value increases monotonically when a document is deleted then
|
||||
// recreated. It can also be compared to values from other documents and
|
||||
// the `read_time` of a query.
|
||||
CreateTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"`
|
||||
// Output only. The time at which the document was last changed.
|
||||
//
|
||||
// This value is initially set to the `create_time` then increases
|
||||
// monotonically with each change to the document. It can also be
|
||||
// compared to values from other documents and the `read_time` of a query.
|
||||
UpdateTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Document) Reset() {
|
||||
*x = Document{}
|
||||
mi := &file_google_firestore_v1_document_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Document) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Document) ProtoMessage() {}
|
||||
|
||||
func (x *Document) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_document_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Document.ProtoReflect.Descriptor instead.
|
||||
func (*Document) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_document_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Document) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Document) GetFields() map[string]*Value {
|
||||
if x != nil {
|
||||
return x.Fields
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Document) GetCreateTime() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreateTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Document) GetUpdateTime() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.UpdateTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A message that can hold any of the supported value types.
|
||||
type Value struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Must have a value set.
|
||||
//
|
||||
// Types that are assignable to ValueType:
|
||||
//
|
||||
// *Value_NullValue
|
||||
// *Value_BooleanValue
|
||||
// *Value_IntegerValue
|
||||
// *Value_DoubleValue
|
||||
// *Value_TimestampValue
|
||||
// *Value_StringValue
|
||||
// *Value_BytesValue
|
||||
// *Value_ReferenceValue
|
||||
// *Value_GeoPointValue
|
||||
// *Value_ArrayValue
|
||||
// *Value_MapValue
|
||||
ValueType isValue_ValueType `protobuf_oneof:"value_type"`
|
||||
}
|
||||
|
||||
func (x *Value) Reset() {
|
||||
*x = Value{}
|
||||
mi := &file_google_firestore_v1_document_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Value) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Value) ProtoMessage() {}
|
||||
|
||||
func (x *Value) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_document_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Value.ProtoReflect.Descriptor instead.
|
||||
func (*Value) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_document_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (m *Value) GetValueType() isValue_ValueType {
|
||||
if m != nil {
|
||||
return m.ValueType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Value) GetNullValue() structpb.NullValue {
|
||||
if x, ok := x.GetValueType().(*Value_NullValue); ok {
|
||||
return x.NullValue
|
||||
}
|
||||
return structpb.NullValue(0)
|
||||
}
|
||||
|
||||
func (x *Value) GetBooleanValue() bool {
|
||||
if x, ok := x.GetValueType().(*Value_BooleanValue); ok {
|
||||
return x.BooleanValue
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Value) GetIntegerValue() int64 {
|
||||
if x, ok := x.GetValueType().(*Value_IntegerValue); ok {
|
||||
return x.IntegerValue
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Value) GetDoubleValue() float64 {
|
||||
if x, ok := x.GetValueType().(*Value_DoubleValue); ok {
|
||||
return x.DoubleValue
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Value) GetTimestampValue() *timestamppb.Timestamp {
|
||||
if x, ok := x.GetValueType().(*Value_TimestampValue); ok {
|
||||
return x.TimestampValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Value) GetStringValue() string {
|
||||
if x, ok := x.GetValueType().(*Value_StringValue); ok {
|
||||
return x.StringValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Value) GetBytesValue() []byte {
|
||||
if x, ok := x.GetValueType().(*Value_BytesValue); ok {
|
||||
return x.BytesValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Value) GetReferenceValue() string {
|
||||
if x, ok := x.GetValueType().(*Value_ReferenceValue); ok {
|
||||
return x.ReferenceValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Value) GetGeoPointValue() *latlng.LatLng {
|
||||
if x, ok := x.GetValueType().(*Value_GeoPointValue); ok {
|
||||
return x.GeoPointValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Value) GetArrayValue() *ArrayValue {
|
||||
if x, ok := x.GetValueType().(*Value_ArrayValue); ok {
|
||||
return x.ArrayValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Value) GetMapValue() *MapValue {
|
||||
if x, ok := x.GetValueType().(*Value_MapValue); ok {
|
||||
return x.MapValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isValue_ValueType interface {
|
||||
isValue_ValueType()
|
||||
}
|
||||
|
||||
type Value_NullValue struct {
|
||||
// A null value.
|
||||
NullValue structpb.NullValue `protobuf:"varint,11,opt,name=null_value,json=nullValue,proto3,enum=google.protobuf.NullValue,oneof"`
|
||||
}
|
||||
|
||||
type Value_BooleanValue struct {
|
||||
// A boolean value.
|
||||
BooleanValue bool `protobuf:"varint,1,opt,name=boolean_value,json=booleanValue,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Value_IntegerValue struct {
|
||||
// An integer value.
|
||||
IntegerValue int64 `protobuf:"varint,2,opt,name=integer_value,json=integerValue,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Value_DoubleValue struct {
|
||||
// A double value.
|
||||
DoubleValue float64 `protobuf:"fixed64,3,opt,name=double_value,json=doubleValue,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Value_TimestampValue struct {
|
||||
// A timestamp value.
|
||||
//
|
||||
// Precise only to microseconds. When stored, any additional precision is
|
||||
// rounded down.
|
||||
TimestampValue *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=timestamp_value,json=timestampValue,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Value_StringValue struct {
|
||||
// A string value.
|
||||
//
|
||||
// The string, represented as UTF-8, must not exceed 1 MiB - 89 bytes.
|
||||
// Only the first 1,500 bytes of the UTF-8 representation are considered by
|
||||
// queries.
|
||||
StringValue string `protobuf:"bytes,17,opt,name=string_value,json=stringValue,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Value_BytesValue struct {
|
||||
// A bytes value.
|
||||
//
|
||||
// Must not exceed 1 MiB - 89 bytes.
|
||||
// Only the first 1,500 bytes are considered by queries.
|
||||
BytesValue []byte `protobuf:"bytes,18,opt,name=bytes_value,json=bytesValue,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Value_ReferenceValue struct {
|
||||
// A reference to a document. For example:
|
||||
// `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
|
||||
ReferenceValue string `protobuf:"bytes,5,opt,name=reference_value,json=referenceValue,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Value_GeoPointValue struct {
|
||||
// A geo point value representing a point on the surface of Earth.
|
||||
GeoPointValue *latlng.LatLng `protobuf:"bytes,8,opt,name=geo_point_value,json=geoPointValue,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Value_ArrayValue struct {
|
||||
// An array value.
|
||||
//
|
||||
// Cannot directly contain another array value, though can contain a
|
||||
// map which contains another array.
|
||||
ArrayValue *ArrayValue `protobuf:"bytes,9,opt,name=array_value,json=arrayValue,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Value_MapValue struct {
|
||||
// A map value.
|
||||
MapValue *MapValue `protobuf:"bytes,6,opt,name=map_value,json=mapValue,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*Value_NullValue) isValue_ValueType() {}
|
||||
|
||||
func (*Value_BooleanValue) isValue_ValueType() {}
|
||||
|
||||
func (*Value_IntegerValue) isValue_ValueType() {}
|
||||
|
||||
func (*Value_DoubleValue) isValue_ValueType() {}
|
||||
|
||||
func (*Value_TimestampValue) isValue_ValueType() {}
|
||||
|
||||
func (*Value_StringValue) isValue_ValueType() {}
|
||||
|
||||
func (*Value_BytesValue) isValue_ValueType() {}
|
||||
|
||||
func (*Value_ReferenceValue) isValue_ValueType() {}
|
||||
|
||||
func (*Value_GeoPointValue) isValue_ValueType() {}
|
||||
|
||||
func (*Value_ArrayValue) isValue_ValueType() {}
|
||||
|
||||
func (*Value_MapValue) isValue_ValueType() {}
|
||||
|
||||
// An array value.
|
||||
type ArrayValue struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Values in the array.
|
||||
Values []*Value `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ArrayValue) Reset() {
|
||||
*x = ArrayValue{}
|
||||
mi := &file_google_firestore_v1_document_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ArrayValue) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ArrayValue) ProtoMessage() {}
|
||||
|
||||
func (x *ArrayValue) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_document_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ArrayValue.ProtoReflect.Descriptor instead.
|
||||
func (*ArrayValue) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_document_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *ArrayValue) GetValues() []*Value {
|
||||
if x != nil {
|
||||
return x.Values
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A map value.
|
||||
type MapValue struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The map's fields.
|
||||
//
|
||||
// The map keys represent field names. Field names matching the regular
|
||||
// expression `__.*__` are reserved. Reserved field names are forbidden except
|
||||
// in certain documented contexts. The map keys, represented as UTF-8, must
|
||||
// not exceed 1,500 bytes and cannot be empty.
|
||||
Fields map[string]*Value `protobuf:"bytes,1,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *MapValue) Reset() {
|
||||
*x = MapValue{}
|
||||
mi := &file_google_firestore_v1_document_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MapValue) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MapValue) ProtoMessage() {}
|
||||
|
||||
func (x *MapValue) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_document_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MapValue.ProtoReflect.Descriptor instead.
|
||||
func (*MapValue) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_document_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *MapValue) GetFields() map[string]*Value {
|
||||
if x != nil {
|
||||
return x.Fields
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_google_firestore_v1_document_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_google_firestore_v1_document_proto_rawDesc = []byte{
|
||||
0x0a, 0x22, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x12, 0x13, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72,
|
||||
0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x62, 0x65, 0x68, 0x61,
|
||||
0x76, 0x69, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75,
|
||||
0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
|
||||
0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x18, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x2f, 0x6c, 0x61, 0x74, 0x6c, 0x6e, 0x67, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x22, 0xb2, 0x02, 0x0a, 0x08, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x41, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x02,
|
||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69,
|
||||
0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x63, 0x75, 0x6d,
|
||||
0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,
|
||||
0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x3b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74,
|
||||
0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67,
|
||||
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54,
|
||||
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65,
|
||||
0x54, 0x69, 0x6d, 0x65, 0x12, 0x3b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74,
|
||||
0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
|
||||
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d,
|
||||
0x65, 0x1a, 0x55, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
|
||||
0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
|
||||
0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73,
|
||||
0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76,
|
||||
0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xc0, 0x04, 0x0a, 0x05, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x12, 0x3b, 0x0a, 0x0a, 0x6e, 0x75, 0x6c, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x18, 0x0b, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x48, 0x00, 0x52, 0x09, 0x6e, 0x75, 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12,
|
||||
0x25, 0x0a, 0x0d, 0x62, 0x6f, 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, 0x62, 0x6f, 0x6f, 0x6c, 0x65, 0x61,
|
||||
0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65,
|
||||
0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52,
|
||||
0x0c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a,
|
||||
0x0c, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20,
|
||||
0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x12, 0x45, 0x0a, 0x0f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5f,
|
||||
0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
|
||||
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x73,
|
||||
0x74, 0x61, 0x6d, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x73, 0x74, 0x72,
|
||||
0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x48,
|
||||
0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21,
|
||||
0x0a, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x12, 0x20,
|
||||
0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x12, 0x29, 0x0a, 0x0f, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x5f, 0x76,
|
||||
0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x72, 0x65,
|
||||
0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3d, 0x0a, 0x0f,
|
||||
0x67, 0x65, 0x6f, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
|
||||
0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x74,
|
||||
0x79, 0x70, 0x65, 0x2e, 0x4c, 0x61, 0x74, 0x4c, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x0d, 0x67, 0x65,
|
||||
0x6f, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x42, 0x0a, 0x0b, 0x61,
|
||||
0x72, 0x72, 0x61, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x1f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74,
|
||||
0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x72, 0x72, 0x61, 0x79, 0x56, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x72, 0x72, 0x61, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12,
|
||||
0x3c, 0x0a, 0x09, 0x6d, 0x61, 0x70, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x06, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65,
|
||||
0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x70, 0x56, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x48, 0x00, 0x52, 0x08, 0x6d, 0x61, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x0c, 0x0a,
|
||||
0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x22, 0x40, 0x0a, 0x0a, 0x41,
|
||||
0x72, 0x72, 0x61, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x76, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0xa4, 0x01,
|
||||
0x0a, 0x08, 0x4d, 0x61, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x41, 0x0a, 0x06, 0x66, 0x69,
|
||||
0x65, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x4d, 0x61, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73,
|
||||
0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x55, 0x0a,
|
||||
0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03,
|
||||
0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30,
|
||||
0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65,
|
||||
0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x3a, 0x02, 0x38, 0x01, 0x42, 0xc5, 0x01, 0x0a, 0x17, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31,
|
||||
0x42, 0x0d, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50,
|
||||
0x01, 0x5a, 0x3b, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||
0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65,
|
||||
0x2f, 0x61, 0x70, 0x69, 0x76, 0x31, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65,
|
||||
0x70, 0x62, 0x3b, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x62, 0xa2, 0x02,
|
||||
0x04, 0x47, 0x43, 0x46, 0x53, 0xaa, 0x02, 0x19, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x43,
|
||||
0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x56,
|
||||
0x31, 0xca, 0x02, 0x19, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x5c, 0x43, 0x6c, 0x6f, 0x75, 0x64,
|
||||
0x5c, 0x46, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5c, 0x56, 0x31, 0xea, 0x02, 0x1c,
|
||||
0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x3a, 0x3a, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x3a, 0x3a, 0x46,
|
||||
0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_google_firestore_v1_document_proto_rawDescOnce sync.Once
|
||||
file_google_firestore_v1_document_proto_rawDescData = file_google_firestore_v1_document_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_google_firestore_v1_document_proto_rawDescGZIP() []byte {
|
||||
file_google_firestore_v1_document_proto_rawDescOnce.Do(func() {
|
||||
file_google_firestore_v1_document_proto_rawDescData = protoimpl.X.CompressGZIP(file_google_firestore_v1_document_proto_rawDescData)
|
||||
})
|
||||
return file_google_firestore_v1_document_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_google_firestore_v1_document_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_google_firestore_v1_document_proto_goTypes = []any{
|
||||
(*Document)(nil), // 0: google.firestore.v1.Document
|
||||
(*Value)(nil), // 1: google.firestore.v1.Value
|
||||
(*ArrayValue)(nil), // 2: google.firestore.v1.ArrayValue
|
||||
(*MapValue)(nil), // 3: google.firestore.v1.MapValue
|
||||
nil, // 4: google.firestore.v1.Document.FieldsEntry
|
||||
nil, // 5: google.firestore.v1.MapValue.FieldsEntry
|
||||
(*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
|
||||
(structpb.NullValue)(0), // 7: google.protobuf.NullValue
|
||||
(*latlng.LatLng)(nil), // 8: google.type.LatLng
|
||||
}
|
||||
var file_google_firestore_v1_document_proto_depIdxs = []int32{
|
||||
4, // 0: google.firestore.v1.Document.fields:type_name -> google.firestore.v1.Document.FieldsEntry
|
||||
6, // 1: google.firestore.v1.Document.create_time:type_name -> google.protobuf.Timestamp
|
||||
6, // 2: google.firestore.v1.Document.update_time:type_name -> google.protobuf.Timestamp
|
||||
7, // 3: google.firestore.v1.Value.null_value:type_name -> google.protobuf.NullValue
|
||||
6, // 4: google.firestore.v1.Value.timestamp_value:type_name -> google.protobuf.Timestamp
|
||||
8, // 5: google.firestore.v1.Value.geo_point_value:type_name -> google.type.LatLng
|
||||
2, // 6: google.firestore.v1.Value.array_value:type_name -> google.firestore.v1.ArrayValue
|
||||
3, // 7: google.firestore.v1.Value.map_value:type_name -> google.firestore.v1.MapValue
|
||||
1, // 8: google.firestore.v1.ArrayValue.values:type_name -> google.firestore.v1.Value
|
||||
5, // 9: google.firestore.v1.MapValue.fields:type_name -> google.firestore.v1.MapValue.FieldsEntry
|
||||
1, // 10: google.firestore.v1.Document.FieldsEntry.value:type_name -> google.firestore.v1.Value
|
||||
1, // 11: google.firestore.v1.MapValue.FieldsEntry.value:type_name -> google.firestore.v1.Value
|
||||
12, // [12:12] is the sub-list for method output_type
|
||||
12, // [12:12] is the sub-list for method input_type
|
||||
12, // [12:12] is the sub-list for extension type_name
|
||||
12, // [12:12] is the sub-list for extension extendee
|
||||
0, // [0:12] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_google_firestore_v1_document_proto_init() }
|
||||
func file_google_firestore_v1_document_proto_init() {
|
||||
if File_google_firestore_v1_document_proto != nil {
|
||||
return
|
||||
}
|
||||
file_google_firestore_v1_document_proto_msgTypes[1].OneofWrappers = []any{
|
||||
(*Value_NullValue)(nil),
|
||||
(*Value_BooleanValue)(nil),
|
||||
(*Value_IntegerValue)(nil),
|
||||
(*Value_DoubleValue)(nil),
|
||||
(*Value_TimestampValue)(nil),
|
||||
(*Value_StringValue)(nil),
|
||||
(*Value_BytesValue)(nil),
|
||||
(*Value_ReferenceValue)(nil),
|
||||
(*Value_GeoPointValue)(nil),
|
||||
(*Value_ArrayValue)(nil),
|
||||
(*Value_MapValue)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_google_firestore_v1_document_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 6,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_google_firestore_v1_document_proto_goTypes,
|
||||
DependencyIndexes: file_google_firestore_v1_document_proto_depIdxs,
|
||||
MessageInfos: file_google_firestore_v1_document_proto_msgTypes,
|
||||
}.Build()
|
||||
File_google_firestore_v1_document_proto = out.File
|
||||
file_google_firestore_v1_document_proto_rawDesc = nil
|
||||
file_google_firestore_v1_document_proto_goTypes = nil
|
||||
file_google_firestore_v1_document_proto_depIdxs = nil
|
||||
}
|
||||
+5038
File diff suppressed because it is too large
Load Diff
+2119
File diff suppressed because it is too large
Load Diff
+409
@@ -0,0 +1,409 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc v4.25.3
|
||||
// source: google/firestore/v1/query_profile.proto
|
||||
|
||||
package firestorepb
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
_ "google.golang.org/genproto/googleapis/api/annotations"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
durationpb "google.golang.org/protobuf/types/known/durationpb"
|
||||
structpb "google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// Explain options for the query.
|
||||
type ExplainOptions struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Optional. Whether to execute this query.
|
||||
//
|
||||
// When false (the default), the query will be planned, returning only
|
||||
// metrics from the planning stages.
|
||||
//
|
||||
// When true, the query will be planned and executed, returning the full
|
||||
// query results along with both planning and execution stage metrics.
|
||||
Analyze bool `protobuf:"varint,1,opt,name=analyze,proto3" json:"analyze,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ExplainOptions) Reset() {
|
||||
*x = ExplainOptions{}
|
||||
mi := &file_google_firestore_v1_query_profile_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExplainOptions) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExplainOptions) ProtoMessage() {}
|
||||
|
||||
func (x *ExplainOptions) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_query_profile_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExplainOptions.ProtoReflect.Descriptor instead.
|
||||
func (*ExplainOptions) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_query_profile_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *ExplainOptions) GetAnalyze() bool {
|
||||
if x != nil {
|
||||
return x.Analyze
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Explain metrics for the query.
|
||||
type ExplainMetrics struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Planning phase information for the query.
|
||||
PlanSummary *PlanSummary `protobuf:"bytes,1,opt,name=plan_summary,json=planSummary,proto3" json:"plan_summary,omitempty"`
|
||||
// Aggregated stats from the execution of the query. Only present when
|
||||
// [ExplainOptions.analyze][google.firestore.v1.ExplainOptions.analyze] is set
|
||||
// to true.
|
||||
ExecutionStats *ExecutionStats `protobuf:"bytes,2,opt,name=execution_stats,json=executionStats,proto3" json:"execution_stats,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ExplainMetrics) Reset() {
|
||||
*x = ExplainMetrics{}
|
||||
mi := &file_google_firestore_v1_query_profile_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExplainMetrics) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExplainMetrics) ProtoMessage() {}
|
||||
|
||||
func (x *ExplainMetrics) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_query_profile_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExplainMetrics.ProtoReflect.Descriptor instead.
|
||||
func (*ExplainMetrics) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_query_profile_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *ExplainMetrics) GetPlanSummary() *PlanSummary {
|
||||
if x != nil {
|
||||
return x.PlanSummary
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ExplainMetrics) GetExecutionStats() *ExecutionStats {
|
||||
if x != nil {
|
||||
return x.ExecutionStats
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Planning phase information for the query.
|
||||
type PlanSummary struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The indexes selected for the query. For example:
|
||||
//
|
||||
// [
|
||||
// {"query_scope": "Collection", "properties": "(foo ASC, __name__ ASC)"},
|
||||
// {"query_scope": "Collection", "properties": "(bar ASC, __name__ ASC)"}
|
||||
// ]
|
||||
IndexesUsed []*structpb.Struct `protobuf:"bytes,1,rep,name=indexes_used,json=indexesUsed,proto3" json:"indexes_used,omitempty"`
|
||||
}
|
||||
|
||||
func (x *PlanSummary) Reset() {
|
||||
*x = PlanSummary{}
|
||||
mi := &file_google_firestore_v1_query_profile_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PlanSummary) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PlanSummary) ProtoMessage() {}
|
||||
|
||||
func (x *PlanSummary) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_query_profile_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PlanSummary.ProtoReflect.Descriptor instead.
|
||||
func (*PlanSummary) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_query_profile_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *PlanSummary) GetIndexesUsed() []*structpb.Struct {
|
||||
if x != nil {
|
||||
return x.IndexesUsed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execution statistics for the query.
|
||||
type ExecutionStats struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Total number of results returned, including documents, projections,
|
||||
// aggregation results, keys.
|
||||
ResultsReturned int64 `protobuf:"varint,1,opt,name=results_returned,json=resultsReturned,proto3" json:"results_returned,omitempty"`
|
||||
// Total time to execute the query in the backend.
|
||||
ExecutionDuration *durationpb.Duration `protobuf:"bytes,3,opt,name=execution_duration,json=executionDuration,proto3" json:"execution_duration,omitempty"`
|
||||
// Total billable read operations.
|
||||
ReadOperations int64 `protobuf:"varint,4,opt,name=read_operations,json=readOperations,proto3" json:"read_operations,omitempty"`
|
||||
// Debugging statistics from the execution of the query. Note that the
|
||||
// debugging stats are subject to change as Firestore evolves. It could
|
||||
// include:
|
||||
//
|
||||
// {
|
||||
// "indexes_entries_scanned": "1000",
|
||||
// "documents_scanned": "20",
|
||||
// "billing_details" : {
|
||||
// "documents_billable": "20",
|
||||
// "index_entries_billable": "1000",
|
||||
// "min_query_cost": "0"
|
||||
// }
|
||||
// }
|
||||
DebugStats *structpb.Struct `protobuf:"bytes,5,opt,name=debug_stats,json=debugStats,proto3" json:"debug_stats,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ExecutionStats) Reset() {
|
||||
*x = ExecutionStats{}
|
||||
mi := &file_google_firestore_v1_query_profile_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExecutionStats) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExecutionStats) ProtoMessage() {}
|
||||
|
||||
func (x *ExecutionStats) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_firestore_v1_query_profile_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExecutionStats.ProtoReflect.Descriptor instead.
|
||||
func (*ExecutionStats) Descriptor() ([]byte, []int) {
|
||||
return file_google_firestore_v1_query_profile_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *ExecutionStats) GetResultsReturned() int64 {
|
||||
if x != nil {
|
||||
return x.ResultsReturned
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ExecutionStats) GetExecutionDuration() *durationpb.Duration {
|
||||
if x != nil {
|
||||
return x.ExecutionDuration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ExecutionStats) GetReadOperations() int64 {
|
||||
if x != nil {
|
||||
return x.ReadOperations
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ExecutionStats) GetDebugStats() *structpb.Struct {
|
||||
if x != nil {
|
||||
return x.DebugStats
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_google_firestore_v1_query_profile_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_google_firestore_v1_query_profile_proto_rawDesc = []byte{
|
||||
0x0a, 0x27, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x70, 0x72, 0x6f, 0x66,
|
||||
0x69, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x13, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64,
|
||||
0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a,
|
||||
0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
||||
0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a,
|
||||
0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
||||
0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2f, 0x0a,
|
||||
0x0e, 0x45, 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12,
|
||||
0x1d, 0x0a, 0x07, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
|
||||
0x42, 0x03, 0xe0, 0x41, 0x01, 0x52, 0x07, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x7a, 0x65, 0x22, 0xa3,
|
||||
0x01, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63,
|
||||
0x73, 0x12, 0x43, 0x0a, 0x0c, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72,
|
||||
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6c,
|
||||
0x61, 0x6e, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x0b, 0x70, 0x6c, 0x61, 0x6e, 0x53,
|
||||
0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x4c, 0x0a, 0x0f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x23, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x53,
|
||||
0x74, 0x61, 0x74, 0x73, 0x52, 0x0e, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x53,
|
||||
0x74, 0x61, 0x74, 0x73, 0x22, 0x49, 0x0a, 0x0b, 0x50, 0x6c, 0x61, 0x6e, 0x53, 0x75, 0x6d, 0x6d,
|
||||
0x61, 0x72, 0x79, 0x12, 0x3a, 0x0a, 0x0c, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x5f, 0x75,
|
||||
0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75,
|
||||
0x63, 0x74, 0x52, 0x0b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x55, 0x73, 0x65, 0x64, 0x22,
|
||||
0xe8, 0x01, 0x0a, 0x0e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61,
|
||||
0x74, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x5f, 0x72, 0x65,
|
||||
0x74, 0x75, 0x72, 0x6e, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65,
|
||||
0x73, 0x75, 0x6c, 0x74, 0x73, 0x52, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x65, 0x64, 0x12, 0x48, 0x0a,
|
||||
0x12, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x52, 0x11, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x44,
|
||||
0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x61, 0x64, 0x5f,
|
||||
0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0e, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73,
|
||||
0x12, 0x38, 0x0a, 0x0b, 0x64, 0x65, 0x62, 0x75, 0x67, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18,
|
||||
0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0a,
|
||||
0x64, 0x65, 0x62, 0x75, 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x42, 0xc9, 0x01, 0x0a, 0x17, 0x63,
|
||||
0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74,
|
||||
0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x11, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x72, 0x6f,
|
||||
0x66, 0x69, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3b, 0x63, 0x6c, 0x6f,
|
||||
0x75, 0x64, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f,
|
||||
0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x76, 0x31,
|
||||
0x2f, 0x66, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x62, 0x3b, 0x66, 0x69, 0x72,
|
||||
0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x62, 0xa2, 0x02, 0x04, 0x47, 0x43, 0x46, 0x53, 0xaa,
|
||||
0x02, 0x19, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x46,
|
||||
0x69, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x19, 0x47, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x5c, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x5c, 0x46, 0x69, 0x72, 0x65, 0x73,
|
||||
0x74, 0x6f, 0x72, 0x65, 0x5c, 0x56, 0x31, 0xea, 0x02, 0x1c, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x3a, 0x3a, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x3a, 0x3a, 0x46, 0x69, 0x72, 0x65, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_google_firestore_v1_query_profile_proto_rawDescOnce sync.Once
|
||||
file_google_firestore_v1_query_profile_proto_rawDescData = file_google_firestore_v1_query_profile_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_google_firestore_v1_query_profile_proto_rawDescGZIP() []byte {
|
||||
file_google_firestore_v1_query_profile_proto_rawDescOnce.Do(func() {
|
||||
file_google_firestore_v1_query_profile_proto_rawDescData = protoimpl.X.CompressGZIP(file_google_firestore_v1_query_profile_proto_rawDescData)
|
||||
})
|
||||
return file_google_firestore_v1_query_profile_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_google_firestore_v1_query_profile_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||
var file_google_firestore_v1_query_profile_proto_goTypes = []any{
|
||||
(*ExplainOptions)(nil), // 0: google.firestore.v1.ExplainOptions
|
||||
(*ExplainMetrics)(nil), // 1: google.firestore.v1.ExplainMetrics
|
||||
(*PlanSummary)(nil), // 2: google.firestore.v1.PlanSummary
|
||||
(*ExecutionStats)(nil), // 3: google.firestore.v1.ExecutionStats
|
||||
(*structpb.Struct)(nil), // 4: google.protobuf.Struct
|
||||
(*durationpb.Duration)(nil), // 5: google.protobuf.Duration
|
||||
}
|
||||
var file_google_firestore_v1_query_profile_proto_depIdxs = []int32{
|
||||
2, // 0: google.firestore.v1.ExplainMetrics.plan_summary:type_name -> google.firestore.v1.PlanSummary
|
||||
3, // 1: google.firestore.v1.ExplainMetrics.execution_stats:type_name -> google.firestore.v1.ExecutionStats
|
||||
4, // 2: google.firestore.v1.PlanSummary.indexes_used:type_name -> google.protobuf.Struct
|
||||
5, // 3: google.firestore.v1.ExecutionStats.execution_duration:type_name -> google.protobuf.Duration
|
||||
4, // 4: google.firestore.v1.ExecutionStats.debug_stats:type_name -> google.protobuf.Struct
|
||||
5, // [5:5] is the sub-list for method output_type
|
||||
5, // [5:5] is the sub-list for method input_type
|
||||
5, // [5:5] is the sub-list for extension type_name
|
||||
5, // [5:5] is the sub-list for extension extendee
|
||||
0, // [0:5] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_google_firestore_v1_query_profile_proto_init() }
|
||||
func file_google_firestore_v1_query_profile_proto_init() {
|
||||
if File_google_firestore_v1_query_profile_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_google_firestore_v1_query_profile_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 4,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_google_firestore_v1_query_profile_proto_goTypes,
|
||||
DependencyIndexes: file_google_firestore_v1_query_profile_proto_depIdxs,
|
||||
MessageInfos: file_google_firestore_v1_query_profile_proto_msgTypes,
|
||||
}.Build()
|
||||
File_google_firestore_v1_query_profile_proto = out.File
|
||||
file_google_firestore_v1_query_profile_proto_rawDesc = nil
|
||||
file_google_firestore_v1_query_profile_proto_goTypes = nil
|
||||
file_google_firestore_v1_query_profile_proto_depIdxs = nil
|
||||
}
|
||||
+1121
File diff suppressed because it is too large
Load Diff
+223
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"schema": "1.0",
|
||||
"comment": "This file maps proto services/RPCs to the corresponding library clients/methods.",
|
||||
"language": "go",
|
||||
"protoPackage": "google.firestore.v1",
|
||||
"libraryPackage": "cloud.google.com/go/firestore/apiv1",
|
||||
"services": {
|
||||
"Firestore": {
|
||||
"clients": {
|
||||
"grpc": {
|
||||
"libraryClient": "Client",
|
||||
"rpcs": {
|
||||
"BatchGetDocuments": {
|
||||
"methods": [
|
||||
"BatchGetDocuments"
|
||||
]
|
||||
},
|
||||
"BatchWrite": {
|
||||
"methods": [
|
||||
"BatchWrite"
|
||||
]
|
||||
},
|
||||
"BeginTransaction": {
|
||||
"methods": [
|
||||
"BeginTransaction"
|
||||
]
|
||||
},
|
||||
"CancelOperation": {
|
||||
"methods": [
|
||||
"CancelOperation"
|
||||
]
|
||||
},
|
||||
"Commit": {
|
||||
"methods": [
|
||||
"Commit"
|
||||
]
|
||||
},
|
||||
"CreateDocument": {
|
||||
"methods": [
|
||||
"CreateDocument"
|
||||
]
|
||||
},
|
||||
"DeleteDocument": {
|
||||
"methods": [
|
||||
"DeleteDocument"
|
||||
]
|
||||
},
|
||||
"DeleteOperation": {
|
||||
"methods": [
|
||||
"DeleteOperation"
|
||||
]
|
||||
},
|
||||
"GetDocument": {
|
||||
"methods": [
|
||||
"GetDocument"
|
||||
]
|
||||
},
|
||||
"GetOperation": {
|
||||
"methods": [
|
||||
"GetOperation"
|
||||
]
|
||||
},
|
||||
"ListCollectionIds": {
|
||||
"methods": [
|
||||
"ListCollectionIds"
|
||||
]
|
||||
},
|
||||
"ListDocuments": {
|
||||
"methods": [
|
||||
"ListDocuments"
|
||||
]
|
||||
},
|
||||
"ListOperations": {
|
||||
"methods": [
|
||||
"ListOperations"
|
||||
]
|
||||
},
|
||||
"Listen": {
|
||||
"methods": [
|
||||
"Listen"
|
||||
]
|
||||
},
|
||||
"PartitionQuery": {
|
||||
"methods": [
|
||||
"PartitionQuery"
|
||||
]
|
||||
},
|
||||
"Rollback": {
|
||||
"methods": [
|
||||
"Rollback"
|
||||
]
|
||||
},
|
||||
"RunAggregationQuery": {
|
||||
"methods": [
|
||||
"RunAggregationQuery"
|
||||
]
|
||||
},
|
||||
"RunQuery": {
|
||||
"methods": [
|
||||
"RunQuery"
|
||||
]
|
||||
},
|
||||
"UpdateDocument": {
|
||||
"methods": [
|
||||
"UpdateDocument"
|
||||
]
|
||||
},
|
||||
"Write": {
|
||||
"methods": [
|
||||
"Write"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"rest": {
|
||||
"libraryClient": "Client",
|
||||
"rpcs": {
|
||||
"BatchGetDocuments": {
|
||||
"methods": [
|
||||
"BatchGetDocuments"
|
||||
]
|
||||
},
|
||||
"BatchWrite": {
|
||||
"methods": [
|
||||
"BatchWrite"
|
||||
]
|
||||
},
|
||||
"BeginTransaction": {
|
||||
"methods": [
|
||||
"BeginTransaction"
|
||||
]
|
||||
},
|
||||
"CancelOperation": {
|
||||
"methods": [
|
||||
"CancelOperation"
|
||||
]
|
||||
},
|
||||
"Commit": {
|
||||
"methods": [
|
||||
"Commit"
|
||||
]
|
||||
},
|
||||
"CreateDocument": {
|
||||
"methods": [
|
||||
"CreateDocument"
|
||||
]
|
||||
},
|
||||
"DeleteDocument": {
|
||||
"methods": [
|
||||
"DeleteDocument"
|
||||
]
|
||||
},
|
||||
"DeleteOperation": {
|
||||
"methods": [
|
||||
"DeleteOperation"
|
||||
]
|
||||
},
|
||||
"GetDocument": {
|
||||
"methods": [
|
||||
"GetDocument"
|
||||
]
|
||||
},
|
||||
"GetOperation": {
|
||||
"methods": [
|
||||
"GetOperation"
|
||||
]
|
||||
},
|
||||
"ListCollectionIds": {
|
||||
"methods": [
|
||||
"ListCollectionIds"
|
||||
]
|
||||
},
|
||||
"ListDocuments": {
|
||||
"methods": [
|
||||
"ListDocuments"
|
||||
]
|
||||
},
|
||||
"ListOperations": {
|
||||
"methods": [
|
||||
"ListOperations"
|
||||
]
|
||||
},
|
||||
"Listen": {
|
||||
"methods": [
|
||||
"Listen"
|
||||
]
|
||||
},
|
||||
"PartitionQuery": {
|
||||
"methods": [
|
||||
"PartitionQuery"
|
||||
]
|
||||
},
|
||||
"Rollback": {
|
||||
"methods": [
|
||||
"Rollback"
|
||||
]
|
||||
},
|
||||
"RunAggregationQuery": {
|
||||
"methods": [
|
||||
"RunAggregationQuery"
|
||||
]
|
||||
},
|
||||
"RunQuery": {
|
||||
"methods": [
|
||||
"RunQuery"
|
||||
]
|
||||
},
|
||||
"UpdateDocument": {
|
||||
"methods": [
|
||||
"UpdateDocument"
|
||||
]
|
||||
},
|
||||
"Write": {
|
||||
"methods": [
|
||||
"Write"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// Copyright 2024 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.
|
||||
|
||||
// Code generated by protoc-gen-go_gapic. DO NOT EDIT.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/googleapis/gax-go/v2/internallog"
|
||||
"github.com/googleapis/gax-go/v2/internallog/grpclog"
|
||||
"google.golang.org/api/googleapi"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const serviceName = "firestore.googleapis.com"
|
||||
|
||||
// For more information on implementing a client constructor hook, see
|
||||
// https://github.com/googleapis/google-cloud-go/wiki/Customizing-constructors.
|
||||
type clientHookParams struct{}
|
||||
type clientHook func(context.Context, clientHookParams) ([]option.ClientOption, error)
|
||||
|
||||
var versionClient string
|
||||
|
||||
func getVersionClient() string {
|
||||
if versionClient == "" {
|
||||
return "UNKNOWN"
|
||||
}
|
||||
return versionClient
|
||||
}
|
||||
|
||||
// DefaultAuthScopes reports the default set of authentication scopes to use with this package.
|
||||
func DefaultAuthScopes() []string {
|
||||
return []string{
|
||||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/datastore",
|
||||
}
|
||||
}
|
||||
|
||||
func executeHTTPRequestWithResponse(ctx context.Context, client *http.Client, req *http.Request, logger *slog.Logger, body []byte, rpc string) ([]byte, *http.Response, error) {
|
||||
logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", internallog.HTTPRequest(req, body))
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", internallog.HTTPResponse(resp, buf))
|
||||
if err = googleapi.CheckResponse(resp); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return buf, resp, nil
|
||||
}
|
||||
|
||||
func executeHTTPRequest(ctx context.Context, client *http.Client, req *http.Request, logger *slog.Logger, body []byte, rpc string) ([]byte, error) {
|
||||
buf, _, err := executeHTTPRequestWithResponse(ctx, client, req, logger, body, rpc)
|
||||
return buf, err
|
||||
}
|
||||
|
||||
func executeStreamingHTTPRequest(ctx context.Context, client *http.Client, req *http.Request, logger *slog.Logger, body []byte, rpc string) (*http.Response, error) {
|
||||
logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", internallog.HTTPRequest(req, body))
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", internallog.HTTPResponse(resp, nil))
|
||||
if err = googleapi.CheckResponse(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func executeRPC[I proto.Message, O proto.Message](ctx context.Context, fn func(context.Context, I, ...grpc.CallOption) (O, error), req I, opts []grpc.CallOption, logger *slog.Logger, rpc string) (O, error) {
|
||||
var zero O
|
||||
logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", grpclog.ProtoMessageRequest(ctx, req))
|
||||
resp, err := fn(ctx, req, opts...)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", grpclog.ProtoMessageResponse(resp))
|
||||
return resp, err
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
// SetGoogleClientInfo sets the name and version of the application in
|
||||
// the `x-goog-api-client` header passed on each request. Also passes any
|
||||
// provided key-value pairs. Intended for use by Google-written clients.
|
||||
//
|
||||
// Internal use only.
|
||||
func (c *Client) SetGoogleClientInfo(keyval ...string) {
|
||||
c.setGoogleClientInfo(keyval...)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright 2023 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Code generated by gapicgen. DO NOT EDIT.
|
||||
|
||||
package firestore
|
||||
|
||||
import "cloud.google.com/go/firestore/internal"
|
||||
|
||||
func init() {
|
||||
versionClient = internal.Version
|
||||
}
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
// Copyright 2022 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
vkit "cloud.google.com/go/firestore/apiv1"
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"golang.org/x/time/rate"
|
||||
"google.golang.org/api/support/bundler"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxBatchSize is the max number of writes to send in a request
|
||||
maxBatchSize = 20
|
||||
// maxRetryAttempts is the max number of times to retry a write
|
||||
maxRetryAttempts = 10
|
||||
// defaultStartingMaximumOpsPerSecond is the starting max number of requests to the service per second
|
||||
defaultStartingMaximumOpsPerSecond = 500
|
||||
// maxWritesPerSecond is the starting limit of writes allowed to callers per second
|
||||
maxWritesPerSecond = maxBatchSize * defaultStartingMaximumOpsPerSecond
|
||||
)
|
||||
|
||||
var (
|
||||
batchWriteRetryCodes = map[codes.Code]bool{
|
||||
codes.ResourceExhausted: true,
|
||||
codes.Unavailable: true,
|
||||
codes.Aborted: true,
|
||||
}
|
||||
)
|
||||
|
||||
// bulkWriterResult contains the WriteResult or error results from an individual
|
||||
// write to the database.
|
||||
type bulkWriterResult struct {
|
||||
result *pb.WriteResult // (cached) result from the operation
|
||||
err error // (cached) any errors that occurred
|
||||
}
|
||||
|
||||
// BulkWriterJob provides read-only access to the results of a BulkWriter write attempt.
|
||||
type BulkWriterJob struct {
|
||||
resultChan chan bulkWriterResult // send errors and results to this channel
|
||||
write *pb.Write // the writes to apply to the database
|
||||
attempts int // number of times this write has been attempted
|
||||
resultsLock sync.Mutex // guards the cached wr and e values for the job
|
||||
result *WriteResult // (cached) result from the operation
|
||||
err error // (cached) any errors that occurred
|
||||
ctx context.Context // context for canceling/timing out results
|
||||
}
|
||||
|
||||
// Results gets the results of the BulkWriter write attempt.
|
||||
// This method blocks if the results for this BulkWriterJob haven't been
|
||||
// received.
|
||||
func (j *BulkWriterJob) Results() (*WriteResult, error) {
|
||||
j.resultsLock.Lock()
|
||||
defer j.resultsLock.Unlock()
|
||||
if j.result == nil && j.err == nil {
|
||||
j.result, j.err = j.processResults() // cache the results for additional calls
|
||||
}
|
||||
return j.result, j.err
|
||||
}
|
||||
|
||||
// processResults checks for errors returned from send() and packages up the
|
||||
// results as WriteResult objects
|
||||
func (j *BulkWriterJob) processResults() (*WriteResult, error) {
|
||||
select {
|
||||
case <-j.ctx.Done():
|
||||
return nil, j.ctx.Err()
|
||||
case bwr := <-j.resultChan:
|
||||
if bwr.err != nil {
|
||||
return nil, bwr.err
|
||||
}
|
||||
return writeResultFromProto(bwr.result)
|
||||
}
|
||||
}
|
||||
|
||||
// setError ensures that an error is returned on the error channel of BulkWriterJob.
|
||||
func (j *BulkWriterJob) setError(e error) {
|
||||
bwr := bulkWriterResult{
|
||||
err: e,
|
||||
result: nil,
|
||||
}
|
||||
j.resultChan <- bwr
|
||||
close(j.resultChan)
|
||||
}
|
||||
|
||||
// A BulkWriter supports concurrent writes to multiple documents. The BulkWriter
|
||||
// submits document writes in maximum batches of 20 writes per request. Each
|
||||
// request can contain many different document writes: create, delete, update,
|
||||
// and set are all supported.
|
||||
//
|
||||
// Only one operation (create, set, update, delete) per document is allowed.
|
||||
// BulkWriter cannot promise atomicity: individual writes can fail or succeed
|
||||
// independent of each other. Bulkwriter does not apply writes in any set order;
|
||||
// thus a document can't have set on it immediately after creation.
|
||||
type BulkWriter struct {
|
||||
database string // the database as resource name: projects/[PROJECT]/databases/[DATABASE]
|
||||
start time.Time // when this BulkWriter was started; used to calculate qps and rate increases
|
||||
vc *vkit.Client // internal client
|
||||
maxOpsPerSecond int // number of requests that can be sent per second
|
||||
docUpdatePaths map[string]bool // document paths with corresponding writes in the queue
|
||||
limiter rate.Limiter // limit requests to server to <= 500 qps
|
||||
bundler *bundler.Bundler // handle bundling up writes to Firestore
|
||||
ctx context.Context // context for canceling all BulkWriter operations
|
||||
isOpenLock sync.RWMutex // guards against setting isOpen concurrently
|
||||
isOpen bool // flag that the BulkWriter is closed
|
||||
}
|
||||
|
||||
// newBulkWriter creates a new instance of the BulkWriter.
|
||||
func newBulkWriter(ctx context.Context, c *Client, database string) *BulkWriter {
|
||||
// Although typically we shouldn't store Context objects, in this case we
|
||||
// need to pass this Context through to the Bundler handler.
|
||||
ctx = withResourceHeader(ctx, c.path())
|
||||
|
||||
bw := &BulkWriter{
|
||||
database: database,
|
||||
start: time.Now(),
|
||||
vc: c.c,
|
||||
isOpen: true,
|
||||
maxOpsPerSecond: defaultStartingMaximumOpsPerSecond,
|
||||
docUpdatePaths: make(map[string]bool),
|
||||
ctx: ctx,
|
||||
limiter: *rate.NewLimiter(rate.Limit(maxWritesPerSecond), 1),
|
||||
}
|
||||
|
||||
// can't initialize within struct above; need instance reference to BulkWriter.send()
|
||||
bw.bundler = bundler.NewBundler(&BulkWriterJob{}, bw.send)
|
||||
bw.bundler.HandlerLimit = bw.maxOpsPerSecond
|
||||
bw.bundler.BundleCountThreshold = maxBatchSize
|
||||
|
||||
return bw
|
||||
}
|
||||
|
||||
// End sends all enqueued writes in parallel and closes the BulkWriter to new requests.
|
||||
// After calling End(), calling any additional method automatically returns
|
||||
// with an error. This method completes when there are no more pending writes
|
||||
// in the queue.
|
||||
func (bw *BulkWriter) End() {
|
||||
bw.isOpenLock.Lock()
|
||||
bw.isOpen = false
|
||||
bw.isOpenLock.Unlock()
|
||||
bw.Flush()
|
||||
}
|
||||
|
||||
// Flush commits all writes that have been enqueued up to this point in parallel.
|
||||
// This method blocks execution.
|
||||
func (bw *BulkWriter) Flush() {
|
||||
bw.bundler.Flush()
|
||||
}
|
||||
|
||||
// Create adds a document creation write to the queue of writes to send.
|
||||
// Note: You cannot write to (Create, Update, Set, or Delete) the same document more than once.
|
||||
func (bw *BulkWriter) Create(doc *DocumentRef, datum interface{}) (*BulkWriterJob, error) {
|
||||
bw.isOpenLock.RLock()
|
||||
defer bw.isOpenLock.RUnlock()
|
||||
err := bw.checkWriteConditions(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w, err := doc.newCreateWrites(datum)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("firestore: cannot create %v with %v. %w", doc.ID, datum, err)
|
||||
}
|
||||
|
||||
if len(w) > 1 {
|
||||
return nil, fmt.Errorf("firestore: too many document writes sent to bulkwriter")
|
||||
}
|
||||
|
||||
j := bw.write(w[0])
|
||||
return j, nil
|
||||
}
|
||||
|
||||
// Delete adds a document deletion write to the queue of writes to send.
|
||||
// Note: You cannot write to (Create, Update, Set, or Delete) the same document more than once.
|
||||
func (bw *BulkWriter) Delete(doc *DocumentRef, preconds ...Precondition) (*BulkWriterJob, error) {
|
||||
bw.isOpenLock.RLock()
|
||||
defer bw.isOpenLock.RUnlock()
|
||||
err := bw.checkWriteConditions(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w, err := doc.newDeleteWrites(preconds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("firestore: cannot delete doc %v. %w", doc.ID, err)
|
||||
}
|
||||
|
||||
if len(w) > 1 {
|
||||
return nil, fmt.Errorf("firestore: too many document writes sent to bulkwriter")
|
||||
}
|
||||
|
||||
j := bw.write(w[0])
|
||||
return j, nil
|
||||
}
|
||||
|
||||
// Set adds a document set write to the queue of writes to send.
|
||||
// Note: You cannot write to (Create, Update, Set, or Delete) the same document more than once.
|
||||
func (bw *BulkWriter) Set(doc *DocumentRef, datum interface{}, opts ...SetOption) (*BulkWriterJob, error) {
|
||||
bw.isOpenLock.RLock()
|
||||
defer bw.isOpenLock.RUnlock()
|
||||
err := bw.checkWriteConditions(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w, err := doc.newSetWrites(datum, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("firestore: cannot set %v on doc %v. %w", datum, doc.ID, err)
|
||||
}
|
||||
|
||||
if len(w) > 1 {
|
||||
return nil, fmt.Errorf("firestore: too many writes sent to bulkwriter")
|
||||
}
|
||||
|
||||
j := bw.write(w[0])
|
||||
return j, nil
|
||||
}
|
||||
|
||||
// Update adds a document update write to the queue of writes to send.
|
||||
// Note: You cannot write to (Create, Update, Set, or Delete) the same document more than once.
|
||||
func (bw *BulkWriter) Update(doc *DocumentRef, updates []Update, preconds ...Precondition) (*BulkWriterJob, error) {
|
||||
bw.isOpenLock.RLock()
|
||||
defer bw.isOpenLock.RUnlock()
|
||||
err := bw.checkWriteConditions(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w, err := doc.newUpdatePathWrites(updates, preconds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("firestore: cannot update doc %v. %w", doc.ID, err)
|
||||
}
|
||||
|
||||
if len(w) > 1 {
|
||||
return nil, fmt.Errorf("firestore: too many writes sent to bulkwriter")
|
||||
}
|
||||
|
||||
j := bw.write(w[0])
|
||||
return j, nil
|
||||
}
|
||||
|
||||
// checkConditions determines whether this write attempt is valid. It returns
|
||||
// an error if either the BulkWriter has already been closed or if it
|
||||
// receives a nil document reference.
|
||||
func (bw *BulkWriter) checkWriteConditions(doc *DocumentRef) error {
|
||||
if !bw.isOpen {
|
||||
return errors.New("firestore: BulkWriter has been closed")
|
||||
}
|
||||
|
||||
if doc == nil {
|
||||
return errors.New("firestore: nil document contents")
|
||||
}
|
||||
|
||||
_, havePath := bw.docUpdatePaths[doc.shortPath]
|
||||
if havePath {
|
||||
return fmt.Errorf("firestore: BulkWriter received duplicate write for path: %v", doc.shortPath)
|
||||
}
|
||||
|
||||
bw.docUpdatePaths[doc.shortPath] = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// write packages up write requests into bulkWriterJob objects.
|
||||
func (bw *BulkWriter) write(w *pb.Write) *BulkWriterJob {
|
||||
|
||||
j := &BulkWriterJob{
|
||||
resultChan: make(chan bulkWriterResult, 1),
|
||||
write: w,
|
||||
ctx: bw.ctx,
|
||||
}
|
||||
|
||||
bw.limiter.Wait(bw.ctx)
|
||||
// ignore operation size constraints and related errors; can't be inferred at compile time
|
||||
// Bundler is set to accept an unlimited amount of bytes
|
||||
_ = bw.bundler.Add(j, 0)
|
||||
|
||||
return j
|
||||
}
|
||||
|
||||
// send transmits writes to the service and matches response results to job channels.
|
||||
func (bw *BulkWriter) send(i interface{}) {
|
||||
bwj := i.([]*BulkWriterJob)
|
||||
|
||||
if len(bwj) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var ws []*pb.Write
|
||||
for _, w := range bwj {
|
||||
ws = append(ws, w.write)
|
||||
}
|
||||
|
||||
bwr := &pb.BatchWriteRequest{
|
||||
Database: bw.database,
|
||||
Writes: ws,
|
||||
Labels: map[string]string{},
|
||||
}
|
||||
|
||||
select {
|
||||
case <-bw.ctx.Done():
|
||||
return
|
||||
default:
|
||||
resp, err := bw.vc.BatchWrite(bw.ctx, bwr)
|
||||
if err != nil {
|
||||
// Do we need to be selective about what kind of errors we send?
|
||||
for _, j := range bwj {
|
||||
j.setError(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Match write results with BulkWriterJob objects
|
||||
for i, res := range resp.WriteResults {
|
||||
s := resp.Status[i]
|
||||
c := s.GetCode()
|
||||
if c != 0 { // Should we do an explicit check against rpc.Code enum?
|
||||
j := bwj[i]
|
||||
j.attempts++
|
||||
|
||||
// Do we need separate retry bundler?
|
||||
_, isRetryable := batchWriteRetryCodes[codes.Code(s.Code)]
|
||||
if j.attempts < maxRetryAttempts && isRetryable {
|
||||
// ignore operation size constraints and related errors; job size can't be inferred at compile time
|
||||
// Bundler is set to accept an unlimited amount of bytes
|
||||
_ = bw.bundler.Add(j, 0)
|
||||
} else {
|
||||
j.setError(status.Error(codes.Code(s.Code), s.Message))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
bwj[i].resultChan <- bulkWriterResult{err: nil, result: res}
|
||||
close(bwj[i].resultChan)
|
||||
}
|
||||
}
|
||||
}
|
||||
+467
@@ -0,0 +1,467 @@
|
||||
// Copyright 2017 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
vkit "cloud.google.com/go/firestore/apiv1"
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"cloud.google.com/go/firestore/internal"
|
||||
"cloud.google.com/go/internal/detect"
|
||||
"cloud.google.com/go/internal/trace"
|
||||
gax "github.com/googleapis/gax-go/v2"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// resourcePrefixHeader is the name of the metadata header used to indicate
|
||||
// the resource being operated on.
|
||||
const resourcePrefixHeader = "google-cloud-resource-prefix"
|
||||
|
||||
// requestParamsHeader is routing header required to access named databases
|
||||
const reqParamsHeader = "x-goog-request-params"
|
||||
|
||||
// reqParamsHeaderVal constructs header from dbPath
|
||||
// dbPath is of the form projects/{project_id}/databases/{database_id}
|
||||
func reqParamsHeaderVal(dbPath string) string {
|
||||
splitPath := strings.Split(dbPath, "/")
|
||||
projectID := splitPath[1]
|
||||
databaseID := splitPath[3]
|
||||
return fmt.Sprintf("project_id=%s&database_id=%s", url.QueryEscape(projectID), url.QueryEscape(databaseID))
|
||||
}
|
||||
|
||||
// DetectProjectID is a sentinel value that instructs NewClient to detect the
|
||||
// project ID. It is given in place of the projectID argument. NewClient will
|
||||
// use the project ID from the given credentials or the default credentials
|
||||
// (https://developers.google.com/accounts/docs/application-default-credentials)
|
||||
// if no credentials were provided. When providing credentials, not all
|
||||
// options will allow NewClient to extract the project ID. Specifically a JWT
|
||||
// does not have the project ID encoded.
|
||||
const DetectProjectID = detect.ProjectIDSentinel
|
||||
|
||||
// DefaultDatabaseID is name of the default database
|
||||
const DefaultDatabaseID = "(default)"
|
||||
|
||||
// A Client provides access to the Firestore service.
|
||||
type Client struct {
|
||||
c *vkit.Client
|
||||
projectID string
|
||||
databaseID string // A client is tied to a single database.
|
||||
readSettings *readSettings // readSettings allows setting a snapshot time to read the database
|
||||
}
|
||||
|
||||
// NewClient creates a new Firestore client that uses the given project.
|
||||
func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (*Client, error) {
|
||||
if projectID == "" {
|
||||
return nil, errors.New("firestore: projectID was empty")
|
||||
}
|
||||
var o []option.ClientOption
|
||||
// If this environment variable is defined, configure the client to talk to the emulator.
|
||||
if addr := os.Getenv("FIRESTORE_EMULATOR_HOST"); addr != "" {
|
||||
conn, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithPerRPCCredentials(emulatorCreds{}))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("firestore: dialing address from env var FIRESTORE_EMULATOR_HOST: %s", err)
|
||||
}
|
||||
o = []option.ClientOption{option.WithGRPCConn(conn)}
|
||||
projectID, _ = detect.ProjectID(ctx, projectID, "", opts...)
|
||||
if projectID == "" {
|
||||
projectID = "dummy-emulator-firestore-project"
|
||||
}
|
||||
}
|
||||
o = append(o, opts...)
|
||||
|
||||
// Detect project ID.
|
||||
projectID, err := detect.ProjectID(ctx, projectID, "", o...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vc, err := vkit.NewClient(ctx, o...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vc.SetGoogleClientInfo("gccl", internal.Version)
|
||||
c := &Client{
|
||||
c: vc,
|
||||
projectID: projectID,
|
||||
databaseID: DefaultDatabaseID,
|
||||
readSettings: &readSettings{},
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// NewClientWithDatabase creates a new Firestore client that accesses the
|
||||
// specified database.
|
||||
func NewClientWithDatabase(ctx context.Context, projectID string, databaseID string, opts ...option.ClientOption) (*Client, error) {
|
||||
if databaseID == "" {
|
||||
return nil, fmt.Errorf("firestore: To create a client using the %s database, please use NewClient", DefaultDatabaseID)
|
||||
}
|
||||
|
||||
client, err := NewClient(ctx, projectID, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.databaseID = databaseID
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Close closes any resources held by the client.
|
||||
//
|
||||
// Close need not be called at program exit.
|
||||
func (c *Client) Close() error {
|
||||
return c.c.Close()
|
||||
}
|
||||
|
||||
func (c *Client) path() string {
|
||||
return fmt.Sprintf("projects/%s/databases/%s", c.projectID, c.databaseID)
|
||||
}
|
||||
|
||||
func withResourceHeader(ctx context.Context, resource string) context.Context {
|
||||
md, ok := metadata.FromOutgoingContext(ctx)
|
||||
if !ok {
|
||||
md = metadata.New(nil)
|
||||
}
|
||||
md = md.Copy()
|
||||
md[resourcePrefixHeader] = []string{resource}
|
||||
return metadata.NewOutgoingContext(ctx, md)
|
||||
}
|
||||
|
||||
func withRequestParamsHeader(ctx context.Context, requestParams string) context.Context {
|
||||
md, ok := metadata.FromOutgoingContext(ctx)
|
||||
if !ok {
|
||||
md = metadata.New(nil)
|
||||
}
|
||||
md = md.Copy()
|
||||
md[reqParamsHeader] = []string{requestParams}
|
||||
return metadata.NewOutgoingContext(ctx, md)
|
||||
}
|
||||
|
||||
// Collection creates a reference to a collection with the given path.
|
||||
// A path is a sequence of IDs separated by slashes.
|
||||
//
|
||||
// Collection returns nil if path contains an even number of IDs or any ID is empty.
|
||||
func (c *Client) Collection(path string) *CollectionRef {
|
||||
coll, _ := c.idsToRef(strings.Split(path, "/"), c.path())
|
||||
return coll
|
||||
}
|
||||
|
||||
// Doc creates a reference to a document with the given path.
|
||||
// A path is a sequence of IDs separated by slashes.
|
||||
//
|
||||
// Doc returns nil if path contains an odd number of IDs or any ID is empty.
|
||||
func (c *Client) Doc(path string) *DocumentRef {
|
||||
_, doc := c.idsToRef(strings.Split(path, "/"), c.path())
|
||||
return doc
|
||||
}
|
||||
|
||||
// CollectionGroup creates a reference to a group of collections that include
|
||||
// the given ID, regardless of parent document.
|
||||
//
|
||||
// For example, consider:
|
||||
// France/Cities/Paris = {population: 100}
|
||||
// Canada/Cities/Montreal = {population: 90}
|
||||
//
|
||||
// CollectionGroup can be used to query across all "Cities" regardless of
|
||||
// its parent "Countries". See ExampleCollectionGroup for a complete example.
|
||||
func (c *Client) CollectionGroup(collectionID string) *CollectionGroupRef {
|
||||
return newCollectionGroupRef(c, c.path(), collectionID)
|
||||
}
|
||||
|
||||
func (c *Client) idsToRef(IDs []string, dbPath string) (*CollectionRef, *DocumentRef) {
|
||||
if len(IDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
for _, id := range IDs {
|
||||
if id == "" {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
coll := newTopLevelCollRef(c, dbPath, IDs[0])
|
||||
i := 1
|
||||
for i < len(IDs) {
|
||||
doc := newDocRef(coll, IDs[i])
|
||||
i++
|
||||
if i == len(IDs) {
|
||||
return nil, doc
|
||||
}
|
||||
coll = newCollRefWithParent(c, doc, IDs[i])
|
||||
i++
|
||||
}
|
||||
return coll, nil
|
||||
}
|
||||
|
||||
// GetAll retrieves multiple documents with a single call. The
|
||||
// DocumentSnapshots are returned in the order of the given DocumentRefs.
|
||||
// The return value will always contain the same number of DocumentSnapshots
|
||||
// as the number of DocumentRefs in the input.
|
||||
//
|
||||
// If the same DocumentRef is specified multiple times in the input, the return
|
||||
// value will contain the same number of DocumentSnapshots referencing the same
|
||||
// document.
|
||||
//
|
||||
// If a document is not present, the corresponding DocumentSnapshot's Exists
|
||||
// method will return false.
|
||||
func (c *Client) GetAll(ctx context.Context, docRefs []*DocumentRef) (_ []*DocumentSnapshot, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.GetAll")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
return c.getAll(ctx, docRefs, nil, nil)
|
||||
}
|
||||
|
||||
func (c *Client) getAll(ctx context.Context, docRefs []*DocumentRef, tid []byte, rs *readSettings) (_ []*DocumentSnapshot, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.Client.BatchGetDocuments")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
var docNames []string
|
||||
docIndices := map[string][]int{} // doc name to positions in docRefs
|
||||
for i, dr := range docRefs {
|
||||
if err := dr.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docNames = append(docNames, dr.Path)
|
||||
docIndices[dr.Path] = append(docIndices[dr.Path], i)
|
||||
}
|
||||
req := &pb.BatchGetDocumentsRequest{
|
||||
Database: c.path(),
|
||||
Documents: docNames,
|
||||
}
|
||||
|
||||
// Note that transaction ID and other consistency selectors are mutually exclusive.
|
||||
// We respect the transaction first, any read options passed by the caller second,
|
||||
// and any read options stored in the client third.
|
||||
if rt, hasOpts := parseReadTime(c, rs); hasOpts {
|
||||
req.ConsistencySelector = &pb.BatchGetDocumentsRequest_ReadTime{ReadTime: rt}
|
||||
}
|
||||
|
||||
if tid != nil {
|
||||
req.ConsistencySelector = &pb.BatchGetDocumentsRequest_Transaction{Transaction: tid}
|
||||
}
|
||||
|
||||
batchGetDocsCtx := withResourceHeader(ctx, req.Database)
|
||||
batchGetDocsCtx = withRequestParamsHeader(batchGetDocsCtx, reqParamsHeaderVal(c.path()))
|
||||
streamClient, err := c.c.BatchGetDocuments(batchGetDocsCtx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and remember all results from the stream.
|
||||
var resps []*pb.BatchGetDocumentsResponse
|
||||
for {
|
||||
resp, err := streamClient.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resps = append(resps, resp)
|
||||
}
|
||||
|
||||
// Results may arrive out of order. Put each at the right indices.
|
||||
docs := make([]*DocumentSnapshot, len(docNames))
|
||||
for _, resp := range resps {
|
||||
var (
|
||||
indices []int
|
||||
doc *pb.Document
|
||||
err error
|
||||
)
|
||||
switch r := resp.Result.(type) {
|
||||
case *pb.BatchGetDocumentsResponse_Found:
|
||||
indices = docIndices[r.Found.Name]
|
||||
doc = r.Found
|
||||
case *pb.BatchGetDocumentsResponse_Missing:
|
||||
indices = docIndices[r.Missing]
|
||||
doc = nil
|
||||
default:
|
||||
return nil, errors.New("firestore: unknown BatchGetDocumentsResponse result type")
|
||||
}
|
||||
for _, index := range indices {
|
||||
if docs[index] != nil {
|
||||
return nil, fmt.Errorf("firestore: %q seen twice", docRefs[index].Path)
|
||||
}
|
||||
docs[index], err = newDocumentSnapshot(docRefs[index], doc, c, resp.ReadTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
// Collections returns an iterator over the top-level collections.
|
||||
func (c *Client) Collections(ctx context.Context) *CollectionIterator {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.Client.ListCollectionIds")
|
||||
defer func() { trace.EndSpan(ctx, nil) }()
|
||||
|
||||
it := &CollectionIterator{
|
||||
client: c,
|
||||
it: c.c.ListCollectionIds(
|
||||
withResourceHeader(ctx, c.path()),
|
||||
&pb.ListCollectionIdsRequest{Parent: c.path() + "/documents"}),
|
||||
}
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(
|
||||
it.fetch,
|
||||
func() int { return len(it.items) },
|
||||
func() interface{} { b := it.items; it.items = nil; return b })
|
||||
return it
|
||||
}
|
||||
|
||||
// Batch returns a WriteBatch.
|
||||
//
|
||||
// Deprecated: The WriteBatch API has been replaced with the transaction and
|
||||
// the bulk writer API. For atomic transaction operations, use `Transaction`.
|
||||
// For bulk read and write operations, use `BulkWriter`.
|
||||
func (c *Client) Batch() *WriteBatch {
|
||||
return &WriteBatch{c: c}
|
||||
}
|
||||
|
||||
// BulkWriter returns a BulkWriter instance.
|
||||
// The context passed to the BulkWriter remains stored through the lifecycle
|
||||
// of the object. This context allows callers to cancel BulkWriter operations.
|
||||
func (c *Client) BulkWriter(ctx context.Context) *BulkWriter {
|
||||
bw := newBulkWriter(ctx, c, c.path())
|
||||
return bw
|
||||
}
|
||||
|
||||
// WithReadOptions specifies constraints for accessing documents from the database,
|
||||
// e.g. at what time snapshot to read the documents.
|
||||
func (c *Client) WithReadOptions(opts ...ReadOption) *Client {
|
||||
for _, ro := range opts {
|
||||
ro.apply(c.readSettings)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// commit calls the Commit RPC outside of a transaction.
|
||||
func (c *Client) commit(ctx context.Context, ws []*pb.Write) (_ []*WriteResult, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.Client.commit")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
req := &pb.CommitRequest{
|
||||
Database: c.path(),
|
||||
Writes: ws,
|
||||
}
|
||||
res, err := c.c.Commit(withResourceHeader(ctx, req.Database), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(res.WriteResults) == 0 {
|
||||
return nil, errors.New("firestore: missing WriteResult")
|
||||
}
|
||||
var wrs []*WriteResult
|
||||
for _, pwr := range res.WriteResults {
|
||||
wr, err := writeResultFromProto(pwr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wrs = append(wrs, wr)
|
||||
}
|
||||
return wrs, nil
|
||||
}
|
||||
|
||||
func (c *Client) commit1(ctx context.Context, ws []*pb.Write) (*WriteResult, error) {
|
||||
wrs, err := c.commit(ctx, ws)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wrs[0], nil
|
||||
}
|
||||
|
||||
// A WriteResult is returned by methods that write documents.
|
||||
type WriteResult struct {
|
||||
// The time at which the document was updated, or created if it did not
|
||||
// previously exist. Writes that do not actually change the document do
|
||||
// not change the update time.
|
||||
UpdateTime time.Time
|
||||
}
|
||||
|
||||
func writeResultFromProto(wr *pb.WriteResult) (*WriteResult, error) {
|
||||
// TODO(jba): Follow up if Delete is supposed to return a nil timestamp.
|
||||
var t time.Time
|
||||
if err := wr.GetUpdateTime().CheckValid(); err == nil {
|
||||
t = wr.GetUpdateTime().AsTime()
|
||||
}
|
||||
return &WriteResult{UpdateTime: t}, nil
|
||||
}
|
||||
|
||||
func sleep(ctx context.Context, dur time.Duration) error {
|
||||
switch err := gax.Sleep(ctx, dur); err {
|
||||
case context.Canceled:
|
||||
return status.Error(codes.Canceled, "context canceled")
|
||||
case context.DeadlineExceeded:
|
||||
return status.Error(codes.DeadlineExceeded, "context deadline exceeded")
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// emulatorCreds is an instance of grpc.PerRPCCredentials that will configure a
|
||||
// client to act as an admin for the Firestore emulator. It always hardcodes
|
||||
// the "authorization" metadata field to contain "Bearer owner", which the
|
||||
// Firestore emulator accepts as valid admin credentials.
|
||||
type emulatorCreds struct{}
|
||||
|
||||
func (ec emulatorCreds) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||
return map[string]string{"authorization": "Bearer owner"}, nil
|
||||
}
|
||||
func (ec emulatorCreds) RequireTransportSecurity() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ReadTime specifies a time-specific snapshot of the database to read.
|
||||
func ReadTime(t time.Time) ReadOption {
|
||||
return readTime(t)
|
||||
}
|
||||
|
||||
type readTime time.Time
|
||||
|
||||
func (rt readTime) apply(rs *readSettings) {
|
||||
rs.readTime = time.Time(rt)
|
||||
}
|
||||
|
||||
// ReadOption interface allows for abstraction of computing read time settings.
|
||||
type ReadOption interface {
|
||||
apply(*readSettings)
|
||||
}
|
||||
|
||||
// readSettings contains the ReadOptions for a read operation
|
||||
type readSettings struct {
|
||||
readTime time.Time
|
||||
}
|
||||
|
||||
// parseReadTime ensures that fallback order of read options is respected.
|
||||
func parseReadTime(c *Client, rs *readSettings) (*timestamppb.Timestamp, bool) {
|
||||
if rs != nil && !rs.readTime.IsZero() {
|
||||
return ×tamppb.Timestamp{Seconds: int64(rs.readTime.Unix())}, true
|
||||
}
|
||||
if c.readSettings != nil && !c.readSettings.readTime.IsZero() {
|
||||
return ×tamppb.Timestamp{Seconds: int64(c.readSettings.readTime.Unix())}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
// Copyright 2019 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
// A CollectionGroupRef is a reference to a group of collections sharing the
|
||||
// same ID.
|
||||
type CollectionGroupRef struct {
|
||||
c *Client
|
||||
|
||||
// Use the methods of Query on a CollectionGroupRef to create and run queries.
|
||||
Query
|
||||
}
|
||||
|
||||
func newCollectionGroupRef(c *Client, dbPath, collectionID string) *CollectionGroupRef {
|
||||
return &CollectionGroupRef{
|
||||
c: c,
|
||||
|
||||
Query: Query{
|
||||
c: c,
|
||||
collectionID: collectionID,
|
||||
path: dbPath,
|
||||
parentPath: dbPath + "/documents",
|
||||
allDescendants: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetPartitionedQueries returns a slice of Query objects, each containing a
|
||||
// partition of a collection group. partitionCount must be a positive value and
|
||||
// the number of returned partitions may be less than the requested number if
|
||||
// providing the desired number would result in partitions with very few documents.
|
||||
//
|
||||
// If a Collection Group Query would return a large number of documents, this
|
||||
// can help to subdivide the query to smaller working units that can be distributed.
|
||||
//
|
||||
// If the goal is to run the queries across processes or workers, it may be useful to use
|
||||
// `Query.Serialize` and `Query.Deserialize` to serialize the query.
|
||||
func (cgr CollectionGroupRef) GetPartitionedQueries(ctx context.Context, partitionCount int) ([]Query, error) {
|
||||
qp, err := cgr.getPartitions(ctx, partitionCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries := make([]Query, len(qp))
|
||||
for i, part := range qp {
|
||||
queries[i] = part.toQuery()
|
||||
}
|
||||
return queries, nil
|
||||
}
|
||||
|
||||
// getPartitions returns a slice of queryPartition objects, describing a start
|
||||
// and end range to query a subsection of the collection group. partitionCount
|
||||
// must be a positive value and the number of returned partitions may be less
|
||||
// than the requested number if providing the desired number would result in
|
||||
// partitions with very few documents.
|
||||
func (cgr CollectionGroupRef) getPartitions(ctx context.Context, partitionCount int) ([]queryPartition, error) {
|
||||
orderedQuery := cgr.query().OrderBy(DocumentID, Asc)
|
||||
|
||||
if partitionCount <= 0 {
|
||||
return nil, errors.New("a positive partitionCount must be provided")
|
||||
} else if partitionCount == 1 {
|
||||
return []queryPartition{{CollectionGroupQuery: orderedQuery}}, nil
|
||||
}
|
||||
|
||||
db := cgr.c.path()
|
||||
ctx = withResourceHeader(ctx, db)
|
||||
|
||||
// CollectionGroup Queries need to be ordered by __name__ ASC.
|
||||
query, err := orderedQuery.toProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
structuredQuery := &firestorepb.PartitionQueryRequest_StructuredQuery{
|
||||
StructuredQuery: query,
|
||||
}
|
||||
|
||||
// Uses default PageSize
|
||||
pbr := &firestorepb.PartitionQueryRequest{
|
||||
Parent: db + "/documents",
|
||||
PartitionCount: int64(partitionCount),
|
||||
QueryType: structuredQuery,
|
||||
}
|
||||
cursorReferences := make([]*firestorepb.Value, 0, partitionCount)
|
||||
iter := cgr.c.c.PartitionQuery(ctx, pbr)
|
||||
for {
|
||||
cursor, err := iter.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetPartitions: %w", err)
|
||||
}
|
||||
cursorReferences = append(cursorReferences, cursor.GetValues()...)
|
||||
}
|
||||
|
||||
// From Proto documentation:
|
||||
// To obtain a complete result set ordered with respect to the results of the
|
||||
// query supplied to PartitionQuery, the results sets should be merged:
|
||||
// cursor A, cursor B, cursor M, cursor Q, cursor U, cursor W
|
||||
// Once we have exhausted the pages, the cursor values need to be sorted in
|
||||
// lexicographical order by segment (areas between '/').
|
||||
sort.Sort(byFirestoreValue(cursorReferences))
|
||||
|
||||
queryPartitions := make([]queryPartition, 0, len(cursorReferences))
|
||||
previousCursor := ""
|
||||
|
||||
for _, cursor := range cursorReferences {
|
||||
cursorRef := cursor.GetReferenceValue()
|
||||
|
||||
// remove the root path from the reference, as queries take cursors
|
||||
// relative to a collection
|
||||
cursorRef = cursorRef[len(orderedQuery.path)+1:]
|
||||
|
||||
qp := queryPartition{
|
||||
CollectionGroupQuery: orderedQuery,
|
||||
StartAt: previousCursor,
|
||||
EndBefore: cursorRef,
|
||||
}
|
||||
queryPartitions = append(queryPartitions, qp)
|
||||
previousCursor = cursorRef
|
||||
}
|
||||
|
||||
// In the case there were no partitions, we still add a single partition to
|
||||
// the result, that covers the complete range.
|
||||
lastPart := queryPartition{CollectionGroupQuery: orderedQuery}
|
||||
if len(cursorReferences) > 0 {
|
||||
cursorRef := cursorReferences[len(cursorReferences)-1].GetReferenceValue()
|
||||
lastPart.StartAt = cursorRef[len(orderedQuery.path)+1:]
|
||||
}
|
||||
queryPartitions = append(queryPartitions, lastPart)
|
||||
|
||||
return queryPartitions, nil
|
||||
}
|
||||
|
||||
// queryPartition provides a Collection Group Reference and start and end split
|
||||
// points allowing for a section of a collection group to be queried. This is
|
||||
// used by GetPartitions which, given a CollectionGroupReference returns smaller
|
||||
// sub-queries or partitions
|
||||
type queryPartition struct {
|
||||
// CollectionGroupQuery is an ordered query on a CollectionGroupReference.
|
||||
// This query must be ordered Asc on __name__.
|
||||
// Example: client.CollectionGroup("collectionID").query().OrderBy(DocumentID, Asc)
|
||||
CollectionGroupQuery Query
|
||||
|
||||
// StartAt is a document reference value, relative to the collection, not
|
||||
// a complete parent path.
|
||||
// Example: "documents/collectionName/documentName"
|
||||
StartAt string
|
||||
|
||||
// EndBefore is a document reference value, relative to the collection, not
|
||||
// a complete parent path.
|
||||
// Example: "documents/collectionName/documentName"
|
||||
EndBefore string
|
||||
}
|
||||
|
||||
// toQuery converts a queryPartition object to a Query object
|
||||
func (qp queryPartition) toQuery() Query {
|
||||
q := *qp.CollectionGroupQuery.query()
|
||||
|
||||
// Remove the leading path before calling StartAt, EndBefore
|
||||
if qp.StartAt != "" {
|
||||
q = q.StartAt(qp.StartAt)
|
||||
}
|
||||
if qp.EndBefore != "" {
|
||||
q = q.EndBefore(qp.EndBefore)
|
||||
}
|
||||
return q
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
// Copyright 2017 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// A CollectionRef is a reference to Firestore collection.
|
||||
type CollectionRef struct {
|
||||
c *Client
|
||||
|
||||
// The full resource path of the collection's parent. Typically Parent.Path,
|
||||
// or c.path if Parent is nil. May be different if this CollectionRef was
|
||||
// created from a stored reference to a different project/DB. Always
|
||||
// includes /documents - that is, the parent is minimally considered to be
|
||||
// "<db>/documents".
|
||||
//
|
||||
// For example, "projects/P/databases/D/documents/coll-1/doc-1".
|
||||
parentPath string
|
||||
|
||||
// The shorter resource path of the collection. A collection "coll-2" in
|
||||
// document "doc-1" in collection "coll-1" would be: "coll-1/doc-1/coll-2".
|
||||
selfPath string
|
||||
|
||||
// Parent is the document of which this collection is a part. It is
|
||||
// nil for top-level collections.
|
||||
Parent *DocumentRef
|
||||
|
||||
// The full resource path of the collection: "projects/P/databases/D/documents..."
|
||||
Path string
|
||||
|
||||
// ID is the collection identifier.
|
||||
ID string
|
||||
|
||||
// Use the methods of Query on a CollectionRef to create and run queries.
|
||||
Query
|
||||
|
||||
// readSettings specifies constraints for reading documents in the collection
|
||||
// e.g. read time
|
||||
readSettings *readSettings
|
||||
}
|
||||
|
||||
func newTopLevelCollRef(c *Client, dbPath, id string) *CollectionRef {
|
||||
return &CollectionRef{
|
||||
c: c,
|
||||
ID: id,
|
||||
parentPath: dbPath + "/documents",
|
||||
selfPath: id,
|
||||
Path: dbPath + "/documents/" + id,
|
||||
Query: Query{
|
||||
c: c,
|
||||
collectionID: id,
|
||||
path: dbPath + "/documents/" + id,
|
||||
parentPath: dbPath + "/documents",
|
||||
},
|
||||
readSettings: &readSettings{},
|
||||
}
|
||||
}
|
||||
|
||||
func newCollRefWithParent(c *Client, parent *DocumentRef, id string) *CollectionRef {
|
||||
selfPath := parent.shortPath + "/" + id
|
||||
return &CollectionRef{
|
||||
c: c,
|
||||
Parent: parent,
|
||||
ID: id,
|
||||
parentPath: parent.Path,
|
||||
selfPath: selfPath,
|
||||
Path: parent.Path + "/" + id,
|
||||
Query: Query{
|
||||
c: c,
|
||||
collectionID: id,
|
||||
path: parent.Path + "/" + id,
|
||||
parentPath: parent.Path,
|
||||
},
|
||||
readSettings: &readSettings{},
|
||||
}
|
||||
}
|
||||
|
||||
// Doc returns a DocumentRef that refers to the document in the collection with the
|
||||
// given identifier.
|
||||
func (c *CollectionRef) Doc(id string) *DocumentRef {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return newDocRef(c, id)
|
||||
}
|
||||
|
||||
// NewDoc returns a DocumentRef with a uniquely generated ID.
|
||||
//
|
||||
// NewDoc will panic if crypto/rand cannot generate enough bytes to make a new
|
||||
// doc ID.
|
||||
func (c *CollectionRef) NewDoc() *DocumentRef {
|
||||
return c.Doc(uniqueID())
|
||||
}
|
||||
|
||||
// Add generates a DocumentRef with a unique ID. It then creates the document
|
||||
// with the given data, which can be a map[string]interface{}, a struct or a
|
||||
// pointer to a struct.
|
||||
//
|
||||
// Add returns an error in the unlikely event that a document with the same ID
|
||||
// already exists.
|
||||
func (c *CollectionRef) Add(ctx context.Context, data interface{}) (*DocumentRef, *WriteResult, error) {
|
||||
d := c.NewDoc()
|
||||
wr, err := d.Create(ctx, data)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return d, wr, nil
|
||||
}
|
||||
|
||||
// DocumentRefs returns references to all the documents in the collection, including
|
||||
// missing documents. A missing document is a document that does not exist but has
|
||||
// sub-documents.
|
||||
func (c *CollectionRef) DocumentRefs(ctx context.Context) *DocumentRefIterator {
|
||||
return newDocumentRefIterator(ctx, c, nil, c.readSettings)
|
||||
}
|
||||
|
||||
const alphanum = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
func uniqueID() string {
|
||||
b := make([]byte, 20)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(fmt.Sprintf("firestore: crypto/rand.Read error: %v", err))
|
||||
}
|
||||
for i, byt := range b {
|
||||
b[i] = alphanum[int(byt)%len(alphanum)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// WithReadOptions specifies constraints for accessing documents from the database,
|
||||
// e.g. at what time snapshot to read the documents.
|
||||
func (c *CollectionRef) WithReadOptions(opts ...ReadOption) *CollectionRef {
|
||||
for _, ro := range opts {
|
||||
ro.apply(c.readSettings)
|
||||
}
|
||||
return c
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
// Copyright 2017 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// DO NOT EDIT doc.go. Modify internal/doc.template, then run make -C internal.
|
||||
|
||||
/*
|
||||
Package firestore provides a client for reading and writing to a Cloud Firestore
|
||||
database.
|
||||
|
||||
See https://cloud.google.com/firestore/docs for an introduction
|
||||
to Cloud Firestore and additional help on using the Firestore API.
|
||||
|
||||
See https://godoc.org/cloud.google.com/go for authentication, timeouts,
|
||||
connection pooling and similar aspects of this package.
|
||||
|
||||
Note: you can't use both Cloud Firestore and Cloud Datastore in the same
|
||||
project.
|
||||
|
||||
# Creating a Client
|
||||
|
||||
To start working with this package, create a client with a project ID:
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "projectID")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
# CollectionRefs and DocumentRefs
|
||||
|
||||
In Firestore, documents are sets of key-value pairs, and collections are groups of
|
||||
documents. A Firestore database consists of a hierarchy of alternating collections
|
||||
and documents, referred to by slash-separated paths like
|
||||
"States/California/Cities/SanFrancisco".
|
||||
|
||||
This client is built around references to collections and documents. CollectionRefs
|
||||
and DocumentRefs are lightweight values that refer to the corresponding database
|
||||
entities. Creating a ref does not involve any network traffic.
|
||||
|
||||
states := client.Collection("States")
|
||||
ny := states.Doc("NewYork")
|
||||
// Or, in a single call:
|
||||
ny = client.Doc("States/NewYork")
|
||||
|
||||
# Reading
|
||||
|
||||
Use DocumentRef.Get to read a document. The result is a DocumentSnapshot.
|
||||
Call its Data method to obtain the entire document contents as a map.
|
||||
|
||||
docsnap, err := ny.Get(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
dataMap := docsnap.Data()
|
||||
fmt.Println(dataMap)
|
||||
|
||||
You can also obtain a single field with DataAt, or extract the data into a struct
|
||||
with DataTo. With the type definition
|
||||
|
||||
type State struct {
|
||||
Capital string `firestore:"capital"`
|
||||
Population float64 `firestore:"pop"` // in millions
|
||||
}
|
||||
|
||||
we can extract the document's data into a value of type State:
|
||||
|
||||
var nyData State
|
||||
if err := docsnap.DataTo(&nyData); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
Note that this client supports struct tags beginning with "firestore:" that work like
|
||||
the tags of the encoding/json package, letting you rename fields, ignore them, or
|
||||
omit their values when empty.
|
||||
|
||||
To retrieve multiple documents from their references in a single call, use
|
||||
Client.GetAll.
|
||||
|
||||
docsnaps, err := client.GetAll(ctx, []*firestore.DocumentRef{
|
||||
states.Doc("Wisconsin"), states.Doc("Ohio"),
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
for _, ds := range docsnaps {
|
||||
_ = ds // TODO: Use ds.
|
||||
}
|
||||
|
||||
# Writing
|
||||
|
||||
For writing individual documents, use the methods on DocumentReference.
|
||||
Create creates a new document.
|
||||
|
||||
wr, err := ny.Create(ctx, State{
|
||||
Capital: "Albany",
|
||||
Population: 19.8,
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(wr)
|
||||
|
||||
The first return value is a WriteResult, which contains the time
|
||||
at which the document was updated.
|
||||
|
||||
Create fails if the document exists. Another method, Set, either replaces an existing
|
||||
document or creates a new one.
|
||||
|
||||
ca := states.Doc("California")
|
||||
_, err = ca.Set(ctx, State{
|
||||
Capital: "Sacramento",
|
||||
Population: 39.14,
|
||||
})
|
||||
|
||||
To update some fields of an existing document, use Update. It takes a list of
|
||||
paths to update and their corresponding values.
|
||||
|
||||
_, err = ca.Update(ctx, []firestore.Update{{Path: "capital", Value: "Sacramento"}})
|
||||
|
||||
Use DocumentRef.Delete to delete a document.
|
||||
|
||||
_, err = ny.Delete(ctx)
|
||||
|
||||
# Preconditions
|
||||
|
||||
You can condition Deletes or Updates on when a document was last changed. Specify
|
||||
these preconditions as an option to a Delete or Update method. The check and the
|
||||
write happen atomically with a single RPC.
|
||||
|
||||
docsnap, err = ca.Get(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
_, err = ca.Update(ctx,
|
||||
[]firestore.Update{{Path: "capital", Value: "Sacramento"}},
|
||||
firestore.LastUpdateTime(docsnap.UpdateTime))
|
||||
|
||||
Here we update a doc only if it hasn't changed since we read it.
|
||||
You could also do this with a transaction.
|
||||
|
||||
To perform multiple writes at once, use a WriteBatch. Its methods chain
|
||||
for convenience.
|
||||
|
||||
WriteBatch.Commit sends the collected writes to the server, where they happen
|
||||
atomically.
|
||||
|
||||
writeResults, err := client.Batch().
|
||||
Create(ny, State{Capital: "Albany"}).
|
||||
Update(ca, []firestore.Update{{Path: "capital", Value: "Sacramento"}}).
|
||||
Delete(client.Doc("States/WestDakota")).
|
||||
Commit(ctx)
|
||||
|
||||
# Queries
|
||||
|
||||
You can use SQL to select documents from a collection. Begin with the collection, and
|
||||
build up a query using Select, Where and other methods of Query.
|
||||
|
||||
q := states.Where("pop", ">", 10).OrderBy("pop", firestore.Desc)
|
||||
|
||||
Supported operators include '<', '<=', '>', '>=', '==', 'in', 'array-contains', and
|
||||
'array-contains-any'.
|
||||
|
||||
Call the Query's Documents method to get an iterator, and use it like
|
||||
the other Google Cloud Client iterators.
|
||||
|
||||
iter := q.Documents(ctx)
|
||||
defer iter.Stop()
|
||||
for {
|
||||
doc, err := iter.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(doc.Data())
|
||||
}
|
||||
|
||||
To get all the documents in a collection, you can use the collection itself
|
||||
as a query.
|
||||
|
||||
iter = client.Collection("States").Documents(ctx)
|
||||
|
||||
Firestore supports similarity search over embedding vectors. See [Query.FindNearest]
|
||||
for details.
|
||||
|
||||
# Collection Group Partition Queries
|
||||
|
||||
You can partition the documents of a Collection Group allowing for smaller subqueries.
|
||||
|
||||
collectionGroup = client.CollectionGroup("States")
|
||||
partitions, err = collectionGroup.GetPartitionedQueries(ctx, 20)
|
||||
|
||||
You can also Serialize/Deserialize queries making it possible to run/stream the
|
||||
queries elsewhere; another process or machine for instance.
|
||||
|
||||
queryProtos := make([][]byte, 0)
|
||||
for _, query := range partitions {
|
||||
protoBytes, err := query.Serialize()
|
||||
// handle err
|
||||
queryProtos = append(queryProtos, protoBytes)
|
||||
...
|
||||
}
|
||||
|
||||
for _, protoBytes := range queryProtos {
|
||||
query, err := client.CollectionGroup("").Deserialize(protoBytes)
|
||||
...
|
||||
}
|
||||
|
||||
# Transactions
|
||||
|
||||
Use a transaction to execute reads and writes atomically. All reads must happen
|
||||
before any writes. Transaction creation, commit, rollback and retry are handled for
|
||||
you by the Client.RunTransaction method; just provide a function and use the
|
||||
read and write methods of the Transaction passed to it.
|
||||
|
||||
ny := client.Doc("States/NewYork")
|
||||
err := client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
|
||||
doc, err := tx.Get(ny) // tx.Get, NOT ny.Get!
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pop, err := doc.DataAt("pop")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Update(ny, []firestore.Update{{Path: "pop", Value: pop.(float64) + 0.2}})
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
# Google Cloud Firestore Emulator
|
||||
|
||||
This package supports the Cloud Firestore emulator, which is useful for testing and
|
||||
development. Environment variables are used to indicate that Firestore traffic should be
|
||||
directed to the emulator instead of the production Firestore service.
|
||||
|
||||
To install and run the emulator and its environment variables, see the documentation
|
||||
at https://cloud.google.com/sdk/gcloud/reference/beta/emulators/firestore/. Once the
|
||||
emulator is running, set FIRESTORE_EMULATOR_HOST to the API endpoint.
|
||||
|
||||
// Set FIRESTORE_EMULATOR_HOST environment variable.
|
||||
err := os.Setenv("FIRESTORE_EMULATOR_HOST", "localhost:9000")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// Create client as usual.
|
||||
client, err := firestore.NewClient(ctx, "my-project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
*/
|
||||
package firestore
|
||||
+875
@@ -0,0 +1,875 @@
|
||||
// Copyright 2017 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
"unicode/utf8"
|
||||
|
||||
vkit "cloud.google.com/go/firestore/apiv1"
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"cloud.google.com/go/internal/trace"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var (
|
||||
errNilDocRef = errors.New("firestore: nil DocumentRef")
|
||||
errInvalidUtf8DocRef = errors.New("firestore: ID in DocumentRef contains invalid UTF-8 characters")
|
||||
)
|
||||
|
||||
// A DocumentRef is a reference to a Firestore document.
|
||||
type DocumentRef struct {
|
||||
// The CollectionRef that this document is a part of. Never nil.
|
||||
Parent *CollectionRef
|
||||
|
||||
// The full resource path of the document. A document "doc-1" in collection
|
||||
// "coll-1" would be: "projects/P/databases/D/documents/coll-1/doc-1".
|
||||
Path string
|
||||
|
||||
// The shorter resource path of the document. A document "doc-1" in
|
||||
// collection "coll-1" would be: "coll-1/doc-1".
|
||||
shortPath string
|
||||
|
||||
// The ID of the document: the last component of the resource path.
|
||||
ID string
|
||||
|
||||
// The options (only read time currently supported) for reading this document
|
||||
readSettings *readSettings
|
||||
}
|
||||
|
||||
func newDocRef(parent *CollectionRef, id string) *DocumentRef {
|
||||
return &DocumentRef{
|
||||
Parent: parent,
|
||||
ID: id,
|
||||
Path: parent.Path + "/" + id,
|
||||
shortPath: parent.selfPath + "/" + id,
|
||||
readSettings: &readSettings{},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DocumentRef) isValid() error {
|
||||
if d == nil {
|
||||
return errNilDocRef
|
||||
}
|
||||
if !utf8.ValidString(d.ID) {
|
||||
return errInvalidUtf8DocRef
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collection returns a reference to sub-collection of this document.
|
||||
func (d *DocumentRef) Collection(id string) *CollectionRef {
|
||||
return newCollRefWithParent(d.Parent.c, d, id)
|
||||
}
|
||||
|
||||
// Get retrieves the document. If the document does not exist, Get return a NotFound error, which
|
||||
// can be checked with
|
||||
//
|
||||
// status.Code(err) == codes.NotFound
|
||||
//
|
||||
// In that case, Get returns a non-nil DocumentSnapshot whose Exists method return false and whose
|
||||
// ReadTime is the time of the failed read operation.
|
||||
func (d *DocumentRef) Get(ctx context.Context) (_ *DocumentSnapshot, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Get")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
docsnaps, err := d.Parent.c.getAll(ctx, []*DocumentRef{d}, nil, d.readSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ds := docsnaps[0]
|
||||
if !ds.Exists() {
|
||||
return ds, status.Errorf(codes.NotFound, "%q not found", d.Path)
|
||||
}
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// Create creates the document with the given data.
|
||||
// It returns an error if a document with the same ID already exists.
|
||||
//
|
||||
// The data argument can be a map with string keys, a struct, or a pointer to a
|
||||
// struct. The map keys or exported struct fields become the fields of the firestore
|
||||
// document.
|
||||
// The values of data are converted to Firestore values as follows:
|
||||
//
|
||||
// - bool converts to Bool.
|
||||
// - string converts to String.
|
||||
// - int, int8, int16, int32 and int64 convert to Integer.
|
||||
// - uint8, uint16 and uint32 convert to Integer. uint, uint64 and uintptr are disallowed,
|
||||
// because they may be able to represent values that cannot be represented in an int64,
|
||||
// which is the underlying type of a Integer.
|
||||
// - float32 and float64 convert to Double.
|
||||
// - []byte converts to Bytes.
|
||||
// - time.Time and *ts.Timestamp convert to Timestamp. ts is the package
|
||||
// "google.golang.org/protobuf/types/known/timestamppb".
|
||||
// - *latlng.LatLng converts to GeoPoint. latlng is the package
|
||||
// "google.golang.org/genproto/googleapis/type/latlng". You should always use
|
||||
// a pointer to a LatLng.
|
||||
// - Slices convert to Array.
|
||||
// - *firestore.DocumentRef converts to Reference.
|
||||
// - Maps and structs convert to Map.
|
||||
// - nils of any type convert to Null.
|
||||
//
|
||||
// Pointers and interface{} are also permitted, and their elements processed
|
||||
// recursively.
|
||||
//
|
||||
// Struct fields can have tags like those used by the encoding/json package. Tags
|
||||
// begin with "firestore:" and are followed by "-", meaning "ignore this field," or
|
||||
// an alternative name for the field. Following the name, these comma-separated
|
||||
// options may be provided:
|
||||
//
|
||||
// - omitempty: Do not encode this field if it is empty. A value is empty
|
||||
// if it is a zero value, or an array, slice or map of length zero.
|
||||
// - serverTimestamp: The field must be of type time.Time. serverTimestamp
|
||||
// is a sentinel token that tells Firestore to substitute the server time
|
||||
// into that field. When writing, if the field has the zero value, the
|
||||
// server will populate the stored document with the time that the request
|
||||
// is processed. However, if the field value is non-zero it won't be saved.
|
||||
func (d *DocumentRef) Create(ctx context.Context, data interface{}) (_ *WriteResult, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Create")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
ws, err := d.newCreateWrites(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Parent.c.commit1(ctx, ws)
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newCreateWrites(data interface{}) ([]*pb.Write, error) {
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc, transforms, err := toProtoDocument(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc.Name = d.Path
|
||||
pc, err := exists(false).preconditionProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.newUpdateWithTransform(doc, nil, pc, transforms, false), nil
|
||||
}
|
||||
|
||||
// Set creates or overwrites the document with the given data. See DocumentRef.Create
|
||||
// for the acceptable values of data. Without options, Set overwrites the document
|
||||
// completely. Specify one of the Merge options to preserve an existing document's
|
||||
// fields. To delete some fields, use a Merge option with firestore.Delete as the
|
||||
// field value.
|
||||
func (d *DocumentRef) Set(ctx context.Context, data interface{}, opts ...SetOption) (_ *WriteResult, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Set")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
ws, err := d.newSetWrites(data, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Parent.c.commit1(ctx, ws)
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newSetWrites(data interface{}, opts []SetOption) ([]*pb.Write, error) {
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data == nil {
|
||||
return nil, errors.New("firestore: nil document contents")
|
||||
}
|
||||
if len(opts) == 0 { // Set without merge
|
||||
doc, serverTimestampPaths, err := toProtoDocument(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc.Name = d.Path
|
||||
return d.newUpdateWithTransform(doc, nil, nil, serverTimestampPaths, true), nil
|
||||
}
|
||||
// Set with merge.
|
||||
// This is just like Update, except for the existence precondition.
|
||||
// So we turn data into a list of (FieldPath, interface{}) pairs (fpv's), as we do
|
||||
// for Update.
|
||||
fieldPaths, allPaths, err := processSetOptions(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var fpvs []fpv
|
||||
v := reflect.ValueOf(data)
|
||||
if allPaths {
|
||||
// Set with MergeAll. Collect all the leaves of the map.
|
||||
if v.Kind() != reflect.Map {
|
||||
return nil, errors.New("firestore: MergeAll can only be specified with map data")
|
||||
}
|
||||
if v.Len() == 0 {
|
||||
// Special case: MergeAll with an empty map.
|
||||
return d.newUpdateWithTransform(&pb.Document{Name: d.Path}, []FieldPath{}, nil, nil, true), nil
|
||||
}
|
||||
fpvsFromData(v, nil, &fpvs)
|
||||
} else {
|
||||
// Set with merge paths. Collect only the values at the given paths.
|
||||
for _, fp := range fieldPaths {
|
||||
val, err := getAtPath(v, fp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fpvs = append(fpvs, fpv{fp, val})
|
||||
}
|
||||
}
|
||||
return d.fpvsToWrites(fpvs, nil)
|
||||
}
|
||||
|
||||
// fpvsFromData converts v into a list of (FieldPath, value) pairs.
|
||||
func fpvsFromData(v reflect.Value, prefix FieldPath, fpvs *[]fpv) {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
for _, k := range v.MapKeys() {
|
||||
fpvsFromData(v.MapIndex(k), prefix.with(k.String()), fpvs)
|
||||
}
|
||||
case reflect.Interface:
|
||||
fpvsFromData(v.Elem(), prefix, fpvs)
|
||||
|
||||
default:
|
||||
var val interface{}
|
||||
if v.IsValid() {
|
||||
val = v.Interface()
|
||||
}
|
||||
*fpvs = append(*fpvs, fpv{prefix, val})
|
||||
}
|
||||
}
|
||||
|
||||
// Delete deletes the document. If the document doesn't exist, it does nothing
|
||||
// and returns no error.
|
||||
func (d *DocumentRef) Delete(ctx context.Context, preconds ...Precondition) (_ *WriteResult, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Delete")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
ws, err := d.newDeleteWrites(preconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Parent.c.commit1(ctx, ws)
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newDeleteWrites(preconds []Precondition) ([]*pb.Write, error) {
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pc, err := processPreconditionsForDelete(preconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*pb.Write{{
|
||||
Operation: &pb.Write_Delete{Delete: d.Path},
|
||||
CurrentDocument: pc,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newUpdatePathWrites(updates []Update, preconds []Precondition) ([]*pb.Write, error) {
|
||||
if len(updates) == 0 {
|
||||
return nil, errors.New("firestore: no paths to update")
|
||||
}
|
||||
var fpvs []fpv
|
||||
for _, u := range updates {
|
||||
v, err := u.process()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fpvs = append(fpvs, v)
|
||||
}
|
||||
pc, err := processPreconditionsForUpdate(preconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.fpvsToWrites(fpvs, pc)
|
||||
}
|
||||
|
||||
func (d *DocumentRef) fpvsToWrites(fpvs []fpv, pc *pb.Precondition) ([]*pb.Write, error) {
|
||||
// Make sure there are no duplications or prefixes among the field paths.
|
||||
var fps []FieldPath
|
||||
for _, fpv := range fpvs {
|
||||
fps = append(fps, fpv.fieldPath)
|
||||
}
|
||||
if err := checkNoDupOrPrefix(fps); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process each fpv.
|
||||
var updatePaths []FieldPath
|
||||
var transforms []*pb.DocumentTransform_FieldTransform
|
||||
doc := &pb.Document{
|
||||
Name: d.Path,
|
||||
Fields: map[string]*pb.Value{},
|
||||
}
|
||||
for _, fpv := range fpvs {
|
||||
switch fpv.value.(type) {
|
||||
case arrayUnion:
|
||||
au := fpv.value.(arrayUnion)
|
||||
t, err := arrayUnionTransform(au, fpv.fieldPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, t)
|
||||
case arrayRemove:
|
||||
ar := fpv.value.(arrayRemove)
|
||||
t, err := arrayRemoveTransform(ar, fpv.fieldPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, t)
|
||||
case transform:
|
||||
t, err := fieldTransform(fpv.value.(transform), fpv.fieldPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, t)
|
||||
|
||||
default:
|
||||
switch fpv.value {
|
||||
case Delete:
|
||||
// Send the field path without a corresponding value.
|
||||
updatePaths = append(updatePaths, fpv.fieldPath)
|
||||
|
||||
case ServerTimestamp:
|
||||
// Use the path in a transform operation.
|
||||
transforms = append(transforms, serverTimestamp(fpv.fieldPath.toServiceFieldPath()))
|
||||
|
||||
default:
|
||||
updatePaths = append(updatePaths, fpv.fieldPath)
|
||||
// Convert the value to a proto and put it into the document.
|
||||
v := reflect.ValueOf(fpv.value)
|
||||
|
||||
pv, _, err := toProtoValue(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setAtPath(doc.Fields, fpv.fieldPath, pv)
|
||||
// Also accumulate any transforms within the value.
|
||||
ts, err := extractTransforms(v, fpv.fieldPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, ts...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return d.newUpdateWithTransform(doc, updatePaths, pc, transforms, false), nil
|
||||
}
|
||||
|
||||
// newUpdateWithTransform constructs operations for a commit. Most generally, it
|
||||
// returns an update operation with update transforms.
|
||||
//
|
||||
// If there are no serverTimestampPaths, the transform is omitted.
|
||||
//
|
||||
// If doc.Fields is empty, there are no updatePaths, and there is no precondition,
|
||||
// the update is omitted, unless updateOnEmpty is true.
|
||||
func (d *DocumentRef) newUpdateWithTransform(doc *pb.Document, updatePaths []FieldPath, pc *pb.Precondition, transforms []*pb.DocumentTransform_FieldTransform, updateOnEmpty bool) []*pb.Write {
|
||||
var ws []*pb.Write
|
||||
var w *pb.Write
|
||||
initializedW := &pb.Write{
|
||||
Operation: &pb.Write_Update{
|
||||
Update: doc,
|
||||
},
|
||||
CurrentDocument: pc,
|
||||
// If the mask is not set for an `update` and the document exists, any
|
||||
// existing data will be overwritten.
|
||||
UpdateMask: &pb.DocumentMask{},
|
||||
}
|
||||
if updateOnEmpty || len(doc.Fields) > 0 ||
|
||||
len(updatePaths) > 0 || (pc != nil && len(transforms) == 0) {
|
||||
w = initializedW
|
||||
var mask *pb.DocumentMask
|
||||
if updatePaths != nil {
|
||||
sfps := toServiceFieldPaths(updatePaths)
|
||||
sort.Strings(sfps) // TODO(jba): make tests pass without this
|
||||
mask = &pb.DocumentMask{FieldPaths: sfps}
|
||||
}
|
||||
w.UpdateMask = mask
|
||||
}
|
||||
if len(transforms) > 0 || pc != nil {
|
||||
if w == nil {
|
||||
w = initializedW
|
||||
}
|
||||
w.UpdateTransforms = transforms
|
||||
}
|
||||
if w != nil {
|
||||
ws = append(ws, w)
|
||||
}
|
||||
return ws
|
||||
}
|
||||
|
||||
// arrayUnion is a special type in firestore. It instructs the server to add its
|
||||
// elements to whatever array already exists, or to create an array if no value
|
||||
// exists.
|
||||
type arrayUnion struct {
|
||||
elems []interface{}
|
||||
}
|
||||
|
||||
// ArrayUnion specifies elements to be added to whatever array already exists in
|
||||
// the server, or to create an array if no value exists.
|
||||
//
|
||||
// If a value exists and it's an array, values are appended to it. Any duplicate
|
||||
// value is ignored.
|
||||
// If a value exists and it's not an array, the value is replaced by an array of
|
||||
// the values in the ArrayUnion.
|
||||
// If a value does not exist, an array of the values in the ArrayUnion is created.
|
||||
//
|
||||
// ArrayUnion must be the value of a field directly; it cannot appear in
|
||||
// array or struct values, or in any value that is itself inside an array or
|
||||
// struct.
|
||||
func ArrayUnion(elems ...interface{}) arrayUnion {
|
||||
return arrayUnion{elems: elems}
|
||||
}
|
||||
|
||||
// This helper converts an arrayUnion into a proto object.
|
||||
func arrayUnionTransform(au arrayUnion, fp FieldPath) (*pb.DocumentTransform_FieldTransform, error) {
|
||||
var elems []*pb.Value
|
||||
for _, v := range au.elems {
|
||||
pv, _, err := toProtoValue(reflect.ValueOf(v))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
elems = append(elems, pv)
|
||||
}
|
||||
return &pb.DocumentTransform_FieldTransform{
|
||||
FieldPath: fp.toServiceFieldPath(),
|
||||
TransformType: &pb.DocumentTransform_FieldTransform_AppendMissingElements{
|
||||
AppendMissingElements: &pb.ArrayValue{Values: elems},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// arrayRemove is a special type in firestore. It instructs the server to remove
|
||||
// the specified values.
|
||||
type arrayRemove struct {
|
||||
elems []interface{}
|
||||
}
|
||||
|
||||
// ArrayRemove specifies elements to be removed from whatever array already
|
||||
// exists in the server.
|
||||
//
|
||||
// If a value exists and it's an array, values are removed from it. All
|
||||
// duplicate values are removed.
|
||||
// If a value exists and it's not an array, the value is replaced by an empty
|
||||
// array.
|
||||
// If a value does not exist, an empty array is created.
|
||||
//
|
||||
// ArrayRemove must be the value of a field directly; it cannot appear in
|
||||
// array or struct values, or in any value that is itself inside an array or
|
||||
// struct.
|
||||
func ArrayRemove(elems ...interface{}) arrayRemove {
|
||||
return arrayRemove{elems: elems}
|
||||
}
|
||||
|
||||
// This helper converts an arrayRemove into a proto object.
|
||||
func arrayRemoveTransform(ar arrayRemove, fp FieldPath) (*pb.DocumentTransform_FieldTransform, error) {
|
||||
var elems []*pb.Value
|
||||
for _, v := range ar.elems {
|
||||
// ServerTimestamp cannot occur in an array, so we ignore transformations here.
|
||||
pv, _, err := toProtoValue(reflect.ValueOf(v))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
elems = append(elems, pv)
|
||||
}
|
||||
return &pb.DocumentTransform_FieldTransform{
|
||||
FieldPath: fp.toServiceFieldPath(),
|
||||
TransformType: &pb.DocumentTransform_FieldTransform_RemoveAllFromArray{
|
||||
RemoveAllFromArray: &pb.ArrayValue{Values: elems},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type transform struct {
|
||||
t *pb.DocumentTransform_FieldTransform
|
||||
|
||||
// For v2 of this package, we may want to remove this field and
|
||||
// return an error directly from the FieldTransformX functions.
|
||||
err error
|
||||
}
|
||||
|
||||
func (t transform) String() string {
|
||||
if t.t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.t.String()
|
||||
}
|
||||
|
||||
// FieldTransformIncrement returns a special value that can be used with Set, Create, or
|
||||
// Update that tells the server to transform the field's current value
|
||||
// by the given value.
|
||||
//
|
||||
// The supported values are:
|
||||
//
|
||||
// int, int8, int16, int32, int64
|
||||
// uint8, uint16, uint32
|
||||
// float32, float64
|
||||
//
|
||||
// If the field does not yet exist, the transformation will set the field to
|
||||
// the given value.
|
||||
func FieldTransformIncrement(n interface{}) transform {
|
||||
v, err := numericTransformValue(n)
|
||||
return transform{
|
||||
t: &pb.DocumentTransform_FieldTransform{
|
||||
TransformType: &pb.DocumentTransform_FieldTransform_Increment{
|
||||
Increment: v,
|
||||
},
|
||||
},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Increment is an alias for FieldTransformIncrement.
|
||||
func Increment(n interface{}) transform {
|
||||
return FieldTransformIncrement(n)
|
||||
}
|
||||
|
||||
// FieldTransformMaximum returns a special value that can be used with Set, Create, or
|
||||
// Update that tells the server to set the field to the maximum of the
|
||||
// field's current value and the given value.
|
||||
//
|
||||
// The supported values are:
|
||||
//
|
||||
// int, int8, int16, int32, int64
|
||||
// uint8, uint16, uint32
|
||||
// float32, float64
|
||||
//
|
||||
// If the field is not an integer or double, or if the field does not yet
|
||||
// exist, the transformation will set the field to the given value. If a
|
||||
// maximum operation is applied where the field and the input value are of
|
||||
// mixed types (that is - one is an integer and one is a double) the field
|
||||
// takes on the type of the larger operand. If the operands are equivalent
|
||||
// (e.g. 3 and 3.0), the field does not change. 0, 0.0, and -0.0 are all zero.
|
||||
// The maximum of a zero stored value and zero input value is always the
|
||||
// stored value. The maximum of any numeric value x and NaN is NaN.
|
||||
func FieldTransformMaximum(n interface{}) transform {
|
||||
v, err := numericTransformValue(n)
|
||||
return transform{
|
||||
t: &pb.DocumentTransform_FieldTransform{
|
||||
TransformType: &pb.DocumentTransform_FieldTransform_Maximum{
|
||||
Maximum: v,
|
||||
},
|
||||
},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// FieldTransformMinimum returns a special value that can be used with Set, Create, or
|
||||
// Update that tells the server to set the field to the minimum of the
|
||||
// field's current value and the given value.
|
||||
//
|
||||
// The supported values are:
|
||||
//
|
||||
// int, int8, int16, int32, int64
|
||||
// uint8, uint16, uint32
|
||||
// float32, float64
|
||||
//
|
||||
// If the field is not an integer or double, or if the field does not yet
|
||||
// exist, the transformation will set the field to the given value. If a
|
||||
// minimum operation is applied where the field and the input value are of
|
||||
// mixed types (that is - one is an integer and one is a double) the field
|
||||
// takes on the type of the smaller operand. If the operands are equivalent
|
||||
// (e.g. 3 and 3.0), the field does not change. 0, 0.0, and -0.0 are all zero.
|
||||
// The minimum of a zero stored value and zero input value is always the
|
||||
// stored value. The minimum of any numeric value x and NaN is NaN.
|
||||
func FieldTransformMinimum(n interface{}) transform {
|
||||
v, err := numericTransformValue(n)
|
||||
return transform{
|
||||
t: &pb.DocumentTransform_FieldTransform{
|
||||
TransformType: &pb.DocumentTransform_FieldTransform_Minimum{
|
||||
Minimum: v,
|
||||
},
|
||||
},
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func numericTransformValue(n interface{}) (*pb.Value, error) {
|
||||
switch n.(type) {
|
||||
case int, int8, int16, int32, int64,
|
||||
uint8, uint16, uint32,
|
||||
float32, float64:
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type %T for Increment; supported values include int, int8, int16, int32, int64, uint8, uint16, uint32, float32, float64", n)
|
||||
}
|
||||
|
||||
v, _, err := toProtoValue(reflect.ValueOf(n))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func fieldTransform(ar transform, fp FieldPath) (*pb.DocumentTransform_FieldTransform, error) {
|
||||
if ar.err != nil {
|
||||
return nil, ar.err
|
||||
}
|
||||
newFt := proto.Clone(ar.t)
|
||||
ft := newFt.(*pb.DocumentTransform_FieldTransform)
|
||||
ft.FieldPath = fp.toServiceFieldPath()
|
||||
return ft, nil
|
||||
}
|
||||
|
||||
type sentinel int
|
||||
|
||||
const (
|
||||
// Delete is used as a value in a call to Update or Set with merge to indicate
|
||||
// that the corresponding key should be deleted.
|
||||
Delete sentinel = iota
|
||||
|
||||
// ServerTimestamp is used as a value in a call to Update to indicate that the
|
||||
// key's value should be set to the time at which the server processed
|
||||
// the request.
|
||||
//
|
||||
// ServerTimestamp must be the value of a field directly; it cannot appear in
|
||||
// array or struct values, or in any value that is itself inside an array or
|
||||
// struct.
|
||||
ServerTimestamp
|
||||
)
|
||||
|
||||
func (s sentinel) String() string {
|
||||
switch s {
|
||||
case Delete:
|
||||
return "Delete"
|
||||
case ServerTimestamp:
|
||||
return "ServerTimestamp"
|
||||
default:
|
||||
return "<?sentinel?>"
|
||||
}
|
||||
}
|
||||
|
||||
// An Update describes an update to a value referred to by a path.
|
||||
// An Update should have either a non-empty Path or a non-empty FieldPath,
|
||||
// but not both.
|
||||
//
|
||||
// See DocumentRef.Create for acceptable values.
|
||||
// To delete a field, specify firestore.Delete as the value.
|
||||
type Update struct {
|
||||
Path string // Will be split on dots, and must not contain any of "˜*/[]".
|
||||
FieldPath FieldPath
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// String returns string representation of firestore.Update
|
||||
func (u Update) String() string {
|
||||
t, ok := u.Value.(transform)
|
||||
if !ok {
|
||||
return fmt.Sprintf("Path: %s FieldPath: %s Value: %s", u.Path, u.FieldPath, u.Value)
|
||||
}
|
||||
return fmt.Sprintf("Path: %s FieldPath: %s Value: %s", u.Path, u.FieldPath, t.String())
|
||||
}
|
||||
|
||||
// An fpv is a pair of validated FieldPath and value.
|
||||
type fpv struct {
|
||||
fieldPath FieldPath
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (u *Update) process() (fpv, error) {
|
||||
if (u.Path != "") == (u.FieldPath != nil) {
|
||||
return fpv{}, fmt.Errorf("firestore: update %+v should have exactly one of Path or FieldPath", u)
|
||||
}
|
||||
fp := u.FieldPath
|
||||
var err error
|
||||
if fp == nil {
|
||||
fp, err = parseDotSeparatedString(u.Path)
|
||||
if err != nil {
|
||||
return fpv{}, err
|
||||
}
|
||||
}
|
||||
if err := fp.validate(); err != nil {
|
||||
return fpv{}, err
|
||||
}
|
||||
return fpv{fp, u.Value}, nil
|
||||
}
|
||||
|
||||
// Update updates the document. The values at the given
|
||||
// field paths are replaced, but other fields of the stored document are untouched.
|
||||
func (d *DocumentRef) Update(ctx context.Context, updates []Update, preconds ...Precondition) (_ *WriteResult, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.Update")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
ws, err := d.newUpdatePathWrites(updates, preconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Parent.c.commit1(ctx, ws)
|
||||
}
|
||||
|
||||
// Collections returns an iterator over the immediate sub-collections of the document.
|
||||
func (d *DocumentRef) Collections(ctx context.Context) *CollectionIterator {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.DocumentRef.ListCollectionIds")
|
||||
defer func() { trace.EndSpan(ctx, nil) }()
|
||||
|
||||
client := d.Parent.c
|
||||
it := &CollectionIterator{
|
||||
client: client,
|
||||
parent: d,
|
||||
it: client.c.ListCollectionIds(
|
||||
withResourceHeader(ctx, client.path()),
|
||||
&pb.ListCollectionIdsRequest{Parent: d.Path}),
|
||||
}
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(
|
||||
it.fetch,
|
||||
func() int { return len(it.items) },
|
||||
func() interface{} { b := it.items; it.items = nil; return b })
|
||||
return it
|
||||
}
|
||||
|
||||
// CollectionIterator is an iterator over sub-collections of a document.
|
||||
type CollectionIterator struct {
|
||||
client *Client
|
||||
parent *DocumentRef
|
||||
it *vkit.StringIterator
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
items []*CollectionRef
|
||||
err error
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
|
||||
func (it *CollectionIterator) PageInfo() *iterator.PageInfo { return it.pageInfo }
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there
|
||||
// are no more results. Once Next returns Done, all subsequent calls will return
|
||||
// Done.
|
||||
func (it *CollectionIterator) Next() (*CollectionRef, error) {
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *CollectionIterator) fetch(pageSize int, pageToken string) (string, error) {
|
||||
if it.err != nil {
|
||||
return "", it.err
|
||||
}
|
||||
return iterFetch(pageSize, pageToken, it.it.PageInfo(), func() error {
|
||||
id, err := it.it.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var cr *CollectionRef
|
||||
if it.parent == nil {
|
||||
cr = newTopLevelCollRef(it.client, it.client.path(), id)
|
||||
} else {
|
||||
cr = newCollRefWithParent(it.client, it.parent, id)
|
||||
}
|
||||
it.items = append(it.items, cr)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetAll returns all the collections remaining from the iterator.
|
||||
func (it *CollectionIterator) GetAll() ([]*CollectionRef, error) {
|
||||
var crs []*CollectionRef
|
||||
for {
|
||||
cr, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crs = append(crs, cr)
|
||||
}
|
||||
return crs, nil
|
||||
}
|
||||
|
||||
// Common fetch code for iterators that are backed by vkit iterators.
|
||||
// TODO(jba): dedup with same function in logging/logadmin.
|
||||
func iterFetch(pageSize int, pageToken string, pi *iterator.PageInfo, next func() error) (string, error) {
|
||||
pi.MaxSize = pageSize
|
||||
pi.Token = pageToken
|
||||
// Get one item, which will fill the buffer.
|
||||
if err := next(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Collect the rest of the buffer.
|
||||
for pi.Remaining() > 0 {
|
||||
if err := next(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return pi.Token, nil
|
||||
}
|
||||
|
||||
// Snapshots returns an iterator over snapshots of the document. Each time the document
|
||||
// changes or is added or deleted, a new snapshot will be generated.
|
||||
func (d *DocumentRef) Snapshots(ctx context.Context) *DocumentSnapshotIterator {
|
||||
return &DocumentSnapshotIterator{
|
||||
docref: d,
|
||||
ws: newWatchStreamForDocument(ctx, d),
|
||||
}
|
||||
}
|
||||
|
||||
// DocumentSnapshotIterator is an iterator over snapshots of a document.
|
||||
// Call Next on the iterator to get a snapshot of the document each time it changes.
|
||||
// Call Stop on the iterator when done.
|
||||
//
|
||||
// For an example, see DocumentRef.Snapshots.
|
||||
type DocumentSnapshotIterator struct {
|
||||
docref *DocumentRef
|
||||
ws *watchStream
|
||||
}
|
||||
|
||||
// Next blocks until the document changes, then returns the DocumentSnapshot for
|
||||
// the current state of the document. If the document has been deleted, Next
|
||||
// returns a DocumentSnapshot whose Exists method returns false.
|
||||
//
|
||||
// Next is not expected to return iterator.Done unless it is called after Stop.
|
||||
// Rarely, networking issues may also cause iterator.Done to be returned.
|
||||
func (it *DocumentSnapshotIterator) Next() (*DocumentSnapshot, error) {
|
||||
btree, _, rt, err := it.ws.nextSnapshot()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = iterator.Done
|
||||
}
|
||||
// watchStream's error is sticky, so SnapshotIterator does not need to remember it.
|
||||
return nil, err
|
||||
}
|
||||
if btree.Len() == 0 { // document deleted
|
||||
return &DocumentSnapshot{Ref: it.docref, ReadTime: rt}, nil
|
||||
}
|
||||
snap, _ := btree.At(0)
|
||||
return snap.(*DocumentSnapshot), nil
|
||||
}
|
||||
|
||||
// Stop stops receiving snapshots. You should always call Stop when you are done with
|
||||
// a DocumentSnapshotIterator, to free up resources. It is not safe to call Stop
|
||||
// concurrently with Next.
|
||||
func (it *DocumentSnapshotIterator) Stop() {
|
||||
it.ws.stop()
|
||||
}
|
||||
|
||||
// WithReadOptions specifies constraints for accessing documents from the database,
|
||||
// e.g. at what time snapshot to read the documents.
|
||||
func (d *DocumentRef) WithReadOptions(opts ...ReadOption) *DocumentRef {
|
||||
for _, ro := range opts {
|
||||
ro.apply(d.readSettings)
|
||||
}
|
||||
return d
|
||||
}
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
// Copyright 2017 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
tspb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// A DocumentSnapshot contains document data and metadata.
|
||||
type DocumentSnapshot struct {
|
||||
// The DocumentRef for this document.
|
||||
Ref *DocumentRef
|
||||
|
||||
// Read-only. The time at which the document was created.
|
||||
// Increases monotonically when a document is deleted then
|
||||
// recreated. It can also be compared to values from other documents and
|
||||
// the read time of a query.
|
||||
CreateTime time.Time
|
||||
|
||||
// Read-only. The time at which the document was last changed. This value
|
||||
// is initially set to CreateTime then increases monotonically with each
|
||||
// change to the document. It can also be compared to values from other
|
||||
// documents and the read time of a query.
|
||||
UpdateTime time.Time
|
||||
|
||||
// Read-only. The time at which the document was read.
|
||||
ReadTime time.Time
|
||||
|
||||
c *Client
|
||||
proto *pb.Document
|
||||
}
|
||||
|
||||
// Exists reports whether the DocumentSnapshot represents an existing document.
|
||||
// Even if Exists returns false, the Ref and ReadTime fields of the DocumentSnapshot
|
||||
// are valid.
|
||||
func (d *DocumentSnapshot) Exists() bool {
|
||||
return d.proto != nil
|
||||
}
|
||||
|
||||
// Data returns the DocumentSnapshot's fields as a map.
|
||||
// It is equivalent to
|
||||
//
|
||||
// var m map[string]interface{}
|
||||
// d.DataTo(&m)
|
||||
//
|
||||
// except that it returns nil if the document does not exist.
|
||||
func (d *DocumentSnapshot) Data() map[string]interface{} {
|
||||
if !d.Exists() {
|
||||
return nil
|
||||
}
|
||||
m, err := createMapFromValueMap(d.proto.Fields, d.c)
|
||||
// Any error here is a bug in the client.
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("firestore: %v", err))
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// DataTo uses the document's fields to populate p, which can be a pointer to a
|
||||
// map[string]interface{} or a pointer to a struct.
|
||||
//
|
||||
// Firestore field values are converted to Go values as follows:
|
||||
// - Null converts to nil.
|
||||
// - Bool converts to bool.
|
||||
// - String converts to string.
|
||||
// - Integer converts int64. When setting a struct field, any signed or unsigned
|
||||
// integer type is permitted except uint, uint64 or uintptr. Overflow is detected
|
||||
// and results in an error.
|
||||
// - Double converts to float64. When setting a struct field, float32 is permitted.
|
||||
// Overflow is detected and results in an error.
|
||||
// - Bytes is converted to []byte.
|
||||
// - Timestamp converts to time.Time.
|
||||
// - GeoPoint converts to *latlng.LatLng, where latlng is the package
|
||||
// "google.golang.org/genproto/googleapis/type/latlng".
|
||||
// - Arrays convert to []interface{}. When setting a struct field, the field
|
||||
// may be a slice or array of any type and is populated recursively.
|
||||
// Slices are resized to the incoming value's size, while arrays that are too
|
||||
// long have excess elements filled with zero values. If the array is too short,
|
||||
// excess incoming values will be dropped.
|
||||
// - Vectors convert to []float64
|
||||
// - Maps convert to map[string]interface{}. When setting a struct field,
|
||||
// maps of key type string and any value type are permitted, and are populated
|
||||
// recursively.
|
||||
// - References are converted to *firestore.DocumentRefs.
|
||||
//
|
||||
// Field names given by struct field tags are observed, as described in
|
||||
// DocumentRef.Create.
|
||||
//
|
||||
// Only the fields actually present in the document are used to populate p. Other fields
|
||||
// of p are left unchanged.
|
||||
//
|
||||
// If the document does not exist, DataTo returns a NotFound error.
|
||||
func (d *DocumentSnapshot) DataTo(p interface{}) error {
|
||||
if !d.Exists() {
|
||||
return status.Errorf(codes.NotFound, "document %s does not exist", d.Ref.Path)
|
||||
}
|
||||
return setFromProtoValue(p, &pb.Value{ValueType: &pb.Value_MapValue{MapValue: &pb.MapValue{Fields: d.proto.Fields}}}, d.c)
|
||||
}
|
||||
|
||||
// DataAt returns the data value denoted by path.
|
||||
//
|
||||
// The path argument can be a single field or a dot-separated sequence of
|
||||
// fields, and must not contain any of the runes "˜*/[]". Use DataAtPath instead for
|
||||
// such a path.
|
||||
//
|
||||
// See DocumentSnapshot.DataTo for how Firestore values are converted to Go values.
|
||||
//
|
||||
// If the document does not exist, DataAt returns a NotFound error.
|
||||
func (d *DocumentSnapshot) DataAt(path string) (interface{}, error) {
|
||||
if !d.Exists() {
|
||||
return nil, status.Errorf(codes.NotFound, "document %s does not exist", d.Ref.Path)
|
||||
}
|
||||
fp, err := parseDotSeparatedString(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.DataAtPath(fp)
|
||||
}
|
||||
|
||||
// DataAtPath returns the data value denoted by the FieldPath fp.
|
||||
// If the document does not exist, DataAtPath returns a NotFound error.
|
||||
func (d *DocumentSnapshot) DataAtPath(fp FieldPath) (interface{}, error) {
|
||||
if !d.Exists() {
|
||||
return nil, status.Errorf(codes.NotFound, "document %s does not exist", d.Ref.Path)
|
||||
}
|
||||
v, err := valueAtPath(fp, d.proto.Fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return createFromProtoValue(v, d.c)
|
||||
}
|
||||
|
||||
// valueAtPath returns the value of m referred to by fp.
|
||||
func valueAtPath(fp FieldPath, m map[string]*pb.Value) (*pb.Value, error) {
|
||||
for _, k := range fp[:len(fp)-1] {
|
||||
v := m[k]
|
||||
if v == nil {
|
||||
return nil, fmt.Errorf("firestore: no field %q", k)
|
||||
}
|
||||
mv := v.GetMapValue()
|
||||
if mv == nil {
|
||||
return nil, fmt.Errorf("firestore: value for field %q is not a map", k)
|
||||
}
|
||||
m = mv.Fields
|
||||
}
|
||||
k := fp[len(fp)-1]
|
||||
v := m[k]
|
||||
if v == nil {
|
||||
return nil, fmt.Errorf("firestore: no field %q", k)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// toProtoDocument converts a Go value to a Document proto.
|
||||
// Valid values are: map[string]T, struct, or pointer to a valid value.
|
||||
// It also returns a list DocumentTransforms.
|
||||
func toProtoDocument(x interface{}) (*pb.Document, []*pb.DocumentTransform_FieldTransform, error) {
|
||||
if x == nil {
|
||||
return nil, nil, errors.New("firestore: nil document contents")
|
||||
}
|
||||
v := reflect.ValueOf(x)
|
||||
pv, _, err := toProtoValue(v)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var transforms []*pb.DocumentTransform_FieldTransform
|
||||
transforms, err = extractTransforms(v, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var fields map[string]*pb.Value
|
||||
if pv != nil {
|
||||
m := pv.GetMapValue()
|
||||
if m == nil {
|
||||
return nil, nil, fmt.Errorf("firestore: cannot convert value of type %T into a map", x)
|
||||
}
|
||||
fields = m.Fields
|
||||
}
|
||||
return &pb.Document{Fields: fields}, transforms, nil
|
||||
}
|
||||
|
||||
func extractTransforms(v reflect.Value, prefix FieldPath) ([]*pb.DocumentTransform_FieldTransform, error) {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
return extractTransformsFromMap(v, prefix)
|
||||
case reflect.Struct:
|
||||
return extractTransformsFromStruct(v, prefix)
|
||||
case reflect.Ptr:
|
||||
if v.IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
return extractTransforms(v.Elem(), prefix)
|
||||
case reflect.Interface:
|
||||
if v.NumMethod() == 0 { // empty interface: recurse on its contents
|
||||
return extractTransforms(v.Elem(), prefix)
|
||||
}
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func extractTransformsFromMap(v reflect.Value, prefix FieldPath) ([]*pb.DocumentTransform_FieldTransform, error) {
|
||||
var transforms []*pb.DocumentTransform_FieldTransform
|
||||
for _, k := range v.MapKeys() {
|
||||
sk := k.Interface().(string) // assume keys are strings; checked in toProtoValue
|
||||
path := prefix.with(sk)
|
||||
mi := v.MapIndex(k)
|
||||
if mi.Interface() == ServerTimestamp {
|
||||
transforms = append(transforms, serverTimestamp(path.toServiceFieldPath()))
|
||||
} else if au, ok := mi.Interface().(arrayUnion); ok {
|
||||
t, err := arrayUnionTransform(au, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, t)
|
||||
} else if ar, ok := mi.Interface().(arrayRemove); ok {
|
||||
t, err := arrayRemoveTransform(ar, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, t)
|
||||
} else if ar, ok := mi.Interface().(transform); ok {
|
||||
t, err := fieldTransform(ar, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, t)
|
||||
} else {
|
||||
ps, err := extractTransforms(mi, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, ps...)
|
||||
}
|
||||
}
|
||||
return transforms, nil
|
||||
}
|
||||
|
||||
func extractTransformsFromStruct(v reflect.Value, prefix FieldPath) ([]*pb.DocumentTransform_FieldTransform, error) {
|
||||
var transforms []*pb.DocumentTransform_FieldTransform
|
||||
fields, err := fieldCache.Fields(v.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range fields {
|
||||
fv := v.FieldByIndex(f.Index)
|
||||
path := prefix.with(f.Name)
|
||||
opts := f.ParsedTag.(tagOptions)
|
||||
if opts.serverTimestamp {
|
||||
var isZero bool
|
||||
switch f.Type {
|
||||
case typeOfGoTime:
|
||||
isZero = fv.Interface().(time.Time).IsZero()
|
||||
case reflect.PtrTo(typeOfGoTime):
|
||||
isZero = fv.IsNil() || fv.Elem().Interface().(time.Time).IsZero()
|
||||
default:
|
||||
return nil, fmt.Errorf("firestore: field %s of struct %s with serverTimestamp tag must be of type time.Time or *time.Time",
|
||||
f.Name, v.Type())
|
||||
}
|
||||
if isZero {
|
||||
transforms = append(transforms, serverTimestamp(path.toServiceFieldPath()))
|
||||
}
|
||||
} else {
|
||||
ps, err := extractTransforms(fv, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transforms = append(transforms, ps...)
|
||||
}
|
||||
}
|
||||
return transforms, nil
|
||||
}
|
||||
|
||||
func newDocumentSnapshot(ref *DocumentRef, proto *pb.Document, c *Client, readTime *tspb.Timestamp) (*DocumentSnapshot, error) {
|
||||
d := &DocumentSnapshot{
|
||||
Ref: ref,
|
||||
c: c,
|
||||
proto: proto,
|
||||
}
|
||||
if proto != nil {
|
||||
if err := proto.GetCreateTime().CheckValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := proto.GetUpdateTime().CheckValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.CreateTime = proto.GetCreateTime().AsTime()
|
||||
d.UpdateTime = proto.GetUpdateTime().AsTime()
|
||||
}
|
||||
if readTime != nil {
|
||||
if err := readTime.CheckValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.ReadTime = readTime.AsTime()
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func serverTimestamp(path string) *pb.DocumentTransform_FieldTransform {
|
||||
return &pb.DocumentTransform_FieldTransform{
|
||||
FieldPath: path,
|
||||
TransformType: &pb.DocumentTransform_FieldTransform_SetToServerValue{
|
||||
SetToServerValue: pb.DocumentTransform_FieldTransform_REQUEST_TIME,
|
||||
},
|
||||
}
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
// Copyright 2017 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"cloud.google.com/go/internal/fields"
|
||||
)
|
||||
|
||||
const invalidRunes = "~*/[]"
|
||||
|
||||
// A FieldPath is a non-empty sequence of non-empty fields that reference a value.
|
||||
//
|
||||
// A FieldPath value should only be necessary if one of the field names contains
|
||||
// one of the runes ".˜*/[]". Most methods accept a simpler form of field path
|
||||
// as a string in which the individual fields are separated by dots.
|
||||
// For example,
|
||||
//
|
||||
// []string{"a", "b"}
|
||||
//
|
||||
// is equivalent to the string form
|
||||
//
|
||||
// "a.b"
|
||||
//
|
||||
// but
|
||||
//
|
||||
// []string{"*"}
|
||||
//
|
||||
// has no equivalent string form.
|
||||
type FieldPath []string
|
||||
|
||||
// parseDotSeparatedString constructs a FieldPath from a string that separates
|
||||
// path components with dots. Other than splitting at dots and checking for invalid
|
||||
// characters, it ignores everything else about the string,
|
||||
// including attempts to quote field path compontents. So "a.`b.c`.d" is parsed into
|
||||
// four parts, "a", "`b", "c`" and "d".
|
||||
func parseDotSeparatedString(s string) (FieldPath, error) {
|
||||
if strings.ContainsAny(s, invalidRunes) {
|
||||
return nil, errInvalidRunesField(s)
|
||||
}
|
||||
fp := FieldPath(strings.Split(s, "."))
|
||||
if err := fp.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fp, nil
|
||||
}
|
||||
|
||||
func (fp1 FieldPath) equal(fp2 FieldPath) bool {
|
||||
if len(fp1) != len(fp2) {
|
||||
return false
|
||||
}
|
||||
for i, c1 := range fp1 {
|
||||
if c1 != fp2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (fp1 FieldPath) prefixOf(fp2 FieldPath) bool {
|
||||
return len(fp1) <= len(fp2) && fp1.equal(fp2[:len(fp1)])
|
||||
}
|
||||
|
||||
// Lexicographic ordering.
|
||||
func (fp1 FieldPath) less(fp2 FieldPath) bool {
|
||||
for i := range fp1 {
|
||||
switch {
|
||||
case i >= len(fp2):
|
||||
return false
|
||||
case fp1[i] < fp2[i]:
|
||||
return true
|
||||
case fp1[i] > fp2[i]:
|
||||
return false
|
||||
}
|
||||
}
|
||||
// fp1 and fp2 are equal up to len(fp1).
|
||||
return len(fp1) < len(fp2)
|
||||
}
|
||||
|
||||
// validate checks the validity of fp and returns an error if it is invalid.
|
||||
func (fp FieldPath) validate() error {
|
||||
if len(fp) == 0 {
|
||||
return errors.New("firestore: empty field path")
|
||||
}
|
||||
for _, c := range fp {
|
||||
if len(c) == 0 {
|
||||
return errors.New("firestore: empty component in field path")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// with creates a new FieldPath consisting of fp followed by k.
|
||||
func (fp FieldPath) with(k string) FieldPath {
|
||||
r := make(FieldPath, len(fp), len(fp)+1)
|
||||
copy(r, fp)
|
||||
return append(r, k)
|
||||
}
|
||||
|
||||
// checkNoDupOrPrefix checks whether any FieldPath is a prefix of (or equal to)
|
||||
// another.
|
||||
// It modifies the order of FieldPaths in its argument (via sorting).
|
||||
func checkNoDupOrPrefix(fps []FieldPath) error {
|
||||
// Sort fps lexicographically.
|
||||
sort.Sort(byPath(fps))
|
||||
// Check adjacent pairs for prefix.
|
||||
for i := 1; i < len(fps); i++ {
|
||||
if fps[i-1].prefixOf(fps[i]) {
|
||||
return fmt.Errorf("field path %v cannot be used in the same update as %v", fps[i-1], fps[i])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type byPath []FieldPath
|
||||
|
||||
func (b byPath) Len() int { return len(b) }
|
||||
func (b byPath) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||
func (b byPath) Less(i, j int) bool { return b[i].less(b[j]) }
|
||||
|
||||
// setAtPath sets val at the location in m specified by fp, creating sub-maps as
|
||||
// needed. m must not be nil. fp is assumed to be valid.
|
||||
func setAtPath(m map[string]*pb.Value, fp FieldPath, val *pb.Value) {
|
||||
if val == nil {
|
||||
return
|
||||
}
|
||||
if len(fp) == 1 {
|
||||
m[fp[0]] = val
|
||||
} else {
|
||||
v, ok := m[fp[0]]
|
||||
if !ok {
|
||||
v = &pb.Value{ValueType: &pb.Value_MapValue{MapValue: &pb.MapValue{Fields: map[string]*pb.Value{}}}}
|
||||
m[fp[0]] = v
|
||||
}
|
||||
// The type assertion below cannot fail, because setAtPath is only called
|
||||
// with either an empty map or one filled by setAtPath itself, and the
|
||||
// set of FieldPaths it is called with has been checked to make sure that
|
||||
// no path is the prefix of any other.
|
||||
setAtPath(v.GetMapValue().Fields, fp[1:], val)
|
||||
}
|
||||
}
|
||||
|
||||
// getAtPath gets the value in data referred to by fp. The data argument can
|
||||
// be a map or a struct.
|
||||
// Compare with valueAtPath, which does the same thing for a document.
|
||||
func getAtPath(v reflect.Value, fp FieldPath) (interface{}, error) {
|
||||
var err error
|
||||
for _, k := range fp {
|
||||
v, err = getAtField(v, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return v.Interface(), nil
|
||||
}
|
||||
|
||||
// getAtField returns the equivalent of v[k], if v is a map, or v.k if v is a struct.
|
||||
func getAtField(v reflect.Value, k string) (reflect.Value, error) {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
if r := v.MapIndex(reflect.ValueOf(k)); r.IsValid() {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
fm, err := fieldMap(v.Type())
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
if f, ok := fm[k]; ok {
|
||||
return v.FieldByIndex(f.Index), nil
|
||||
}
|
||||
|
||||
case reflect.Interface:
|
||||
return getAtField(v.Elem(), k)
|
||||
|
||||
case reflect.Ptr:
|
||||
return getAtField(v.Elem(), k)
|
||||
}
|
||||
return reflect.Value{}, fmt.Errorf("firestore: no field %q for value %#v", k, v)
|
||||
}
|
||||
|
||||
// fieldMapCache holds maps from from Firestore field name to struct field,
|
||||
// keyed by struct type.
|
||||
var fieldMapCache sync.Map
|
||||
|
||||
func fieldMap(t reflect.Type) (map[string]fields.Field, error) {
|
||||
x, ok := fieldMapCache.Load(t)
|
||||
if !ok {
|
||||
fieldList, err := fieldCache.Fields(t)
|
||||
if err != nil {
|
||||
x = err
|
||||
} else {
|
||||
m := map[string]fields.Field{}
|
||||
for _, f := range fieldList {
|
||||
m[f.Name] = f
|
||||
}
|
||||
x = m
|
||||
}
|
||||
fieldMapCache.Store(t, x)
|
||||
}
|
||||
if err, ok := x.(error); ok {
|
||||
return nil, err
|
||||
}
|
||||
return x.(map[string]fields.Field), nil
|
||||
}
|
||||
|
||||
// toServiceFieldPath converts fp the form required by the Firestore service.
|
||||
// It assumes fp has been validated.
|
||||
func (fp FieldPath) toServiceFieldPath() string {
|
||||
cs := make([]string, len(fp))
|
||||
for i, c := range fp {
|
||||
cs[i] = toServiceFieldPathComponent(c)
|
||||
}
|
||||
return strings.Join(cs, ".")
|
||||
}
|
||||
|
||||
func toServiceFieldPaths(fps []FieldPath) []string {
|
||||
var sfps []string
|
||||
for _, fp := range fps {
|
||||
sfps = append(sfps, fp.toServiceFieldPath())
|
||||
}
|
||||
return sfps
|
||||
}
|
||||
|
||||
// Google SQL syntax for an unquoted field.
|
||||
var unquotedFieldRegexp = regexp.MustCompile("^[A-Za-z_][A-Za-z_0-9]*$")
|
||||
|
||||
// toServiceFieldPathComponent returns a string that represents key and is a valid
|
||||
// field path component.
|
||||
func toServiceFieldPathComponent(key string) string {
|
||||
if unquotedFieldRegexp.MatchString(key) {
|
||||
return key
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
buf.WriteRune('`')
|
||||
for _, r := range key {
|
||||
if r == '`' || r == '\\' {
|
||||
buf.WriteRune('\\')
|
||||
}
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
buf.WriteRune('`')
|
||||
return buf.String()
|
||||
}
|
||||
+476
@@ -0,0 +1,476 @@
|
||||
// Copyright 2017 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"cloud.google.com/go/internal/fields"
|
||||
)
|
||||
|
||||
func setFromProtoValue(x interface{}, vproto *pb.Value, c *Client) error {
|
||||
v := reflect.ValueOf(x)
|
||||
if v.Kind() != reflect.Ptr || v.IsNil() {
|
||||
return errors.New("firestore: nil or not a pointer")
|
||||
}
|
||||
return setReflectFromProtoValue(v.Elem(), vproto, c)
|
||||
}
|
||||
|
||||
// setReflectFromProtoValue sets vDest from a Firestore Value.
|
||||
// vDest must be a settable value.
|
||||
func setReflectFromProtoValue(vDest reflect.Value, vprotoSrc *pb.Value, c *Client) error {
|
||||
typeErr := func() error {
|
||||
return fmt.Errorf("firestore: cannot set type %s to %s", vDest.Type(), typeString(vprotoSrc))
|
||||
}
|
||||
|
||||
valTypeSrc := vprotoSrc.ValueType
|
||||
// A Null value sets anything nullable to nil, and has no effect
|
||||
// on anything else.
|
||||
if _, ok := valTypeSrc.(*pb.Value_NullValue); ok {
|
||||
switch vDest.Kind() {
|
||||
case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
|
||||
vDest.Set(reflect.Zero(vDest.Type()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle special types first.
|
||||
switch vDest.Type() {
|
||||
case typeOfByteSlice:
|
||||
x, ok := valTypeSrc.(*pb.Value_BytesValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
vDest.SetBytes(x.BytesValue)
|
||||
return nil
|
||||
|
||||
case typeOfGoTime:
|
||||
x, ok := valTypeSrc.(*pb.Value_TimestampValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
if err := x.TimestampValue.CheckValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
vDest.Set(reflect.ValueOf(x.TimestampValue.AsTime()))
|
||||
return nil
|
||||
|
||||
case typeOfProtoTimestamp:
|
||||
x, ok := valTypeSrc.(*pb.Value_TimestampValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
vDest.Set(reflect.ValueOf(x.TimestampValue))
|
||||
return nil
|
||||
|
||||
case typeOfLatLng:
|
||||
x, ok := valTypeSrc.(*pb.Value_GeoPointValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
vDest.Set(reflect.ValueOf(x.GeoPointValue))
|
||||
return nil
|
||||
|
||||
case typeOfDocumentRef:
|
||||
x, ok := valTypeSrc.(*pb.Value_ReferenceValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
dr, err := pathToDoc(x.ReferenceValue, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vDest.Set(reflect.ValueOf(dr))
|
||||
return nil
|
||||
|
||||
case typeOfVector32:
|
||||
val, err := vector32FromProtoValue(vprotoSrc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vDest.Set(reflect.ValueOf(val))
|
||||
return nil
|
||||
case typeOfVector64:
|
||||
val, err := vector64FromProtoValue(vprotoSrc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vDest.Set(reflect.ValueOf(val))
|
||||
return nil
|
||||
}
|
||||
|
||||
switch vDest.Kind() {
|
||||
case reflect.Bool:
|
||||
x, ok := valTypeSrc.(*pb.Value_BooleanValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
vDest.SetBool(x.BooleanValue)
|
||||
|
||||
case reflect.String:
|
||||
x, ok := valTypeSrc.(*pb.Value_StringValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
vDest.SetString(x.StringValue)
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
var i int64
|
||||
switch x := valTypeSrc.(type) {
|
||||
case *pb.Value_IntegerValue:
|
||||
i = x.IntegerValue
|
||||
case *pb.Value_DoubleValue:
|
||||
f := x.DoubleValue
|
||||
i = int64(f)
|
||||
if float64(i) != f {
|
||||
return fmt.Errorf("firestore: float %f does not fit into %s", f, vDest.Type())
|
||||
}
|
||||
default:
|
||||
return typeErr()
|
||||
}
|
||||
if vDest.OverflowInt(i) {
|
||||
return overflowErr(vDest, i)
|
||||
}
|
||||
vDest.SetInt(i)
|
||||
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
||||
var u uint64
|
||||
switch x := valTypeSrc.(type) {
|
||||
case *pb.Value_IntegerValue:
|
||||
u = uint64(x.IntegerValue)
|
||||
case *pb.Value_DoubleValue:
|
||||
f := x.DoubleValue
|
||||
u = uint64(f)
|
||||
if float64(u) != f {
|
||||
return fmt.Errorf("firestore: float %f does not fit into %s", f, vDest.Type())
|
||||
}
|
||||
default:
|
||||
return typeErr()
|
||||
}
|
||||
if vDest.OverflowUint(u) {
|
||||
return overflowErr(vDest, u)
|
||||
}
|
||||
vDest.SetUint(u)
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
var f float64
|
||||
switch x := valTypeSrc.(type) {
|
||||
case *pb.Value_DoubleValue:
|
||||
f = x.DoubleValue
|
||||
case *pb.Value_IntegerValue:
|
||||
f = float64(x.IntegerValue)
|
||||
if int64(f) != x.IntegerValue {
|
||||
return overflowErr(vDest, x.IntegerValue)
|
||||
}
|
||||
default:
|
||||
return typeErr()
|
||||
}
|
||||
if vDest.OverflowFloat(f) {
|
||||
return overflowErr(vDest, f)
|
||||
}
|
||||
vDest.SetFloat(f)
|
||||
|
||||
case reflect.Slice:
|
||||
x, ok := valTypeSrc.(*pb.Value_ArrayValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
vals := x.ArrayValue.Values
|
||||
vlen := vDest.Len()
|
||||
xlen := len(vals)
|
||||
// Make a slice of the right size, avoiding allocation if possible.
|
||||
switch {
|
||||
case vlen < xlen:
|
||||
vDest.Set(reflect.MakeSlice(vDest.Type(), xlen, xlen))
|
||||
case vlen > xlen:
|
||||
vDest.SetLen(xlen)
|
||||
}
|
||||
return populateRepeated(vDest, vals, xlen, c)
|
||||
|
||||
case reflect.Array:
|
||||
x, ok := valTypeSrc.(*pb.Value_ArrayValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
vals := x.ArrayValue.Values
|
||||
xlen := len(vals)
|
||||
vlen := vDest.Len()
|
||||
minlen := vlen
|
||||
// Set extra elements to their zero value.
|
||||
if vlen > xlen {
|
||||
z := reflect.Zero(vDest.Type().Elem())
|
||||
for i := xlen; i < vlen; i++ {
|
||||
vDest.Index(i).Set(z)
|
||||
}
|
||||
minlen = xlen
|
||||
}
|
||||
return populateRepeated(vDest, vals, minlen, c)
|
||||
|
||||
case reflect.Map:
|
||||
x, ok := valTypeSrc.(*pb.Value_MapValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
return populateMap(vDest, x.MapValue.Fields, c)
|
||||
|
||||
case reflect.Ptr:
|
||||
// If the pointer is nil, set it to a zero value.
|
||||
if vDest.IsNil() {
|
||||
vDest.Set(reflect.New(vDest.Type().Elem()))
|
||||
}
|
||||
return setReflectFromProtoValue(vDest.Elem(), vprotoSrc, c)
|
||||
|
||||
case reflect.Struct:
|
||||
x, ok := valTypeSrc.(*pb.Value_MapValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
return populateStruct(vDest, x.MapValue.Fields, c)
|
||||
|
||||
case reflect.Interface:
|
||||
if vDest.NumMethod() == 0 { // empty interface
|
||||
// If v holds a pointer, set the pointer.
|
||||
if !vDest.IsNil() && vDest.Elem().Kind() == reflect.Ptr {
|
||||
return setReflectFromProtoValue(vDest.Elem(), vprotoSrc, c)
|
||||
}
|
||||
// Otherwise, create a fresh value.
|
||||
x, err := createFromProtoValue(vprotoSrc, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vDest.Set(reflect.ValueOf(x))
|
||||
return nil
|
||||
}
|
||||
// Any other kind of interface is an error.
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
return fmt.Errorf("firestore: cannot set type %s", vDest.Type())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// populateRepeated sets the first n elements of vr, which must be a slice or
|
||||
// array, to the corresponding elements of vals.
|
||||
func populateRepeated(vr reflect.Value, vals []*pb.Value, n int, c *Client) error {
|
||||
for i := 0; i < n; i++ {
|
||||
if err := setReflectFromProtoValue(vr.Index(i), vals[i], c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// populateMap sets the elements of vm, which must be a map, from the
|
||||
// corresponding elements of pm.
|
||||
//
|
||||
// Since a map value is not settable, this function always creates a new
|
||||
// element for each corresponding map key. Existing values of vm are
|
||||
// overwritten. This happens even if the map value is something like a pointer
|
||||
// to a struct, where we could in theory populate the existing struct value
|
||||
// instead of discarding it. This behavior matches encoding/json.
|
||||
func populateMap(vm reflect.Value, pm map[string]*pb.Value, c *Client) error {
|
||||
t := vm.Type()
|
||||
if t.Key().Kind() != reflect.String {
|
||||
return errors.New("firestore: map key type is not string")
|
||||
}
|
||||
if vm.IsNil() {
|
||||
vm.Set(reflect.MakeMap(t))
|
||||
}
|
||||
et := t.Elem()
|
||||
for k, vproto := range pm {
|
||||
el := reflect.New(et).Elem()
|
||||
if err := setReflectFromProtoValue(el, vproto, c); err != nil {
|
||||
return err
|
||||
}
|
||||
vm.SetMapIndex(reflect.ValueOf(k), el)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createMapFromValueMap creates a fresh map and populates it with pm.
|
||||
func createMapFromValueMap(pm map[string]*pb.Value, c *Client) (map[string]interface{}, error) {
|
||||
m := map[string]interface{}{}
|
||||
for k, pv := range pm {
|
||||
v, err := createFromProtoValue(pv, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m[k] = v
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// populateStruct sets the fields of vs, which must be a struct, from
|
||||
// the matching elements of pm.
|
||||
func populateStruct(vs reflect.Value, pm map[string]*pb.Value, c *Client) error {
|
||||
fs, err := fieldCache.Fields(vs.Type())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type match struct {
|
||||
vproto *pb.Value
|
||||
f *fields.Field
|
||||
}
|
||||
// Find best field matches
|
||||
matched := make(map[string]match)
|
||||
for k, vproto := range pm {
|
||||
f := fs.Match(k)
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := matched[f.Name]; ok {
|
||||
// If multiple case insensitive fields match, the exact match
|
||||
// should win.
|
||||
if f.Name == k {
|
||||
matched[k] = match{vproto: vproto, f: f}
|
||||
}
|
||||
} else {
|
||||
matched[f.Name] = match{vproto: vproto, f: f}
|
||||
}
|
||||
}
|
||||
|
||||
// Reflect values
|
||||
for _, v := range matched {
|
||||
f := v.f
|
||||
vproto := v.vproto
|
||||
|
||||
if err := setReflectFromProtoValue(vs.FieldByIndex(f.Index), vproto, c); err != nil {
|
||||
return fmt.Errorf("%s.%s: %w", vs.Type(), f.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createFromProtoValue(vproto *pb.Value, c *Client) (interface{}, error) {
|
||||
switch v := vproto.ValueType.(type) {
|
||||
case *pb.Value_NullValue:
|
||||
return nil, nil
|
||||
case *pb.Value_BooleanValue:
|
||||
return v.BooleanValue, nil
|
||||
case *pb.Value_IntegerValue:
|
||||
return v.IntegerValue, nil
|
||||
case *pb.Value_DoubleValue:
|
||||
return v.DoubleValue, nil
|
||||
case *pb.Value_TimestampValue:
|
||||
if err := v.TimestampValue.CheckValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.TimestampValue.AsTime(), nil
|
||||
case *pb.Value_StringValue:
|
||||
return v.StringValue, nil
|
||||
case *pb.Value_BytesValue:
|
||||
return v.BytesValue, nil
|
||||
case *pb.Value_ReferenceValue:
|
||||
return pathToDoc(v.ReferenceValue, c)
|
||||
case *pb.Value_GeoPointValue:
|
||||
return v.GeoPointValue, nil
|
||||
case *pb.Value_ArrayValue:
|
||||
vals := v.ArrayValue.Values
|
||||
ret := make([]interface{}, len(vals))
|
||||
for i, v := range vals {
|
||||
r, err := createFromProtoValue(v, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[i] = r
|
||||
}
|
||||
return ret, nil
|
||||
|
||||
case *pb.Value_MapValue:
|
||||
fields := v.MapValue.Fields
|
||||
ret := make(map[string]interface{}, len(fields))
|
||||
for k, v := range fields {
|
||||
r, err := createFromProtoValue(v, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[k] = r
|
||||
}
|
||||
|
||||
typeVal, ok := ret[typeKey]
|
||||
if !ok || typeVal != typeValVector {
|
||||
// Map is not a vector. Return the map
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Special handling for vector
|
||||
return vector64FromProtoValue(vproto)
|
||||
default:
|
||||
return nil, fmt.Errorf("firestore: unknown value type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert a document path to a DocumentRef.
|
||||
func pathToDoc(docPath string, c *Client) (*DocumentRef, error) {
|
||||
projID, dbID, docIDs, err := parseDocumentPath(docPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentResourceName := fmt.Sprintf("projects/%s/databases/%s", projID, dbID)
|
||||
_, doc := c.idsToRef(docIDs, parentResourceName)
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// A document path should be of the form "projects/P/databases/D/documents/coll1/doc1/coll2/doc2/...".
|
||||
func parseDocumentPath(path string) (projectID, databaseID string, docPath []string, err error) {
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 6 || parts[0] != "projects" || parts[2] != "databases" || parts[4] != "documents" {
|
||||
return "", "", nil, fmt.Errorf("firestore: malformed document path %q", path)
|
||||
}
|
||||
docp := parts[5:]
|
||||
if len(docp)%2 != 0 {
|
||||
return "", "", nil, fmt.Errorf("firestore: path %q refers to collection, not document", path)
|
||||
}
|
||||
return parts[1], parts[3], docp, nil
|
||||
}
|
||||
|
||||
func typeString(vproto *pb.Value) string {
|
||||
switch vproto.ValueType.(type) {
|
||||
case *pb.Value_NullValue:
|
||||
return "null"
|
||||
case *pb.Value_BooleanValue:
|
||||
return "bool"
|
||||
case *pb.Value_IntegerValue:
|
||||
return "int"
|
||||
case *pb.Value_DoubleValue:
|
||||
return "float"
|
||||
case *pb.Value_TimestampValue:
|
||||
return "timestamp"
|
||||
case *pb.Value_StringValue:
|
||||
return "string"
|
||||
case *pb.Value_BytesValue:
|
||||
return "bytes"
|
||||
case *pb.Value_ReferenceValue:
|
||||
return "reference"
|
||||
case *pb.Value_GeoPointValue:
|
||||
return "GeoPoint"
|
||||
case *pb.Value_MapValue:
|
||||
return "map"
|
||||
case *pb.Value_ArrayValue:
|
||||
return "array"
|
||||
default:
|
||||
return "<unknown Value type>"
|
||||
}
|
||||
}
|
||||
|
||||
func overflowErr(v reflect.Value, x interface{}) error {
|
||||
return fmt.Errorf("firestore: value %v overflows type %s", x, v.Type())
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2022 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
|
||||
// Version is the current tagged release of the library.
|
||||
const Version = "1.18.0"
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright 2018 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
vkit "cloud.google.com/go/firestore/apiv1"
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"cloud.google.com/go/internal/trace"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
// DocumentRefIterator is an iterator over DocumentRefs.
|
||||
type DocumentRefIterator struct {
|
||||
client *Client
|
||||
it *vkit.DocumentIterator
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
items []*DocumentRef
|
||||
err error
|
||||
}
|
||||
|
||||
func newDocumentRefIterator(ctx context.Context, cr *CollectionRef, tid []byte, rs *readSettings) *DocumentRefIterator {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.ListDocuments")
|
||||
defer func() { trace.EndSpan(ctx, nil) }()
|
||||
|
||||
client := cr.c
|
||||
req := &pb.ListDocumentsRequest{
|
||||
Parent: cr.parentPath,
|
||||
CollectionId: cr.ID,
|
||||
ShowMissing: true,
|
||||
Mask: &pb.DocumentMask{}, // empty mask: we want only the ref
|
||||
}
|
||||
|
||||
// Transactions and ReadTime are mutually exclusive; Transactions should be
|
||||
// respected before read time.
|
||||
if rt, hasOpts := parseReadTime(client, rs); hasOpts {
|
||||
req.ConsistencySelector = &pb.ListDocumentsRequest_ReadTime{ReadTime: rt}
|
||||
}
|
||||
if tid != nil {
|
||||
req.ConsistencySelector = &pb.ListDocumentsRequest_Transaction{Transaction: tid}
|
||||
}
|
||||
it := &DocumentRefIterator{
|
||||
client: client,
|
||||
it: client.c.ListDocuments(withResourceHeader(ctx, client.path()), req),
|
||||
}
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(
|
||||
it.fetch,
|
||||
func() int { return len(it.items) },
|
||||
func() interface{} { b := it.items; it.items = nil; return b })
|
||||
return it
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
|
||||
func (it *DocumentRefIterator) PageInfo() *iterator.PageInfo { return it.pageInfo }
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there
|
||||
// are no more results. Once Next returns Done, all subsequent calls will return
|
||||
// Done.
|
||||
func (it *DocumentRefIterator) Next() (*DocumentRef, error) {
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *DocumentRefIterator) fetch(pageSize int, pageToken string) (string, error) {
|
||||
if it.err != nil {
|
||||
return "", it.err
|
||||
}
|
||||
return iterFetch(pageSize, pageToken, it.it.PageInfo(), func() error {
|
||||
docProto, err := it.it.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
docRef, err := pathToDoc(docProto.Name, it.client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
it.items = append(it.items, docRef)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetAll returns all the DocumentRefs remaining from the iterator.
|
||||
func (it *DocumentRefIterator) GetAll() ([]*DocumentRef, error) {
|
||||
var drs []*DocumentRef
|
||||
for {
|
||||
dr, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
drs = append(drs, dr)
|
||||
}
|
||||
return drs, nil
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
// Copyright 2017 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// A Precondition modifies a Firestore update or delete operation.
|
||||
type Precondition interface {
|
||||
// Returns the corresponding Precondition proto.
|
||||
preconditionProto() (*pb.Precondition, error)
|
||||
}
|
||||
|
||||
// Exists is a Precondition that checks for the existence of a resource before
|
||||
// writing to it. If the check fails, the write does not occur.
|
||||
var Exists Precondition
|
||||
|
||||
func init() {
|
||||
// Initialize here so godoc doesn't show the internal value.
|
||||
Exists = exists(true)
|
||||
}
|
||||
|
||||
type exists bool
|
||||
|
||||
func (e exists) preconditionProto() (*pb.Precondition, error) {
|
||||
return &pb.Precondition{
|
||||
ConditionType: &pb.Precondition_Exists{Exists: bool(e)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e exists) String() string {
|
||||
if e {
|
||||
return "Exists"
|
||||
}
|
||||
return "DoesNotExist"
|
||||
}
|
||||
|
||||
// LastUpdateTime returns a Precondition that checks that a resource must exist and
|
||||
// must have last been updated at the given time. If the check fails, the write
|
||||
// does not occur.
|
||||
func LastUpdateTime(t time.Time) Precondition { return lastUpdateTime(t) }
|
||||
|
||||
type lastUpdateTime time.Time
|
||||
|
||||
func (u lastUpdateTime) preconditionProto() (*pb.Precondition, error) {
|
||||
ts := timestamppb.New(time.Time(u))
|
||||
return &pb.Precondition{
|
||||
ConditionType: &pb.Precondition_UpdateTime{UpdateTime: ts},
|
||||
}, ts.CheckValid()
|
||||
}
|
||||
|
||||
func (u lastUpdateTime) String() string { return fmt.Sprintf("LastUpdateTime(%s)", time.Time(u)) }
|
||||
|
||||
func processPreconditionsForDelete(preconds []Precondition) (*pb.Precondition, error) {
|
||||
// At most one option permitted.
|
||||
switch len(preconds) {
|
||||
case 0:
|
||||
return nil, nil
|
||||
case 1:
|
||||
return preconds[0].preconditionProto()
|
||||
default:
|
||||
return nil, fmt.Errorf("firestore: conflicting preconditions: %+v", preconds)
|
||||
}
|
||||
}
|
||||
|
||||
func processPreconditionsForUpdate(preconds []Precondition) (*pb.Precondition, error) {
|
||||
// At most one option permitted, and it cannot be Exists.
|
||||
switch len(preconds) {
|
||||
case 0:
|
||||
// If the user doesn't provide any options, default to Exists(true).
|
||||
return exists(true).preconditionProto()
|
||||
case 1:
|
||||
if _, ok := preconds[0].(exists); ok {
|
||||
return nil, errors.New("cannot use Exists with Update")
|
||||
}
|
||||
return preconds[0].preconditionProto()
|
||||
default:
|
||||
return nil, fmt.Errorf("firestore: conflicting preconditions: %+v", preconds)
|
||||
}
|
||||
}
|
||||
|
||||
func processPreconditionsForVerify(preconds []Precondition) (*pb.Precondition, error) {
|
||||
// At most one option permitted.
|
||||
switch len(preconds) {
|
||||
case 0:
|
||||
return nil, nil
|
||||
case 1:
|
||||
return preconds[0].preconditionProto()
|
||||
default:
|
||||
return nil, fmt.Errorf("firestore: conflicting preconditions: %+v", preconds)
|
||||
}
|
||||
}
|
||||
|
||||
// A SetOption modifies a Firestore set operation.
|
||||
type SetOption interface {
|
||||
fieldPaths() (fps []FieldPath, all bool, err error)
|
||||
}
|
||||
|
||||
// MergeAll is a SetOption that causes all the field paths given in the data argument
|
||||
// to Set to be overwritten. It is not supported for struct data.
|
||||
var MergeAll SetOption = merge{all: true}
|
||||
|
||||
// Merge returns a SetOption that causes only the given field paths to be
|
||||
// overwritten. Other fields on the existing document will be untouched. It is an
|
||||
// error if a provided field path does not refer to a value in the data passed to
|
||||
// Set.
|
||||
func Merge(fps ...FieldPath) SetOption {
|
||||
for _, fp := range fps {
|
||||
if err := fp.validate(); err != nil {
|
||||
return merge{err: err}
|
||||
}
|
||||
}
|
||||
return merge{paths: fps}
|
||||
}
|
||||
|
||||
type merge struct {
|
||||
all bool
|
||||
paths []FieldPath
|
||||
err error
|
||||
}
|
||||
|
||||
func (m merge) String() string {
|
||||
if m.err != nil {
|
||||
return fmt.Sprintf("<Merge error: %v>", m.err)
|
||||
}
|
||||
if m.all {
|
||||
return "MergeAll"
|
||||
}
|
||||
return fmt.Sprintf("Merge(%+v)", m.paths)
|
||||
}
|
||||
|
||||
func (m merge) fieldPaths() (fps []FieldPath, all bool, err error) {
|
||||
if m.err != nil {
|
||||
return nil, false, m.err
|
||||
}
|
||||
if err := checkNoDupOrPrefix(m.paths); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if m.all {
|
||||
return nil, true, nil
|
||||
}
|
||||
return m.paths, false, nil
|
||||
}
|
||||
|
||||
func processSetOptions(opts []SetOption) (fps []FieldPath, all bool, err error) {
|
||||
switch len(opts) {
|
||||
case 0:
|
||||
return nil, false, nil
|
||||
case 1:
|
||||
return opts[0].fieldPaths()
|
||||
default:
|
||||
return nil, false, fmt.Errorf("conflicting options: %+v", opts)
|
||||
}
|
||||
}
|
||||
|
||||
type runQuerySettings struct {
|
||||
// Explain options for the query. If set, additional query
|
||||
// statistics will be returned. If not, only query results will be returned.
|
||||
explainOptions *pb.ExplainOptions
|
||||
}
|
||||
|
||||
// newRunQuerySettings creates a runQuerySettings with a given RunOption slice.
|
||||
func newRunQuerySettings(opts []RunOption) (*runQuerySettings, error) {
|
||||
s := &runQuerySettings{}
|
||||
for _, o := range opts {
|
||||
if o == nil {
|
||||
return nil, errors.New("firestore: RunOption cannot be nil")
|
||||
}
|
||||
err := o.apply(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// RunOption are options used while running a query
|
||||
type RunOption interface {
|
||||
apply(*runQuerySettings) error
|
||||
}
|
||||
|
||||
// ExplainOptions are Query Explain options.
|
||||
// Query Explain allows you to submit Cloud Firestore queries to the backend and
|
||||
// receive detailed performance statistics on backend query execution in return.
|
||||
type ExplainOptions struct {
|
||||
// When false (the default), Query Explain plans the query, but skips over the
|
||||
// execution stage. This will return planner stage information.
|
||||
//
|
||||
// When true, Query Explain both plans and executes the query. This returns all
|
||||
// the planner information along with statistics from the query execution runtime.
|
||||
// This will include the billing information of the query along with system-level
|
||||
// insights into the query execution.
|
||||
Analyze bool
|
||||
}
|
||||
|
||||
func (e ExplainOptions) apply(s *runQuerySettings) error {
|
||||
if s.explainOptions != nil {
|
||||
return errors.New("firestore: ExplainOptions can be specified only once")
|
||||
}
|
||||
pbExplainOptions := pb.ExplainOptions{Analyze: e.Analyze}
|
||||
s.explainOptions = &pbExplainOptions
|
||||
return nil
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
// Copyright 2018 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
tspb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// Returns a negative number, zero, or a positive number depending on whether a is
|
||||
// less than, equal to, or greater than b according to Firestore's ordering of
|
||||
// values.
|
||||
func compareValues(a, b *pb.Value) int {
|
||||
ta := typeOrder(a)
|
||||
tb := typeOrder(b)
|
||||
if ta != tb {
|
||||
return compareInt64s(int64(ta), int64(tb))
|
||||
}
|
||||
switch a := a.ValueType.(type) {
|
||||
case *pb.Value_NullValue:
|
||||
return 0 // nulls are equal
|
||||
|
||||
case *pb.Value_BooleanValue:
|
||||
av := a.BooleanValue
|
||||
bv := b.GetBooleanValue()
|
||||
switch {
|
||||
case av && !bv:
|
||||
return 1
|
||||
case bv && !av:
|
||||
return -1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
|
||||
case *pb.Value_IntegerValue:
|
||||
return compareNumbers(float64(a.IntegerValue), toFloat(b))
|
||||
|
||||
case *pb.Value_DoubleValue:
|
||||
return compareNumbers(a.DoubleValue, toFloat(b))
|
||||
|
||||
case *pb.Value_TimestampValue:
|
||||
return compareTimestamps(a.TimestampValue, b.GetTimestampValue())
|
||||
|
||||
case *pb.Value_StringValue:
|
||||
return strings.Compare(a.StringValue, b.GetStringValue())
|
||||
|
||||
case *pb.Value_BytesValue:
|
||||
return bytes.Compare(a.BytesValue, b.GetBytesValue())
|
||||
|
||||
case *pb.Value_ReferenceValue:
|
||||
return compareReferences(a.ReferenceValue, b.GetReferenceValue())
|
||||
|
||||
case *pb.Value_GeoPointValue:
|
||||
ag := a.GeoPointValue
|
||||
bg := b.GetGeoPointValue()
|
||||
if ag.Latitude != bg.Latitude {
|
||||
return compareFloat64s(ag.Latitude, bg.Latitude)
|
||||
}
|
||||
return compareFloat64s(ag.Longitude, bg.Longitude)
|
||||
|
||||
case *pb.Value_ArrayValue:
|
||||
return compareArrays(a.ArrayValue.Values, b.GetArrayValue().Values)
|
||||
|
||||
case *pb.Value_MapValue:
|
||||
return compareMaps(a.MapValue.Fields, b.GetMapValue().Fields)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("bad value type: %v", a))
|
||||
}
|
||||
}
|
||||
|
||||
// Treats NaN as less than any non-NaN.
|
||||
func compareNumbers(a, b float64) int {
|
||||
switch {
|
||||
case math.IsNaN(a):
|
||||
if math.IsNaN(b) {
|
||||
return 0
|
||||
}
|
||||
return -1
|
||||
case math.IsNaN(b):
|
||||
return 1
|
||||
default:
|
||||
return compareFloat64s(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
// Return v as a float64, assuming it's an Integer or Double.
|
||||
func toFloat(v *pb.Value) float64 {
|
||||
if x, ok := v.ValueType.(*pb.Value_IntegerValue); ok {
|
||||
return float64(x.IntegerValue)
|
||||
}
|
||||
return v.GetDoubleValue()
|
||||
}
|
||||
|
||||
func compareTimestamps(a, b *tspb.Timestamp) int {
|
||||
if c := compareInt64s(a.Seconds, b.Seconds); c != 0 {
|
||||
return c
|
||||
}
|
||||
return compareInt64s(int64(a.Nanos), int64(b.Nanos))
|
||||
}
|
||||
|
||||
func compareReferences(a, b string) int {
|
||||
// Compare path components lexicographically.
|
||||
pa := strings.Split(a, "/")
|
||||
pb := strings.Split(b, "/")
|
||||
return compareSequences(len(pa), len(pb), func(i int) int {
|
||||
return strings.Compare(pa[i], pb[i])
|
||||
})
|
||||
}
|
||||
|
||||
func compareArrays(a, b []*pb.Value) int {
|
||||
return compareSequences(len(a), len(b), func(i int) int {
|
||||
return compareValues(a[i], b[i])
|
||||
})
|
||||
}
|
||||
|
||||
func compareMaps(a, b map[string]*pb.Value) int {
|
||||
sortedKeys := func(m map[string]*pb.Value) []string {
|
||||
var ks []string
|
||||
for k := range m {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
sort.Strings(ks)
|
||||
return ks
|
||||
}
|
||||
|
||||
aks := sortedKeys(a)
|
||||
bks := sortedKeys(b)
|
||||
return compareSequences(len(aks), len(bks), func(i int) int {
|
||||
if c := strings.Compare(aks[i], bks[i]); c != 0 {
|
||||
return c
|
||||
}
|
||||
k := aks[i]
|
||||
return compareValues(a[k], b[k])
|
||||
})
|
||||
}
|
||||
|
||||
func compareSequences(len1, len2 int, compare func(int) int) int {
|
||||
for i := 0; i < len1 && i < len2; i++ {
|
||||
if c := compare(i); c != 0 {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return compareInt64s(int64(len1), int64(len2))
|
||||
}
|
||||
|
||||
func compareFloat64s(a, b float64) int {
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func compareInt64s(a, b int64) int {
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Return an integer corresponding to the type of value stored in v, such that
|
||||
// comparing the resulting integers gives the Firestore ordering for types.
|
||||
func typeOrder(v *pb.Value) int {
|
||||
switch v.ValueType.(type) {
|
||||
case *pb.Value_NullValue:
|
||||
return 0
|
||||
case *pb.Value_BooleanValue:
|
||||
return 1
|
||||
case *pb.Value_IntegerValue:
|
||||
return 2
|
||||
case *pb.Value_DoubleValue:
|
||||
return 2
|
||||
case *pb.Value_TimestampValue:
|
||||
return 3
|
||||
case *pb.Value_StringValue:
|
||||
return 4
|
||||
case *pb.Value_BytesValue:
|
||||
return 5
|
||||
case *pb.Value_ReferenceValue:
|
||||
return 6
|
||||
case *pb.Value_GeoPointValue:
|
||||
return 7
|
||||
case *pb.Value_ArrayValue:
|
||||
return 8
|
||||
case *pb.Value_MapValue:
|
||||
return 9
|
||||
default:
|
||||
panic(fmt.Sprintf("bad value type: %v", v))
|
||||
}
|
||||
}
|
||||
|
||||
// byReferenceValue implements sort.Interface for []*firestorepb.Value
|
||||
type byFirestoreValue []*pb.Value
|
||||
|
||||
func (a byFirestoreValue) Len() int { return len(a) }
|
||||
func (a byFirestoreValue) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byFirestoreValue) Less(i, j int) bool { return compareValues(a[i], a[j]) < 0 }
|
||||
+1756
File diff suppressed because it is too large
Load Diff
+316
@@ -0,0 +1,316 @@
|
||||
// Copyright 2017 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"cloud.google.com/go/internal/fields"
|
||||
"google.golang.org/genproto/googleapis/type/latlng"
|
||||
ts "google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
var nullValue = &pb.Value{ValueType: &pb.Value_NullValue{}}
|
||||
|
||||
var (
|
||||
typeOfByteSlice = reflect.TypeOf([]byte{})
|
||||
typeOfGoTime = reflect.TypeOf(time.Time{})
|
||||
typeOfLatLng = reflect.TypeOf((*latlng.LatLng)(nil))
|
||||
typeOfDocumentRef = reflect.TypeOf((*DocumentRef)(nil))
|
||||
typeOfProtoTimestamp = reflect.TypeOf((*ts.Timestamp)(nil))
|
||||
typeOfVector64 = reflect.TypeOf(Vector64{})
|
||||
typeOfVector32 = reflect.TypeOf(Vector32{})
|
||||
)
|
||||
|
||||
// toProtoValue converts a Go value to a Firestore Value protobuf.
|
||||
// Some corner cases:
|
||||
// - All nils (nil interface, nil slice, nil map, nil pointer) are converted to
|
||||
// a NullValue (not a nil *pb.Value). toProtoValue never returns (nil, false, nil).
|
||||
// It returns (nil, true, nil) if everything in the value is ServerTimestamp.
|
||||
// - An error is returned for uintptr, uint, and uint64, because Firestore uses
|
||||
// an int64 to represent integral values, and those types can't be properly
|
||||
// represented in an int64.
|
||||
// - An error is returned for the special Delete value.
|
||||
//
|
||||
// toProtoValue also reports whether it recursively encountered a transform.
|
||||
func toProtoValue(v reflect.Value) (pbv *pb.Value, sawTransform bool, err error) {
|
||||
if !v.IsValid() {
|
||||
return nullValue, false, nil
|
||||
}
|
||||
vi := v.Interface()
|
||||
if vi == Delete {
|
||||
return nil, false, errors.New("firestore: cannot use Delete in value")
|
||||
}
|
||||
if vi == ServerTimestamp {
|
||||
return nil, false, errors.New("firestore: must use ServerTimestamp as a map value")
|
||||
}
|
||||
switch x := vi.(type) {
|
||||
case []byte:
|
||||
return &pb.Value{ValueType: &pb.Value_BytesValue{BytesValue: x}}, false, nil
|
||||
case time.Time:
|
||||
return &pb.Value{ValueType: &pb.Value_TimestampValue{TimestampValue: ts.New(x)}}, false, nil
|
||||
case *ts.Timestamp:
|
||||
if x == nil {
|
||||
// gRPC doesn't like nil oneofs. Use NullValue.
|
||||
return nullValue, false, nil
|
||||
}
|
||||
return &pb.Value{ValueType: &pb.Value_TimestampValue{TimestampValue: x}}, false, nil
|
||||
case Vector32:
|
||||
return vectorToProtoValue(x), false, nil
|
||||
case Vector64:
|
||||
return vectorToProtoValue(x), false, nil
|
||||
case *latlng.LatLng:
|
||||
if x == nil {
|
||||
// gRPC doesn't like nil oneofs. Use NullValue.
|
||||
return nullValue, false, nil
|
||||
}
|
||||
return &pb.Value{ValueType: &pb.Value_GeoPointValue{GeoPointValue: x}}, false, nil
|
||||
case *DocumentRef:
|
||||
if x == nil {
|
||||
// gRPC doesn't like nil oneofs. Use NullValue.
|
||||
return nullValue, false, nil
|
||||
}
|
||||
return &pb.Value{ValueType: &pb.Value_ReferenceValue{ReferenceValue: x.Path}}, false, nil
|
||||
// Do not add bool, string, int, etc. to this switch; leave them in the
|
||||
// reflect-based switch below. Moving them here would drop support for
|
||||
// types whose underlying types are those primitives.
|
||||
// E.g. Given "type mybool bool", an ordinary type switch on bool will
|
||||
// not catch a mybool, but the reflect.Kind of a mybool is reflect.Bool.
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
return &pb.Value{ValueType: &pb.Value_BooleanValue{BooleanValue: v.Bool()}}, false, nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return &pb.Value{ValueType: &pb.Value_IntegerValue{IntegerValue: v.Int()}}, false, nil
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
||||
return &pb.Value{ValueType: &pb.Value_IntegerValue{IntegerValue: int64(v.Uint())}}, false, nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return floatToProtoValue(v.Float()), false, nil
|
||||
case reflect.String:
|
||||
return stringToProtoValue(v.String()), false, nil
|
||||
case reflect.Array:
|
||||
return arrayToProtoValue(v)
|
||||
case reflect.Slice:
|
||||
return sliceToProtoValue(v)
|
||||
case reflect.Map:
|
||||
return mapToProtoValue(v)
|
||||
case reflect.Struct:
|
||||
return structToProtoValue(v)
|
||||
case reflect.Ptr:
|
||||
if v.IsNil() {
|
||||
return nullValue, false, nil
|
||||
}
|
||||
return toProtoValue(v.Elem())
|
||||
case reflect.Interface:
|
||||
if v.NumMethod() == 0 { // empty interface: recurse on its contents
|
||||
return toProtoValue(v.Elem())
|
||||
}
|
||||
fallthrough // any other interface value is an error
|
||||
|
||||
default:
|
||||
return nil, false, fmt.Errorf("firestore: cannot convert type %s to value", v.Type())
|
||||
}
|
||||
}
|
||||
|
||||
func stringToProtoValue(s string) *pb.Value {
|
||||
return &pb.Value{ValueType: &pb.Value_StringValue{StringValue: s}}
|
||||
}
|
||||
|
||||
func floatToProtoValue(f float64) *pb.Value {
|
||||
return &pb.Value{ValueType: &pb.Value_DoubleValue{DoubleValue: f}}
|
||||
}
|
||||
|
||||
// arrayToProtoValue converts a array to a Firestore Value protobuf and reports
|
||||
// whether a transform was encountered.
|
||||
func arrayToProtoValue(v reflect.Value) (*pb.Value, bool, error) {
|
||||
vals := make([]*pb.Value, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
val, sawTransform, err := toProtoValue(v.Index(i))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if sawTransform {
|
||||
return nil, false, fmt.Errorf("firestore: transforms cannot occur in an array, but saw some in %v", v.Index(i))
|
||||
}
|
||||
vals[i] = val
|
||||
}
|
||||
return &pb.Value{ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{Values: vals}}}, false, nil
|
||||
}
|
||||
|
||||
// sliceToProtoValue converts a slice to a Firestore Value protobuf and reports
|
||||
// whether a transform was encountered.
|
||||
func sliceToProtoValue(v reflect.Value) (*pb.Value, bool, error) {
|
||||
// A nil slice is converted to a null value.
|
||||
if v.IsNil() {
|
||||
return nullValue, false, nil
|
||||
}
|
||||
return arrayToProtoValue(v)
|
||||
}
|
||||
|
||||
// mapToProtoValue converts a map to a Firestore Value protobuf and reports whether
|
||||
// a transform was encountered.
|
||||
func mapToProtoValue(v reflect.Value) (*pb.Value, bool, error) {
|
||||
if v.Type().Key().Kind() != reflect.String {
|
||||
return nil, false, errors.New("firestore: map key type must be string")
|
||||
}
|
||||
// A nil map is converted to a null value.
|
||||
if v.IsNil() {
|
||||
return nullValue, false, nil
|
||||
}
|
||||
m := map[string]*pb.Value{}
|
||||
sawTransform := false
|
||||
for _, k := range v.MapKeys() {
|
||||
mi := v.MapIndex(k)
|
||||
if mi.Interface() == ServerTimestamp {
|
||||
sawTransform = true
|
||||
continue
|
||||
} else if _, ok := mi.Interface().(arrayUnion); ok {
|
||||
sawTransform = true
|
||||
continue
|
||||
} else if _, ok := mi.Interface().(arrayRemove); ok {
|
||||
sawTransform = true
|
||||
continue
|
||||
}
|
||||
val, sst, err := toProtoValue(mi)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if sst {
|
||||
sawTransform = true
|
||||
}
|
||||
if val == nil { // value was a map with all ServerTimestamp values
|
||||
continue
|
||||
}
|
||||
m[k.String()] = val
|
||||
}
|
||||
var pv *pb.Value
|
||||
if len(m) == 0 && sawTransform {
|
||||
// The entire map consisted of transform values.
|
||||
pv = nil
|
||||
} else {
|
||||
pv = &pb.Value{ValueType: &pb.Value_MapValue{MapValue: &pb.MapValue{Fields: m}}}
|
||||
}
|
||||
return pv, sawTransform, nil
|
||||
}
|
||||
|
||||
// structToProtoValue converts a struct to a Firestore Value protobuf and reports
|
||||
// whether a transform was encountered.
|
||||
func structToProtoValue(v reflect.Value) (*pb.Value, bool, error) {
|
||||
m := map[string]*pb.Value{}
|
||||
fields, err := fieldCache.Fields(v.Type())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
sawTransform := false
|
||||
if _, ok := v.Interface().(arrayUnion); ok {
|
||||
return nil, false, errors.New("firestore: ArrayUnion may not be used in structs")
|
||||
}
|
||||
if _, ok := v.Interface().(arrayRemove); ok {
|
||||
return nil, false, errors.New("firestore: ArrayRemove may not be used in structs")
|
||||
}
|
||||
|
||||
for _, f := range fields {
|
||||
fv := v.FieldByIndex(f.Index)
|
||||
opts := f.ParsedTag.(tagOptions)
|
||||
if opts.serverTimestamp {
|
||||
// TODO(jba): should we return a non-zero time?
|
||||
sawTransform = true
|
||||
continue
|
||||
}
|
||||
if opts.omitEmpty && isEmptyValue(fv) {
|
||||
continue
|
||||
}
|
||||
val, sst, err := toProtoValue(fv)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if sst {
|
||||
sawTransform = true
|
||||
}
|
||||
if val == nil { // value was a map with all ServerTimestamp values
|
||||
continue
|
||||
}
|
||||
m[f.Name] = val
|
||||
}
|
||||
var pv *pb.Value
|
||||
if len(m) == 0 && sawTransform {
|
||||
// The entire struct consisted of ServerTimestamp or omitempty values.
|
||||
pv = nil
|
||||
} else {
|
||||
pv = &pb.Value{ValueType: &pb.Value_MapValue{MapValue: &pb.MapValue{Fields: m}}}
|
||||
}
|
||||
return pv, sawTransform, nil
|
||||
}
|
||||
|
||||
type tagOptions struct {
|
||||
omitEmpty bool // do not marshal value if empty
|
||||
serverTimestamp bool // set time.Time to server timestamp on write
|
||||
}
|
||||
|
||||
// parseTag interprets firestore struct field tags.
|
||||
func parseTag(t reflect.StructTag) (name string, keep bool, other interface{}, err error) {
|
||||
name, keep, opts, err := fields.ParseStandardTag("firestore", t)
|
||||
if err != nil {
|
||||
return "", false, nil, fmt.Errorf("firestore: %w", err)
|
||||
}
|
||||
tagOpts := tagOptions{}
|
||||
for _, opt := range opts {
|
||||
switch opt {
|
||||
case "omitempty":
|
||||
tagOpts.omitEmpty = true
|
||||
case "serverTimestamp":
|
||||
tagOpts.serverTimestamp = true
|
||||
default:
|
||||
return "", false, nil, fmt.Errorf("firestore: unknown tag option: %q", opt)
|
||||
}
|
||||
}
|
||||
return name, keep, tagOpts, nil
|
||||
}
|
||||
|
||||
// isLeafType determines whether or not a type is a 'leaf type'
|
||||
// and should not be recursed into, but considered one field.
|
||||
func isLeafType(t reflect.Type) bool {
|
||||
return t == typeOfGoTime || t == typeOfLatLng || t == typeOfProtoTimestamp
|
||||
}
|
||||
|
||||
var fieldCache = fields.NewCache(parseTag, nil, isLeafType)
|
||||
|
||||
// isEmptyValue is taken from the encoding/json package in the
|
||||
// standard library.
|
||||
// TODO(jba): move to the fields package
|
||||
func isEmptyValue(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||
return v.Len() == 0
|
||||
case reflect.Bool:
|
||||
return !v.Bool()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return v.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float() == 0
|
||||
case reflect.Interface, reflect.Ptr:
|
||||
return v.IsNil()
|
||||
}
|
||||
if v.Type() == typeOfGoTime {
|
||||
return v.Interface().(time.Time).IsZero()
|
||||
}
|
||||
return false
|
||||
}
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
// Copyright 2017 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"cloud.google.com/go/internal/trace"
|
||||
gax "github.com/googleapis/gax-go/v2"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// Transaction represents a Firestore transaction.
|
||||
type Transaction struct {
|
||||
c *Client
|
||||
ctx context.Context
|
||||
id []byte
|
||||
writes []*pb.Write
|
||||
maxAttempts int
|
||||
readOnly bool
|
||||
readAfterWrite bool
|
||||
readSettings *readSettings
|
||||
explainOptions *ExplainOptions
|
||||
}
|
||||
|
||||
// A TransactionOption is an option passed to Client.Transaction.
|
||||
type TransactionOption interface {
|
||||
config(t *Transaction)
|
||||
handleCommitResponse(r *pb.CommitResponse)
|
||||
}
|
||||
|
||||
// MaxAttempts is a TransactionOption that configures the maximum number of times to
|
||||
// try a transaction. In defaults to DefaultTransactionMaxAttempts.
|
||||
func MaxAttempts(n int) maxAttempts { return maxAttempts(n) }
|
||||
|
||||
type maxAttempts int
|
||||
|
||||
func (m maxAttempts) config(t *Transaction) { t.maxAttempts = int(m) }
|
||||
func (m maxAttempts) handleCommitResponse(r *pb.CommitResponse) {}
|
||||
|
||||
// DefaultTransactionMaxAttempts is the default number of times to attempt a transaction.
|
||||
const DefaultTransactionMaxAttempts = 5
|
||||
|
||||
// ReadOnly is a TransactionOption that makes the transaction read-only. Read-only
|
||||
// transactions cannot issue write operations, but are more efficient.
|
||||
var ReadOnly = ro{}
|
||||
|
||||
type ro struct{}
|
||||
|
||||
func (ro) config(t *Transaction) { t.readOnly = true }
|
||||
func (ro) handleCommitResponse(r *pb.CommitResponse) {}
|
||||
|
||||
// CommitResponse exposes information about a committed transaction.
|
||||
type CommitResponse struct {
|
||||
response *pb.CommitResponse
|
||||
}
|
||||
|
||||
// CommitTime returns the commit time from the commit response.
|
||||
func (r *CommitResponse) CommitTime() time.Time {
|
||||
return r.response.CommitTime.AsTime()
|
||||
}
|
||||
|
||||
// commitResponse is the TransactionOption to record a commit response.
|
||||
type commitResponse struct {
|
||||
responseTo *CommitResponse
|
||||
}
|
||||
|
||||
func (c commitResponse) config(t *Transaction) {}
|
||||
func (c commitResponse) handleCommitResponse(r *pb.CommitResponse) {
|
||||
c.responseTo.response = r
|
||||
}
|
||||
|
||||
// WithCommitResponseTo returns a TransactionOption that specifies where the
|
||||
// CommitResponse should be written on successful commit. Nothing is written
|
||||
// on a failed commit.
|
||||
func WithCommitResponseTo(r *CommitResponse) commitResponse {
|
||||
return commitResponse{responseTo: r}
|
||||
}
|
||||
|
||||
var (
|
||||
// Defined here for testing.
|
||||
errReadAfterWrite = errors.New("firestore: read after write in transaction")
|
||||
errWriteReadOnly = errors.New("firestore: write in read-only transaction")
|
||||
errNestedTransaction = errors.New("firestore: nested transaction")
|
||||
)
|
||||
|
||||
type transactionInProgressKey struct{}
|
||||
|
||||
// RunTransaction runs f in a transaction. f should use the transaction it is given
|
||||
// for all Firestore operations. For any operation requiring a context, f should use
|
||||
// the context it is passed, not the first argument to RunTransaction.
|
||||
//
|
||||
// f must not call Commit or Rollback on the provided Transaction.
|
||||
//
|
||||
// If f returns nil, RunTransaction commits the transaction. If the commit fails due
|
||||
// to a conflicting transaction, RunTransaction retries f. It gives up and returns an
|
||||
// error after a number of attempts that can be configured with the MaxAttempts
|
||||
// option. If the commit succeeds, RunTransaction returns a nil error.
|
||||
//
|
||||
// If f returns non-nil, then the transaction will be rolled back and
|
||||
// this method will return the same error. The function f is not retried.
|
||||
//
|
||||
// Note that when f returns, the transaction is not committed. Calling code
|
||||
// must not assume that any of f's changes have been committed until
|
||||
// RunTransaction returns nil.
|
||||
//
|
||||
// Since f may be called more than once, f should usually be idempotent – that is, it
|
||||
// should have the same result when called multiple times.
|
||||
func (c *Client) RunTransaction(ctx context.Context, f func(context.Context, *Transaction) error, opts ...TransactionOption) (err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.Client.RunTransaction")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
if ctx.Value(transactionInProgressKey{}) != nil {
|
||||
return errNestedTransaction
|
||||
}
|
||||
db := c.path()
|
||||
t := &Transaction{
|
||||
c: c,
|
||||
ctx: withResourceHeader(ctx, db),
|
||||
maxAttempts: DefaultTransactionMaxAttempts,
|
||||
readSettings: &readSettings{},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.config(t)
|
||||
}
|
||||
var txOpts *pb.TransactionOptions
|
||||
if t.readOnly {
|
||||
txOpts = &pb.TransactionOptions{
|
||||
Mode: &pb.TransactionOptions_ReadOnly_{ReadOnly: &pb.TransactionOptions_ReadOnly{}},
|
||||
}
|
||||
}
|
||||
var backoff gax.Backoff
|
||||
var commitResponse *pb.CommitResponse
|
||||
// TODO(jba): use other than the standard backoff parameters?
|
||||
// TODO(jba): get backoff time from gRPC trailer metadata? See
|
||||
// extractRetryDelay in https://code.googlesource.com/gocloud/+/master/spanner/retry.go.
|
||||
for i := 0; i < t.maxAttempts; i++ {
|
||||
t.ctx = trace.StartSpan(t.ctx, "cloud.google.com/go/firestore.Client.BeginTransaction")
|
||||
var res *pb.BeginTransactionResponse
|
||||
res, err = t.c.c.BeginTransaction(t.ctx, &pb.BeginTransactionRequest{
|
||||
Database: db,
|
||||
Options: txOpts,
|
||||
})
|
||||
trace.EndSpan(t.ctx, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.id = res.Transaction
|
||||
err = f(context.WithValue(ctx, transactionInProgressKey{}, 1), t)
|
||||
// Read after write can only be checked client-side, so we make sure to check
|
||||
// even if the user does not.
|
||||
if err == nil && t.readAfterWrite {
|
||||
err = errReadAfterWrite
|
||||
}
|
||||
if err != nil {
|
||||
t.rollback()
|
||||
// Prefer f's returned error to rollback error.
|
||||
return err
|
||||
}
|
||||
t.ctx = trace.StartSpan(t.ctx, "cloud.google.com/go/firestore.Client.Commit")
|
||||
commitResponse, err = t.c.c.Commit(t.ctx, &pb.CommitRequest{
|
||||
Database: t.c.path(),
|
||||
Writes: t.writes,
|
||||
Transaction: t.id,
|
||||
})
|
||||
trace.EndSpan(t.ctx, err)
|
||||
|
||||
// on success, handle the commit response
|
||||
if err == nil {
|
||||
for _, opt := range opts {
|
||||
opt.handleCommitResponse(commitResponse)
|
||||
}
|
||||
}
|
||||
|
||||
// If a read-write transaction returns Aborted, retry.
|
||||
// On success or other failures, return here.
|
||||
if t.readOnly || status.Code(err) != codes.Aborted {
|
||||
// According to the Firestore team, we should not roll back here
|
||||
// if err != nil. But spanner does.
|
||||
// See https://code.googlesource.com/gocloud/+/master/spanner/transaction.go#740.
|
||||
return err
|
||||
}
|
||||
|
||||
if txOpts == nil {
|
||||
// txOpts can only be nil if is the first retry of a read-write transaction.
|
||||
// (It is only set here and in the body of "if t.readOnly" above.)
|
||||
// Mention the transaction ID in BeginTransaction so the service
|
||||
// knows it is a retry.
|
||||
txOpts = &pb.TransactionOptions{
|
||||
Mode: &pb.TransactionOptions_ReadWrite_{
|
||||
ReadWrite: &pb.TransactionOptions_ReadWrite{RetryTransaction: t.id},
|
||||
},
|
||||
}
|
||||
}
|
||||
// Use exponential backoff to avoid contention with other running
|
||||
// transactions.
|
||||
if cerr := sleep(ctx, backoff.Pause()); cerr != nil {
|
||||
err = cerr
|
||||
break
|
||||
}
|
||||
|
||||
// Reset state for the next attempt.
|
||||
t.writes = nil
|
||||
}
|
||||
// If we run out of retries, return the last error we saw (which should
|
||||
// be the Aborted from Commit, or a context error).
|
||||
if err != nil {
|
||||
t.rollback()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Transaction) rollback() {
|
||||
_ = t.c.c.Rollback(t.ctx, &pb.RollbackRequest{
|
||||
Database: t.c.path(),
|
||||
Transaction: t.id,
|
||||
})
|
||||
// Ignore the rollback error.
|
||||
// TODO(jba): Log it?
|
||||
// Note: Rollback is idempotent so it will be retried by the gapic layer.
|
||||
}
|
||||
|
||||
// Get gets the document in the context of the transaction. The transaction holds a
|
||||
// pessimistic lock on the returned document. If the document does not exist, Get
|
||||
// returns a NotFound error, which can be checked with
|
||||
//
|
||||
// status.Code(err) == codes.NotFound
|
||||
func (t *Transaction) Get(dr *DocumentRef) (*DocumentSnapshot, error) {
|
||||
docsnaps, err := t.GetAll([]*DocumentRef{dr})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ds := docsnaps[0]
|
||||
if !ds.Exists() {
|
||||
return ds, status.Errorf(codes.NotFound, "%q not found", dr.Path)
|
||||
}
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// GetAll retrieves multiple documents with a single call. The DocumentSnapshots are
|
||||
// returned in the order of the given DocumentRefs. If a document is not present, the
|
||||
// corresponding DocumentSnapshot's Exists method will return false. The transaction
|
||||
// holds a pessimistic lock on all of the returned documents.
|
||||
func (t *Transaction) GetAll(drs []*DocumentRef) ([]*DocumentSnapshot, error) {
|
||||
if len(t.writes) > 0 {
|
||||
t.readAfterWrite = true
|
||||
return nil, errReadAfterWrite
|
||||
}
|
||||
return t.c.getAll(t.ctx, drs, t.id, t.readSettings)
|
||||
}
|
||||
|
||||
// A Queryer is a Query or a CollectionRef. CollectionRefs act as queries whose
|
||||
// results are all the documents in the collection.
|
||||
type Queryer interface {
|
||||
query() *Query
|
||||
}
|
||||
|
||||
// Documents returns a DocumentIterator based on given Query or CollectionRef. The
|
||||
// results will be in the context of the transaction.
|
||||
func (t *Transaction) Documents(q Queryer) *DocumentIterator {
|
||||
if len(t.writes) > 0 {
|
||||
t.readAfterWrite = true
|
||||
return &DocumentIterator{err: errReadAfterWrite}
|
||||
}
|
||||
query := q.query()
|
||||
return &DocumentIterator{
|
||||
iter: newQueryDocumentIterator(t.ctx, query, t.id, t.readSettings), q: query,
|
||||
}
|
||||
}
|
||||
|
||||
// DocumentRefs returns references to all the documents in the collection, including
|
||||
// missing documents. A missing document is a document that does not exist but has
|
||||
// sub-documents.
|
||||
func (t *Transaction) DocumentRefs(cr *CollectionRef) *DocumentRefIterator {
|
||||
if len(t.writes) > 0 {
|
||||
t.readAfterWrite = true
|
||||
return &DocumentRefIterator{err: errReadAfterWrite}
|
||||
}
|
||||
return newDocumentRefIterator(t.ctx, cr, t.id, t.readSettings)
|
||||
}
|
||||
|
||||
// Create adds a Create operation to the Transaction.
|
||||
// See DocumentRef.Create for details.
|
||||
func (t *Transaction) Create(dr *DocumentRef, data interface{}) error {
|
||||
return t.addWrites(dr.newCreateWrites(data))
|
||||
}
|
||||
|
||||
// Set adds a Set operation to the Transaction.
|
||||
// See DocumentRef.Set for details.
|
||||
func (t *Transaction) Set(dr *DocumentRef, data interface{}, opts ...SetOption) error {
|
||||
return t.addWrites(dr.newSetWrites(data, opts))
|
||||
}
|
||||
|
||||
// Delete adds a Delete operation to the Transaction.
|
||||
// See DocumentRef.Delete for details.
|
||||
func (t *Transaction) Delete(dr *DocumentRef, opts ...Precondition) error {
|
||||
return t.addWrites(dr.newDeleteWrites(opts))
|
||||
}
|
||||
|
||||
// Update adds a new Update operation to the Transaction.
|
||||
// See DocumentRef.Update for details.
|
||||
func (t *Transaction) Update(dr *DocumentRef, data []Update, opts ...Precondition) error {
|
||||
return t.addWrites(dr.newUpdatePathWrites(data, opts))
|
||||
}
|
||||
|
||||
func (t *Transaction) addWrites(ws []*pb.Write, err error) error {
|
||||
if t.readOnly {
|
||||
return errWriteReadOnly
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.writes = append(t.writes, ws...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithReadOptions specifies constraints for accessing documents from the database,
|
||||
// e.g. at what time snapshot to read the documents.
|
||||
func (t *Transaction) WithReadOptions(opts ...ReadOption) *Transaction {
|
||||
for _, ro := range opts {
|
||||
ro.apply(t.readSettings)
|
||||
}
|
||||
return t
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
// Copyright 2024 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
)
|
||||
|
||||
const (
|
||||
typeKey = "__type__"
|
||||
typeValVector = "__vector__"
|
||||
valueKey = "value"
|
||||
)
|
||||
|
||||
// Vector64 is an embedding vector of float64s.
|
||||
type Vector64 []float64
|
||||
|
||||
// Vector32 is an embedding vector of float32s.
|
||||
type Vector32 []float32
|
||||
|
||||
// vectorToProtoValue returns a Firestore [pb.Value] representing the Vector.
|
||||
func vectorToProtoValue[T float32 | float64](v []T) *pb.Value {
|
||||
if v == nil {
|
||||
return nullValue
|
||||
}
|
||||
pbVals := make([]*pb.Value, len(v))
|
||||
for i, val := range v {
|
||||
pbVals[i] = floatToProtoValue(float64(val))
|
||||
}
|
||||
|
||||
return &pb.Value{
|
||||
ValueType: &pb.Value_MapValue{
|
||||
MapValue: &pb.MapValue{
|
||||
Fields: map[string]*pb.Value{
|
||||
typeKey: stringToProtoValue(typeValVector),
|
||||
valueKey: {
|
||||
ValueType: &pb.Value_ArrayValue{
|
||||
ArrayValue: &pb.ArrayValue{Values: pbVals},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func vector32FromProtoValue(v *pb.Value) (Vector32, error) {
|
||||
return vectorFromProtoValue[float32](v)
|
||||
}
|
||||
|
||||
func vector64FromProtoValue(v *pb.Value) (Vector64, error) {
|
||||
return vectorFromProtoValue[float64](v)
|
||||
}
|
||||
|
||||
func vectorFromProtoValue[T float32 | float64](v *pb.Value) ([]T, error) {
|
||||
pbArrVals, err := pbValToVectorVals(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
floats := make([]T, len(pbArrVals))
|
||||
for i, fval := range pbArrVals {
|
||||
dv, ok := fval.ValueType.(*pb.Value_DoubleValue)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("firestore: failed to convert %v to *pb.Value_DoubleValue", fval.ValueType)
|
||||
}
|
||||
floats[i] = T(dv.DoubleValue)
|
||||
}
|
||||
return floats, nil
|
||||
}
|
||||
|
||||
func pbValToVectorVals(v *pb.Value) ([]*pb.Value, error) {
|
||||
/*
|
||||
Vector is stored as:
|
||||
{
|
||||
"__type__": "__vector__",
|
||||
"value": []float64{},
|
||||
}
|
||||
*/
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
pbMap, ok := v.ValueType.(*pb.Value_MapValue)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("firestore: cannot convert %v to *pb.Value_MapValue", v.ValueType)
|
||||
}
|
||||
m := pbMap.MapValue.Fields
|
||||
var typeVal string
|
||||
typeVal, err := stringFromProtoValue(m[typeKey])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if typeVal != typeValVector {
|
||||
return nil, fmt.Errorf("firestore: value of %v : %v is not %v", typeKey, typeVal, typeValVector)
|
||||
}
|
||||
pbVal, ok := m[valueKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("firestore: %v not present in %v", valueKey, m)
|
||||
}
|
||||
|
||||
pbArr, ok := pbVal.ValueType.(*pb.Value_ArrayValue)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("firestore: failed to convert %v to *pb.Value_ArrayValue", pbVal.ValueType)
|
||||
}
|
||||
|
||||
return pbArr.ArrayValue.Values, nil
|
||||
}
|
||||
|
||||
func stringFromProtoValue(v *pb.Value) (string, error) {
|
||||
if v == nil {
|
||||
return "", fmt.Errorf("firestore: failed to convert %v to string", v)
|
||||
}
|
||||
sv, ok := v.ValueType.(*pb.Value_StringValue)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("firestore: failed to convert %v to *pb.Value_StringValue", v.ValueType)
|
||||
}
|
||||
return sv.StringValue, nil
|
||||
}
|
||||
+524
@@ -0,0 +1,524 @@
|
||||
// Copyright 2018 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"cloud.google.com/go/internal/btree"
|
||||
"cloud.google.com/go/internal/trace"
|
||||
gax "github.com/googleapis/gax-go/v2"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// LogWatchStreams controls whether watch stream status changes are logged.
|
||||
// This feature is EXPERIMENTAL and may disappear at any time.
|
||||
var LogWatchStreams = false
|
||||
|
||||
// DocumentChangeKind describes the kind of change to a document between
|
||||
// query snapshots.
|
||||
type DocumentChangeKind int
|
||||
|
||||
const (
|
||||
// DocumentAdded indicates that the document was added for the first time.
|
||||
DocumentAdded DocumentChangeKind = iota
|
||||
// DocumentRemoved indicates that the document was removed.
|
||||
DocumentRemoved
|
||||
// DocumentModified indicates that the document was modified.
|
||||
DocumentModified
|
||||
)
|
||||
|
||||
// A DocumentChange describes the change to a document from one query snapshot to the next.
|
||||
type DocumentChange struct {
|
||||
Kind DocumentChangeKind
|
||||
Doc *DocumentSnapshot
|
||||
// The zero-based index of the document in the sequence of query results prior to this change,
|
||||
// or -1 if the document was not present.
|
||||
OldIndex int
|
||||
// The zero-based index of the document in the sequence of query results after this change,
|
||||
// or -1 if the document is no longer present.
|
||||
NewIndex int
|
||||
}
|
||||
|
||||
// Implementation of realtime updates (a.k.a. watch).
|
||||
// This code is closely based on the Node.js implementation,
|
||||
// https://github.com/googleapis/nodejs-firestore/blob/master/src/watch.js.
|
||||
|
||||
// The sole target ID for all streams from this client.
|
||||
// Variable for testing.
|
||||
var watchTargetID int32 = 'g' + 'o'
|
||||
|
||||
var defaultBackoff = gax.Backoff{
|
||||
// Values from https://github.com/googleapis/nodejs-firestore/blob/master/src/backoff.js.
|
||||
Initial: 1 * time.Second,
|
||||
Max: 60 * time.Second,
|
||||
Multiplier: 1.5,
|
||||
}
|
||||
|
||||
// not goroutine-safe
|
||||
type watchStream struct {
|
||||
ctx context.Context
|
||||
c *Client
|
||||
lc pb.Firestore_ListenClient // the gRPC stream
|
||||
target *pb.Target // document or query being watched
|
||||
backoff gax.Backoff // for stream retries
|
||||
err error // sticky permanent error
|
||||
readTime time.Time // time of most recent snapshot
|
||||
current bool // saw CURRENT, but not RESET; precondition for a snapshot
|
||||
hasReturned bool // have we returned a snapshot yet?
|
||||
compare func(a, b *DocumentSnapshot) (int, error) // compare documents according to query
|
||||
|
||||
// An ordered tree where DocumentSnapshots are the keys.
|
||||
docTree *btree.BTree
|
||||
// Map of document name to DocumentSnapshot for the last returned snapshot.
|
||||
docMap map[string]*DocumentSnapshot
|
||||
// Map of document name to DocumentSnapshot for accumulated changes for the current snapshot.
|
||||
// A nil value means the document was removed.
|
||||
changeMap map[string]*DocumentSnapshot
|
||||
}
|
||||
|
||||
func newWatchStreamForDocument(ctx context.Context, dr *DocumentRef) *watchStream {
|
||||
// A single document is always equal to itself.
|
||||
compare := func(_, _ *DocumentSnapshot) (int, error) { return 0, nil }
|
||||
return newWatchStream(ctx, dr.Parent.c, compare, &pb.Target{
|
||||
TargetType: &pb.Target_Documents{
|
||||
Documents: &pb.Target_DocumentsTarget{Documents: []string{dr.Path}},
|
||||
},
|
||||
TargetId: watchTargetID,
|
||||
})
|
||||
}
|
||||
|
||||
func newWatchStreamForQuery(ctx context.Context, q Query) (*watchStream, error) {
|
||||
qp, err := q.toProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target := &pb.Target{
|
||||
TargetType: &pb.Target_Query{
|
||||
Query: &pb.Target_QueryTarget{
|
||||
Parent: q.parentPath,
|
||||
QueryType: &pb.Target_QueryTarget_StructuredQuery{StructuredQuery: qp},
|
||||
},
|
||||
},
|
||||
TargetId: watchTargetID,
|
||||
}
|
||||
return newWatchStream(ctx, q.c, q.compareFunc(), target), nil
|
||||
}
|
||||
|
||||
const btreeDegree = 4
|
||||
|
||||
func newWatchStream(ctx context.Context, c *Client, compare func(_, _ *DocumentSnapshot) (int, error), target *pb.Target) *watchStream {
|
||||
w := &watchStream{
|
||||
ctx: ctx,
|
||||
c: c,
|
||||
compare: compare,
|
||||
target: target,
|
||||
backoff: defaultBackoff,
|
||||
docMap: map[string]*DocumentSnapshot{},
|
||||
changeMap: map[string]*DocumentSnapshot{},
|
||||
}
|
||||
w.docTree = btree.New(btreeDegree, func(a, b interface{}) bool {
|
||||
return w.less(a.(*DocumentSnapshot), b.(*DocumentSnapshot))
|
||||
})
|
||||
return w
|
||||
}
|
||||
|
||||
func (s *watchStream) less(a, b *DocumentSnapshot) bool {
|
||||
c, err := s.compare(a, b)
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return false
|
||||
}
|
||||
return c < 0
|
||||
}
|
||||
|
||||
// Once nextSnapshot returns an error, it will always return the same error.
|
||||
func (s *watchStream) nextSnapshot() (*btree.BTree, []DocumentChange, time.Time, error) {
|
||||
if s.err != nil {
|
||||
return nil, nil, time.Time{}, s.err
|
||||
}
|
||||
var changes []DocumentChange
|
||||
for {
|
||||
// Process messages until we are in a consistent state.
|
||||
for !s.handleNextMessage() {
|
||||
}
|
||||
if s.err != nil {
|
||||
_ = s.close() // ignore error
|
||||
return nil, nil, time.Time{}, s.err
|
||||
}
|
||||
var newDocTree *btree.BTree
|
||||
newDocTree, changes = s.computeSnapshot(s.docTree, s.docMap, s.changeMap, s.readTime)
|
||||
if s.err != nil {
|
||||
return nil, nil, time.Time{}, s.err
|
||||
}
|
||||
// Only return a snapshot if something has changed, or this is the first snapshot.
|
||||
if !s.hasReturned || newDocTree != s.docTree {
|
||||
s.docTree = newDocTree
|
||||
break
|
||||
}
|
||||
}
|
||||
s.changeMap = map[string]*DocumentSnapshot{}
|
||||
s.hasReturned = true
|
||||
return s.docTree, changes, s.readTime, nil
|
||||
}
|
||||
|
||||
// Read a message from the stream and handle it. Return true when
|
||||
// we're in a consistent state, or there is a permanent error.
|
||||
func (s *watchStream) handleNextMessage() bool {
|
||||
res, err := s.recv()
|
||||
if err != nil {
|
||||
s.err = err
|
||||
// Errors returned by recv are permanent.
|
||||
return true
|
||||
}
|
||||
switch r := res.ResponseType.(type) {
|
||||
case *pb.ListenResponse_TargetChange:
|
||||
return s.handleTargetChange(r.TargetChange)
|
||||
|
||||
case *pb.ListenResponse_DocumentChange:
|
||||
name := r.DocumentChange.Document.Name
|
||||
s.logf("DocumentChange %q", name)
|
||||
if hasWatchTargetID(r.DocumentChange.TargetIds) { // document changed
|
||||
ref, err := pathToDoc(name, s.c)
|
||||
if err == nil {
|
||||
s.changeMap[name], err = newDocumentSnapshot(ref, r.DocumentChange.Document, s.c, nil)
|
||||
}
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return true
|
||||
}
|
||||
} else if hasWatchTargetID(r.DocumentChange.RemovedTargetIds) { // document removed
|
||||
s.changeMap[name] = nil
|
||||
}
|
||||
|
||||
case *pb.ListenResponse_DocumentDelete:
|
||||
s.logf("Delete %q", r.DocumentDelete.Document)
|
||||
s.changeMap[r.DocumentDelete.Document] = nil
|
||||
|
||||
case *pb.ListenResponse_DocumentRemove:
|
||||
s.logf("Remove %q", r.DocumentRemove.Document)
|
||||
s.changeMap[r.DocumentRemove.Document] = nil
|
||||
|
||||
case *pb.ListenResponse_Filter:
|
||||
s.logf("Filter %d", r.Filter.Count)
|
||||
if int(r.Filter.Count) != s.currentSize() {
|
||||
s.resetDocs() // Remove all the current results.
|
||||
// The filter didn't match; close the stream so it will be re-opened on the next
|
||||
// call to nextSnapshot.
|
||||
_ = s.close() // ignore error
|
||||
s.lc = nil
|
||||
}
|
||||
|
||||
default:
|
||||
s.err = fmt.Errorf("unknown response type %T", r)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Return true if in a consistent state, or there is a permanent error.
|
||||
func (s *watchStream) handleTargetChange(tc *pb.TargetChange) bool {
|
||||
switch tc.TargetChangeType {
|
||||
case pb.TargetChange_NO_CHANGE:
|
||||
s.logf("TargetNoChange %d %v", len(tc.TargetIds), tc.ReadTime)
|
||||
if len(tc.TargetIds) == 0 && tc.ReadTime != nil && s.current {
|
||||
// Everything is up-to-date, so we are ready to return a snapshot.
|
||||
if err := tc.ReadTime.CheckValid(); err != nil {
|
||||
s.err = err
|
||||
return true
|
||||
}
|
||||
s.readTime = tc.ReadTime.AsTime()
|
||||
s.target.ResumeType = &pb.Target_ResumeToken{ResumeToken: tc.ResumeToken}
|
||||
return true
|
||||
}
|
||||
|
||||
case pb.TargetChange_ADD:
|
||||
s.logf("TargetAdd")
|
||||
if tc.TargetIds[0] != watchTargetID {
|
||||
s.err = errors.New("unexpected target ID sent by server")
|
||||
return true
|
||||
}
|
||||
|
||||
case pb.TargetChange_REMOVE:
|
||||
s.logf("TargetRemove")
|
||||
// We should never see a remove.
|
||||
if tc.Cause != nil {
|
||||
s.err = status.Error(codes.Code(tc.Cause.Code), tc.Cause.Message)
|
||||
} else {
|
||||
s.err = status.Error(codes.Internal, "firestore: client saw REMOVE")
|
||||
}
|
||||
return true
|
||||
|
||||
// The targets reflect all changes committed before the targets were added
|
||||
// to the stream.
|
||||
case pb.TargetChange_CURRENT:
|
||||
s.logf("TargetCurrent")
|
||||
s.current = true
|
||||
|
||||
// The targets have been reset, and a new initial state for the targets will be
|
||||
// returned in subsequent changes. Whatever changes have happened so far no
|
||||
// longer matter.
|
||||
case pb.TargetChange_RESET:
|
||||
s.logf("TargetReset")
|
||||
s.resetDocs()
|
||||
|
||||
default:
|
||||
s.err = fmt.Errorf("firestore: unknown TargetChange type %s", tc.TargetChangeType)
|
||||
return true
|
||||
}
|
||||
// If we see a resume token and our watch ID is affected, we assume the stream
|
||||
// is now healthy, so we reset our backoff time to the minimum.
|
||||
if tc.ResumeToken != nil && (len(tc.TargetIds) == 0 || hasWatchTargetID(tc.TargetIds)) {
|
||||
s.backoff = defaultBackoff
|
||||
}
|
||||
return false // not in a consistent state, keep receiving
|
||||
}
|
||||
|
||||
func (s *watchStream) resetDocs() {
|
||||
s.target.ResumeType = nil // clear resume token
|
||||
s.current = false
|
||||
s.changeMap = map[string]*DocumentSnapshot{}
|
||||
// Mark each document as deleted. If documents are not deleted, they
|
||||
// will be send again by the server.
|
||||
it := s.docTree.BeforeIndex(0)
|
||||
for it.Next() {
|
||||
s.changeMap[it.Key.(*DocumentSnapshot).Ref.Path] = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *watchStream) currentSize() int {
|
||||
_, adds, deletes := extractChanges(s.docMap, s.changeMap)
|
||||
return len(s.docMap) + len(adds) - len(deletes)
|
||||
}
|
||||
|
||||
// Return the changes that have occurred since the last snapshot.
|
||||
func extractChanges(docMap, changeMap map[string]*DocumentSnapshot) (updates, adds []*DocumentSnapshot, deletes []string) {
|
||||
for name, doc := range changeMap {
|
||||
switch {
|
||||
case doc == nil:
|
||||
if _, ok := docMap[name]; ok {
|
||||
deletes = append(deletes, name)
|
||||
}
|
||||
case docMap[name] != nil:
|
||||
updates = append(updates, doc)
|
||||
default:
|
||||
adds = append(adds, doc)
|
||||
}
|
||||
}
|
||||
return updates, adds, deletes
|
||||
}
|
||||
|
||||
// For development only.
|
||||
// TODO(jba): remove.
|
||||
func assert(b bool) {
|
||||
if !b {
|
||||
panic("assertion failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Applies the mutations in changeMap to both the document tree and the
|
||||
// document lookup map. Modifies docMap in place and returns a new docTree.
|
||||
// If there were no changes, returns docTree unmodified.
|
||||
func (s *watchStream) computeSnapshot(docTree *btree.BTree, docMap, changeMap map[string]*DocumentSnapshot, readTime time.Time) (*btree.BTree, []DocumentChange) {
|
||||
var changes []DocumentChange
|
||||
updatedTree := docTree
|
||||
assert(docTree.Len() == len(docMap))
|
||||
updates, adds, deletes := extractChanges(docMap, changeMap)
|
||||
if len(adds) > 0 || len(deletes) > 0 {
|
||||
updatedTree = docTree.Clone()
|
||||
}
|
||||
// Process the sorted changes in the order that is expected by our clients
|
||||
// (removals, additions, and then modifications). We also need to sort the
|
||||
// individual changes to assure that oldIndex/newIndex keep incrementing.
|
||||
deldocs := make([]*DocumentSnapshot, len(deletes))
|
||||
for i, d := range deletes {
|
||||
deldocs[i] = docMap[d]
|
||||
}
|
||||
sort.Sort(byLess{deldocs, s.less})
|
||||
for _, oldDoc := range deldocs {
|
||||
assert(oldDoc != nil)
|
||||
delete(docMap, oldDoc.Ref.Path)
|
||||
_, oldi := updatedTree.GetWithIndex(oldDoc)
|
||||
// TODO(jba): have btree.Delete return old index
|
||||
_, found := updatedTree.Delete(oldDoc)
|
||||
assert(found)
|
||||
changes = append(changes, DocumentChange{
|
||||
Kind: DocumentRemoved,
|
||||
Doc: oldDoc,
|
||||
OldIndex: oldi,
|
||||
NewIndex: -1,
|
||||
})
|
||||
}
|
||||
sort.Sort(byLess{adds, s.less})
|
||||
for _, newDoc := range adds {
|
||||
name := newDoc.Ref.Path
|
||||
assert(docMap[name] == nil)
|
||||
newDoc.ReadTime = readTime
|
||||
docMap[name] = newDoc
|
||||
updatedTree.Set(newDoc, nil)
|
||||
// TODO(jba): change btree so Set returns index as second value.
|
||||
_, newi := updatedTree.GetWithIndex(newDoc)
|
||||
changes = append(changes, DocumentChange{
|
||||
Kind: DocumentAdded,
|
||||
Doc: newDoc,
|
||||
OldIndex: -1,
|
||||
NewIndex: newi,
|
||||
})
|
||||
}
|
||||
sort.Sort(byLess{updates, s.less})
|
||||
for _, newDoc := range updates {
|
||||
name := newDoc.Ref.Path
|
||||
oldDoc := docMap[name]
|
||||
assert(oldDoc != nil)
|
||||
if newDoc.UpdateTime.Equal(oldDoc.UpdateTime) {
|
||||
continue
|
||||
}
|
||||
if updatedTree == docTree {
|
||||
updatedTree = docTree.Clone()
|
||||
}
|
||||
newDoc.ReadTime = readTime
|
||||
docMap[name] = newDoc
|
||||
_, oldi := updatedTree.GetWithIndex(oldDoc)
|
||||
updatedTree.Delete(oldDoc)
|
||||
updatedTree.Set(newDoc, nil)
|
||||
_, newi := updatedTree.GetWithIndex(newDoc)
|
||||
changes = append(changes, DocumentChange{
|
||||
Kind: DocumentModified,
|
||||
Doc: newDoc,
|
||||
OldIndex: oldi,
|
||||
NewIndex: newi,
|
||||
})
|
||||
}
|
||||
assert(updatedTree.Len() == len(docMap))
|
||||
return updatedTree, changes
|
||||
}
|
||||
|
||||
type byLess struct {
|
||||
s []*DocumentSnapshot
|
||||
less func(a, b *DocumentSnapshot) bool
|
||||
}
|
||||
|
||||
func (b byLess) Len() int { return len(b.s) }
|
||||
func (b byLess) Swap(i, j int) { b.s[i], b.s[j] = b.s[j], b.s[i] }
|
||||
func (b byLess) Less(i, j int) bool { return b.less(b.s[i], b.s[j]) }
|
||||
|
||||
func hasWatchTargetID(ids []int32) bool {
|
||||
for _, id := range ids {
|
||||
if id == watchTargetID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *watchStream) logf(format string, args ...interface{}) {
|
||||
if LogWatchStreams {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Close the stream. From this point on, calls to nextSnapshot will return
|
||||
// io.EOF, or the error from CloseSend.
|
||||
func (s *watchStream) stop() {
|
||||
err := s.close()
|
||||
if s.err != nil { // don't change existing error
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
// if an error occurs while closing the stream
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
// if we close successfully,
|
||||
s.err = io.EOF // normal shutdown
|
||||
}
|
||||
|
||||
func (s *watchStream) close() error {
|
||||
if s.lc == nil {
|
||||
return nil
|
||||
}
|
||||
return s.lc.CloseSend()
|
||||
}
|
||||
|
||||
// recv receives the next message from the stream. It also handles opening the stream
|
||||
// initially, and reopening it on non-permanent errors.
|
||||
// recv doesn't have to be goroutine-safe.
|
||||
func (s *watchStream) recv() (*pb.ListenResponse, error) {
|
||||
var err error
|
||||
for {
|
||||
if s.lc == nil {
|
||||
s.lc, err = s.open()
|
||||
if err != nil {
|
||||
// Do not retry if open fails.
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
res, err := s.lc.Recv()
|
||||
if err == nil || isPermanentWatchError(err) {
|
||||
return res, err
|
||||
}
|
||||
// Non-permanent error. Sleep and retry.
|
||||
s.changeMap = map[string]*DocumentSnapshot{} // clear changeMap
|
||||
dur := s.backoff.Pause()
|
||||
// If we're out of quota, wait a long time before retrying.
|
||||
if status.Code(err) == codes.ResourceExhausted {
|
||||
dur = s.backoff.Max
|
||||
}
|
||||
if err := sleep(s.ctx, dur); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.lc = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *watchStream) open() (lc pb.Firestore_ListenClient, err error) {
|
||||
s.ctx = trace.StartSpan(s.ctx, "cloud.google.com/go/firestore.watchStream.Listen")
|
||||
defer func() { trace.EndSpan(s.ctx, err) }()
|
||||
|
||||
dbPath := s.c.path()
|
||||
lc, err = s.c.c.Listen(withResourceHeader(s.ctx, dbPath))
|
||||
if err == nil {
|
||||
err = lc.Send(&pb.ListenRequest{
|
||||
Database: dbPath,
|
||||
TargetChange: &pb.ListenRequest_AddTarget{AddTarget: s.target},
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lc, nil
|
||||
}
|
||||
|
||||
func isPermanentWatchError(err error) bool {
|
||||
if err == io.EOF {
|
||||
// Retry on normal end-of-stream.
|
||||
return false
|
||||
}
|
||||
switch status.Code(err) {
|
||||
case codes.Unknown, codes.DeadlineExceeded, codes.ResourceExhausted,
|
||||
codes.Internal, codes.Unavailable, codes.Unauthenticated:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
// Copyright 2017 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"cloud.google.com/go/internal/trace"
|
||||
)
|
||||
|
||||
// A WriteBatch holds multiple database updates. Build a batch with the Create, Set,
|
||||
// Update and Delete methods, then run it with the Commit method. Errors in Create,
|
||||
// Set, Update or Delete are recorded instead of being returned immediately. The
|
||||
// first such error is returned by Commit.
|
||||
//
|
||||
// Deprecated: The WriteBatch API has been replaced with the transaction and
|
||||
// the bulk writer API. For atomic transaction operations, use `Transaction`.
|
||||
// For bulk read and write operations, use `BulkWriter`.
|
||||
type WriteBatch struct {
|
||||
c *Client
|
||||
err error
|
||||
writes []*pb.Write
|
||||
}
|
||||
|
||||
func (b *WriteBatch) add(ws []*pb.Write, err error) *WriteBatch {
|
||||
if b.err != nil {
|
||||
return b
|
||||
}
|
||||
if err != nil {
|
||||
b.err = err
|
||||
return b
|
||||
}
|
||||
b.writes = append(b.writes, ws...)
|
||||
return b
|
||||
}
|
||||
|
||||
// Create adds a Create operation to the batch.
|
||||
// See DocumentRef.Create for details.
|
||||
func (b *WriteBatch) Create(dr *DocumentRef, data interface{}) *WriteBatch {
|
||||
return b.add(dr.newCreateWrites(data))
|
||||
}
|
||||
|
||||
// Set adds a Set operation to the batch.
|
||||
// See DocumentRef.Set for details.
|
||||
func (b *WriteBatch) Set(dr *DocumentRef, data interface{}, opts ...SetOption) *WriteBatch {
|
||||
return b.add(dr.newSetWrites(data, opts))
|
||||
}
|
||||
|
||||
// Delete adds a Delete operation to the batch.
|
||||
// See DocumentRef.Delete for details.
|
||||
func (b *WriteBatch) Delete(dr *DocumentRef, opts ...Precondition) *WriteBatch {
|
||||
return b.add(dr.newDeleteWrites(opts))
|
||||
}
|
||||
|
||||
// Update adds an Update operation to the batch.
|
||||
// See DocumentRef.Update for details.
|
||||
func (b *WriteBatch) Update(dr *DocumentRef, data []Update, opts ...Precondition) *WriteBatch {
|
||||
return b.add(dr.newUpdatePathWrites(data, opts))
|
||||
}
|
||||
|
||||
// Commit applies all the writes in the batch to the database atomically. Commit
|
||||
// returns an error if there are no writes in the batch, if any errors occurred in
|
||||
// constructing the writes, or if the Commmit operation fails.
|
||||
func (b *WriteBatch) Commit(ctx context.Context) (_ []*WriteResult, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/firestore.WriteBatch.Commit")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
if b.err != nil {
|
||||
return nil, b.err
|
||||
}
|
||||
if len(b.writes) == 0 {
|
||||
return nil, errors.New("firestore: cannot commit empty WriteBatch")
|
||||
}
|
||||
return b.c.commit(ctx, b.writes)
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
This package is a fork of github.com/jba/btree at commit
|
||||
d4edd57f39b8425fc2c631047ff4dc6024d82a4f, which itself was a fork of
|
||||
github.com/google/btree at 316fb6d3f031ae8f4d457c6c5186b9e3ded70435.
|
||||
|
||||
This directory makes the following modifications:
|
||||
|
||||
- Updated copyright notice.
|
||||
- removed LICENSE (it is the same as the repo-wide license, Apache 2.0)
|
||||
- Removed examples_test.go and .travis.yml.
|
||||
- Added this file.
|
||||
|
||||
+1021
File diff suppressed because it is too large
Load Diff
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright 2014 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
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package btree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (t *BTree) print(w io.Writer) {
|
||||
t.root.print(w, 0)
|
||||
}
|
||||
|
||||
func (n *node) print(w io.Writer, level int) {
|
||||
indent := strings.Repeat(" ", level)
|
||||
if n == nil {
|
||||
fmt.Fprintf(w, "%s<nil>\n", indent)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "%s%v\n", indent, n.items)
|
||||
for _, c := range n.children {
|
||||
c.print(w, level+1)
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
// Package detect is used find information from the environment.
|
||||
package detect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/oauth2/google"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/transport"
|
||||
)
|
||||
|
||||
const (
|
||||
// ProjectIDSentinel is the value that users should pass for the project ID
|
||||
// to enable detection.
|
||||
ProjectIDSentinel = "*detect-project-id*"
|
||||
envProjectID = "GOOGLE_CLOUD_PROJECT"
|
||||
)
|
||||
|
||||
var (
|
||||
adcLookupFunc func(context.Context, ...option.ClientOption) (*google.Credentials, error) = transport.Creds
|
||||
envLookupFunc func(string) string = os.Getenv
|
||||
)
|
||||
|
||||
// ProjectID tries to detect the project ID from the environment if the sentinel
|
||||
// value, "*detect-project-id*", is sent. It looks in the following order:
|
||||
// 1. GOOGLE_CLOUD_PROJECT envvar
|
||||
// 2. ADC creds.ProjectID
|
||||
// 3. A static value if the environment is emulated.
|
||||
func ProjectID(ctx context.Context, projectID, emulatorEnvVar string, opts ...option.ClientOption) (string, error) {
|
||||
if projectID != ProjectIDSentinel {
|
||||
return projectID, nil
|
||||
}
|
||||
// 1. Try a well known environment variable
|
||||
if id := envLookupFunc(envProjectID); id != "" {
|
||||
return id, nil
|
||||
}
|
||||
// 2. Try ADC
|
||||
creds, err := adcLookupFunc(ctx, opts...)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetching creds: %v", err)
|
||||
}
|
||||
// 3. If ADC does not work, and the environment is emulated, return a const value.
|
||||
if creds.ProjectID == "" && emulatorEnvVar != "" && envLookupFunc(emulatorEnvVar) != "" {
|
||||
return "emulated-project", nil
|
||||
}
|
||||
// 4. If 1-3 don't work, error out
|
||||
if creds.ProjectID == "" {
|
||||
return "", errors.New("unable to detect projectID, please refer to docs for DetectProjectID")
|
||||
}
|
||||
// Success from ADC
|
||||
return creds.ProjectID, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user