chore(deps): bump google.golang.org/api from 0.172.0 to 0.175.0 (#3128)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.172.0 to 0.175.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.172.0...v0.175.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
parent
d251b81da2
commit
072213b3fc
13
go.mod
13
go.mod
|
|
@ -47,8 +47,7 @@ require (
|
|||
|
||||
require (
|
||||
cloud.google.com/go v0.112.1 // indirect
|
||||
cloud.google.com/go/compute v1.25.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.7 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
|
|
@ -139,14 +138,16 @@ require (
|
|||
golang.org/x/sys v0.19.0
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/api v0.172.0
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
|
||||
google.golang.org/grpc v1.62.1 // indirect
|
||||
google.golang.org/api v0.175.0
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||
google.golang.org/grpc v1.63.2 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.2.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.1 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect
|
||||
|
|
@ -194,7 +195,7 @@ require (
|
|||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.4.0 // indirect
|
||||
|
|
|
|||
26
go.sum
26
go.sum
|
|
@ -1,10 +1,12 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
|
||||
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
|
||||
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
|
||||
cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/auth v0.2.2 h1:gmxNJs4YZYcw6YvKRtVBaF2fyUE6UrWPyzU8jHvYfmI=
|
||||
cloud.google.com/go/auth v0.2.2/go.mod h1:2bDNJWtWziDT3Pu1URxHHbkHE/BbOCuyUiKIGcNvafo=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.1 h1:VSPmMmUlT8CkIZ2PzD9AlLN+R3+D1clXMWHHa6vG/Ag=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.1/go.mod h1:tOdK/k+D2e4GEwfBRA48dKNQiDsqIXxLh7VU319eV0g=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
|
||||
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
|
||||
cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw=
|
||||
|
|
@ -640,26 +642,26 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
|||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.172.0 h1:/1OcMZGPmW1rX2LCu2CmGUD1KXK1+pfzxotxyRUCCdk=
|
||||
google.golang.org/api v0.172.0/go.mod h1:+fJZq6QXWfa9pXhnIzsjx4yI22d4aI9ZpLb58gvXjis=
|
||||
google.golang.org/api v0.175.0 h1:9bMDh10V9cBuU8N45Wlc3cKkItfqMRV0Fi8UscLEtbY=
|
||||
google.golang.org/api v0.175.0/go.mod h1:Rra+ltKu14pps/4xTycZfobMgLpbosoaaL7c+SEMrO8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
|
||||
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
|
||||
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
# Changelog
|
||||
|
||||
## [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))
|
||||
0
vendor/cloud.google.com/go/compute/LICENSE → vendor/cloud.google.com/go/auth/LICENSE
generated
vendored
0
vendor/cloud.google.com/go/compute/LICENSE → vendor/cloud.google.com/go/auth/LICENSE
generated
vendored
|
|
@ -0,0 +1,4 @@
|
|||
# auth
|
||||
|
||||
This module is currently EXPERIMENTAL and under active development. It is not
|
||||
yet intended to be used.
|
||||
|
|
@ -0,0 +1,476 @@
|
|||
// 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/jwt"
|
||||
)
|
||||
|
||||
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 = 215 * time.Second
|
||||
|
||||
universeDomainDefault = "googleapis.com"
|
||||
)
|
||||
|
||||
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 10 seconds.
|
||||
func (t *Token) IsValid() bool {
|
||||
return t.isValidWithEarlyExpiry(defaultExpiryDelta)
|
||||
}
|
||||
|
||||
func (t *Token) isValidWithEarlyExpiry(earlyExpiry time.Duration) bool {
|
||||
if t == nil || t.Value == "" {
|
||||
return false
|
||||
}
|
||||
if t.Expiry.IsZero() {
|
||||
return true
|
||||
}
|
||||
return !t.Expiry.Round(0).Add(-earlyExpiry).Before(timeNow())
|
||||
}
|
||||
|
||||
// Credentials holds Google credentials, including
|
||||
// [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. Most users
|
||||
// will want to build this object a function from the
|
||||
// [cloud.google.com/go/auth/credentials] package.
|
||||
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 provided options for configuring a
|
||||
// CachedTokenProvider.
|
||||
type CachedTokenProviderOptions struct {
|
||||
// DisableAutoRefresh makes the TokenProvider always return the same token,
|
||||
// even if it is expired.
|
||||
DisableAutoRefresh bool
|
||||
// ExpireEarly configures the amount of time before a token expires, that it
|
||||
// should be refreshed. If unset, the default value is 10 seconds.
|
||||
ExpireEarly time.Duration
|
||||
}
|
||||
|
||||
func (ctpo *CachedTokenProviderOptions) autoRefresh() bool {
|
||||
if ctpo == nil {
|
||||
return true
|
||||
}
|
||||
return !ctpo.DisableAutoRefresh
|
||||
}
|
||||
|
||||
func (ctpo *CachedTokenProviderOptions) expireEarly() time.Duration {
|
||||
if ctpo == nil {
|
||||
return defaultExpiryDelta
|
||||
}
|
||||
return ctpo.ExpireEarly
|
||||
}
|
||||
|
||||
// NewCachedTokenProvider wraps a [TokenProvider] to cache the tokens returned
|
||||
// by the underlying provider. By default it will refresh tokens ten seconds
|
||||
// before they expire, but this time can be configured with the optional
|
||||
// options.
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
type cachedTokenProvider struct {
|
||||
tp TokenProvider
|
||||
autoRefresh bool
|
||||
expireEarly time.Duration
|
||||
|
||||
mu sync.Mutex
|
||||
cachedToken *Token
|
||||
}
|
||||
|
||||
func (c *cachedTokenProvider) Token(ctx context.Context) (*Token, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.cachedToken.IsValid() || !c.autoRefresh {
|
||||
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
|
||||
}
|
||||
|
||||
func (o *Options2LO) client() *http.Client {
|
||||
if o.Client != nil {
|
||||
return o.Client
|
||||
}
|
||||
return internal.CloneDefaultClient()
|
||||
}
|
||||
|
||||
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()}, nil
|
||||
}
|
||||
|
||||
type tokenProvider2LO struct {
|
||||
opts *Options2LO
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
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)
|
||||
resp, err := tp.Client.PostForm(tp.opts.TokenURL, v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := internal.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// 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(earlyExpiry time.Duration, scope ...string) auth.TokenProvider {
|
||||
return auth.NewCachedTokenProvider(computeProvider{scopes: scope}, &auth.CachedTokenProviderOptions{
|
||||
ExpireEarly: earlyExpiry,
|
||||
})
|
||||
}
|
||||
|
||||
// computeProvider fetches tokens from the google cloud metadata service.
|
||||
type computeProvider struct {
|
||||
scopes []string
|
||||
}
|
||||
|
||||
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 := metadata.Get(tokenURI.String())
|
||||
if err != nil {
|
||||
return nil, 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
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
// 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/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"
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
// 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 opts.CredentialsJSON != nil {
|
||||
return readCredentialsFileJSON(opts.CredentialsJSON, opts)
|
||||
}
|
||||
if filename := credsfile.GetFileNameFromEnv(opts.CredentialsFile); filename != "" {
|
||||
if creds, err := readCredentialsFile(filename, opts); err == nil {
|
||||
return creds, err
|
||||
}
|
||||
}
|
||||
|
||||
fileName := credsfile.GetWellKnownFileName()
|
||||
if b, err := os.ReadFile(fileName); err == nil {
|
||||
return readCredentialsFileJSON(b, opts)
|
||||
}
|
||||
|
||||
if OnGCE() {
|
||||
return auth.NewCredentials(&auth.CredentialsOptions{
|
||||
TokenProvider: computeTokenProvider(opts.EarlyTokenRefresh, opts.Scopes...),
|
||||
ProjectIDProvider: auth.CredentialsPropertyFunc(func(context.Context) (string, error) {
|
||||
return metadata.ProjectID()
|
||||
}),
|
||||
UniverseDomainProvider: &internal.ComputeUniverseDomainProvider{},
|
||||
}), 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.
|
||||
EarlyTokenRefresh time.Duration
|
||||
// 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
|
||||
}
|
||||
|
||||
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.CloneDefaultClient()
|
||||
}
|
||||
|
||||
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(),
|
||||
EarlyTokenExpiry: opts.EarlyTokenRefresh,
|
||||
AuthHandlerOpts: handleOpts,
|
||||
// TODO(codyoss): refactor this out. We need to add in auto-detection
|
||||
// for this use case.
|
||||
AuthStyle: auth.StyleInParams,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 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, quotaProjectID, 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
|
||||
}
|
||||
quotaProjectID = f.QuotaProjectID
|
||||
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
|
||||
}
|
||||
quotaProjectID = f.QuotaProjectID
|
||||
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
|
||||
}
|
||||
quotaProjectID = f.QuotaProjectID
|
||||
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),
|
||||
QuotaProjectIDProvider: internalauth.StaticCredentialsProperty(quotaProjectID),
|
||||
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) {
|
||||
if opts.UseSelfSignedJWT {
|
||||
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,
|
||||
}
|
||||
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,
|
||||
}
|
||||
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(),
|
||||
}
|
||||
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(),
|
||||
}
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
func handleGDCHServiceAccount(f *credsfile.GDCHServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
|
||||
return gdch.NewTokenProvider(f, &gdch.Options{
|
||||
STSAudience: opts.STSAudience,
|
||||
Client: opts.client(),
|
||||
})
|
||||
}
|
||||
547
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/aws_provider.go
generated
vendored
Normal file
547
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/aws_provider.go
generated
vendored
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
// 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"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth/internal"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (sp *awsSubjectProvider) subjectToken(ctx context.Context) (string, error) {
|
||||
// Set Defaults
|
||||
if sp.RegionalCredVerificationURL == "" {
|
||||
sp.RegionalCredVerificationURL = defaultRegionalCredentialVerificationURL
|
||||
}
|
||||
if sp.requestSigner == nil {
|
||||
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.NewRequest("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)
|
||||
|
||||
resp, err := sp.Client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := internal.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("credentials: unable to retrieve AWS session token: %s", respBody)
|
||||
}
|
||||
return string(respBody), 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)
|
||||
}
|
||||
|
||||
resp, err := sp.Client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := internal.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("credentials: unable to retrieve AWS region - %s", respBody)
|
||||
}
|
||||
|
||||
// This endpoint will return the region in format: us-east-2b.
|
||||
// Only the us-east-2 part should be used.
|
||||
bodyLen := len(respBody)
|
||||
if bodyLen == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return string(respBody[: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)
|
||||
}
|
||||
|
||||
resp, err := sp.Client.Do(req)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := internal.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return result, fmt.Errorf("credentials: unable to retrieve AWS security credentials - %s", respBody)
|
||||
}
|
||||
err = json.Unmarshal(respBody, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
resp, err := sp.Client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := internal.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("credentials: unable to retrieve AWS role name - %s", respBody)
|
||||
}
|
||||
return string(respBody), 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())
|
||||
}
|
||||
284
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/executable_provider.go
generated
vendored
Normal file
284
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/executable_provider.go
generated
vendored
Normal file
|
|
@ -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)
|
||||
}
|
||||
367
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/externalaccount.go
generated
vendored
Normal file
367
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/externalaccount.go
generated
vendored
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
// 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"
|
||||
"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"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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()
|
||||
stp, err := newSubjectTokenProvider(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tp := &tokenProvider{
|
||||
client: opts.Client,
|
||||
opts: opts,
|
||||
stp: stp,
|
||||
}
|
||||
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: opts.Client,
|
||||
URL: opts.ServiceAccountImpersonationURL,
|
||||
Scopes: scopes,
|
||||
Tp: auth.NewCachedTokenProvider(tp, nil),
|
||||
TokenLifetimeSeconds: opts.ServiceAccountImpersonationLifetimeSeconds,
|
||||
})
|
||||
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
|
||||
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,
|
||||
})
|
||||
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) {
|
||||
reqOpts := &RequestOptions{Audience: o.Audience, SubjectTokenType: o.SubjectTokenType}
|
||||
if o.AwsSecurityCredentialsProvider != nil {
|
||||
return &awsSubjectProvider{
|
||||
securityCredentialsProvider: o.AwsSecurityCredentialsProvider,
|
||||
TargetResource: o.Audience,
|
||||
reqOpts: reqOpts,
|
||||
}, 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,
|
||||
}
|
||||
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}, 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
|
||||
}
|
||||
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)
|
||||
}
|
||||
78
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/file_provider.go
generated
vendored
Normal file
78
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/file_provider.go
generated
vendored
Normal file
|
|
@ -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
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/info.go
generated
vendored
Normal file
74
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/info.go
generated
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2022 Google LLC
|
||||
// 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.
|
||||
|
|
@ -12,7 +12,19 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
package externalaccount
|
||||
|
||||
// Version is the current tagged release of the library.
|
||||
const Version = "1.25.1"
|
||||
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)
|
||||
}
|
||||
93
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/url_provider.go
generated
vendored
Normal file
93
vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/url_provider.go
generated
vendored
Normal file
|
|
@ -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"
|
||||
"net/http"
|
||||
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/credsfile"
|
||||
)
|
||||
|
||||
const (
|
||||
fileTypeText = "text"
|
||||
fileTypeJSON = "json"
|
||||
urlProviderType = "url"
|
||||
programmaticProviderType = "programmatic"
|
||||
)
|
||||
|
||||
type urlSubjectProvider struct {
|
||||
URL string
|
||||
Headers map[string]string
|
||||
Format *credsfile.Format
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
resp, err := sp.Client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("credentials: invalid response when retrieving subject token: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := internal.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("credentials: invalid body in subject token URL query: %w", err)
|
||||
}
|
||||
if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
|
||||
return "", fmt.Errorf("credentials: status code %d: %s", c, respBody)
|
||||
}
|
||||
|
||||
if sp.Format == nil {
|
||||
return string(respBody), nil
|
||||
}
|
||||
switch sp.Format.Type {
|
||||
case "json":
|
||||
jsonData := make(map[string]interface{})
|
||||
err = json.Unmarshal(respBody, &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(respBody), nil
|
||||
default:
|
||||
return "", errors.New("credentials: invalid credential_source file format type: " + sp.Format.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (sp *urlSubjectProvider) providerType() string {
|
||||
return urlProviderType
|
||||
}
|
||||
110
vendor/cloud.google.com/go/auth/credentials/internal/externalaccountuser/externalaccountuser.go
generated
vendored
Normal file
110
vendor/cloud.google.com/go/auth/credentials/internal/externalaccountuser/externalaccountuser.go
generated
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// 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"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/credentials/internal/stsexchange"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
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
|
||||
}
|
||||
182
vendor/cloud.google.com/go/auth/credentials/internal/gdch/gdch.go
generated
vendored
Normal file
182
vendor/cloud.google.com/go/auth/credentials/internal/gdch/gdch.go
generated
vendored
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
// 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/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"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"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
pk, 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,
|
||||
pk: pk,
|
||||
pkID: f.PrivateKeyID,
|
||||
certPool: certPool,
|
||||
client: o.Client,
|
||||
}
|
||||
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
|
||||
pk *rsa.PrivateKey
|
||||
pkID string
|
||||
certPool *x509.CertPool
|
||||
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
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.pk)
|
||||
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)
|
||||
resp, err := g.client.PostForm(g.tokenURL, v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := internal.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
151
vendor/cloud.google.com/go/auth/credentials/internal/impersonate/impersonate.go
generated
vendored
Normal file
151
vendor/cloud.google.com/go/auth/credentials/internal/impersonate/impersonate.go
generated
vendored
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
// 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"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
resp, err := o.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: unable to generate access token: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := internal.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: unable to read body: %w", err)
|
||||
}
|
||||
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
vendor/cloud.google.com/go/auth/credentials/internal/stsexchange/sts_exchange.go
generated
vendored
Normal file
167
vendor/cloud.google.com/go/auth/credentials/internal/stsexchange/sts_exchange.go
generated
vendored
Normal file
|
|
@ -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"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
)
|
||||
|
||||
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
|
||||
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()
|
||||
|
||||
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)))
|
||||
|
||||
resp, err := opts.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: invalid response from Secure Token Server: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := internal.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// 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/rsa"
|
||||
"fmt"
|
||||
"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) {
|
||||
pk, 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(),
|
||||
pk: pk,
|
||||
pkID: f.PrivateKeyID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type selfSignedTokenProvider struct {
|
||||
email string
|
||||
audience string
|
||||
scopes []string
|
||||
pk *rsa.PrivateKey
|
||||
pkID string
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
msg, err := jwt.EncodeJWS(h, c, tp.pk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials: could not encode JWT: %w", err)
|
||||
}
|
||||
return &auth.Token{Value: msg, Type: internal.TokenTypeBearer, Expiry: exp}, nil
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
// 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/compute/metadata"
|
||||
"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, _ *Options) bool {
|
||||
if tp == nil {
|
||||
return false
|
||||
}
|
||||
tok, err := tp.Token(context.Background())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if tok == nil {
|
||||
return false
|
||||
}
|
||||
if source, _ := tok.Metadata["auth.google.tokenSource"].(string); source != "compute-metadata" {
|
||||
return false
|
||||
}
|
||||
if acct, _ := tok.Metadata["auth.google.serviceAccount"].(string); acct != "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) && metadata.OnGCE() && 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
|
||||
}
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
// 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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/credentials"
|
||||
"cloud.google.com/go/auth/internal"
|
||||
"cloud.google.com/go/auth/internal/transport"
|
||||
"go.opencensus.io/plugin/ocgrpc"
|
||||
"google.golang.org/grpc"
|
||||
grpccreds "google.golang.org/grpc/credentials"
|
||||
grpcinsecure "google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
// Options used to configure a [GRPCClientConnPool] from [Dial].
|
||||
type Options struct {
|
||||
// DisableTelemetry disables default telemetry (OpenCensus). 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
|
||||
// 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
|
||||
|
||||
// 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) 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.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
|
||||
}
|
||||
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,
|
||||
Client: opts.client(),
|
||||
UniverseDomain: opts.UniverseDomain,
|
||||
}
|
||||
if io := opts.InternalOptions; io != nil {
|
||||
tOpts.DefaultEndpointTemplate = io.DefaultEndpointTemplate
|
||||
tOpts.DefaultMTLSEndpoint = io.DefaultMTLSEndpoint
|
||||
}
|
||||
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),
|
||||
}
|
||||
|
||||
// Authentication can only be sent when communicating over a secure connection.
|
||||
if !opts.DisableAuthentication {
|
||||
metadata := opts.Metadata
|
||||
creds, err := credentials.DetectDefault(opts.resolveDetectOptions())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts.Credentials != nil {
|
||||
creds = opts.Credentials
|
||||
}
|
||||
|
||||
qp, err := creds.QuotaProjectID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if qp != "" {
|
||||
if metadata == nil {
|
||||
metadata = make(map[string]string, 1)
|
||||
}
|
||||
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 = addOCStatsHandler(grpcOpts, opts)
|
||||
grpcOpts = append(grpcOpts, opts.GRPCDialOpts...)
|
||||
|
||||
return grpc.DialContext(ctx, endpoint, grpcOpts...)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
func (c *grpcCredentialsProvider) getClientUniverseDomain() string {
|
||||
if c.clientUniverseDomain == "" {
|
||||
return internal.DefaultUniverseDomain
|
||||
}
|
||||
return c.clientUniverseDomain
|
||||
}
|
||||
|
||||
func (c *grpcCredentialsProvider) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||
credentialsUniverseDomain, err := c.creds.UniverseDomain(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := transport.ValidateUniverseDomain(c.getClientUniverseDomain(), credentialsUniverseDomain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := c.creds.Token(ctx)
|
||||
if 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 addOCStatsHandler(dialOpts []grpc.DialOption, opts *Options) []grpc.DialOption {
|
||||
if opts.DisableTelemetry {
|
||||
return dialOpts
|
||||
}
|
||||
return append(dialOpts, grpc.WithStatsHandler(&ocgrpc.ClientHandler{}))
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
// 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 (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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 (OpenCensus). 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
|
||||
// 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
|
||||
|
||||
// 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) 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
|
||||
}
|
||||
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 needs more control over their transport.
|
||||
SkipValidation 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.
|
||||
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 {
|
||||
base = http.DefaultTransport.(*http.Transport).Clone()
|
||||
}
|
||||
client.Transport = &authTransport{
|
||||
creds: creds,
|
||||
base: base,
|
||||
// TODO(quartzmo): Somehow set clientUniverseDomain from impersonate calls.
|
||||
}
|
||||
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,
|
||||
}
|
||||
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
|
||||
}
|
||||
trans, err := newTransport(defaultBaseTransport(clientCertProvider, dialTLSContext), 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)
|
||||
}
|
||||
|
|
@ -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 httptransport
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.opencensus.io/trace"
|
||||
"go.opencensus.io/trace/propagation"
|
||||
)
|
||||
|
||||
const (
|
||||
httpHeaderMaxSize = 200
|
||||
cloudTraceHeader = `X-Cloud-Trace-Context`
|
||||
)
|
||||
|
||||
// asserts the httpFormat fulfills this foreign interface
|
||||
var _ propagation.HTTPFormat = (*httpFormat)(nil)
|
||||
|
||||
// httpFormat implements propagation.httpFormat to propagate
|
||||
// traces in HTTP headers for Google Cloud Platform and Cloud Trace.
|
||||
type httpFormat struct{}
|
||||
|
||||
// SpanContextFromRequest extracts a Cloud Trace span context from incoming requests.
|
||||
func (f *httpFormat) SpanContextFromRequest(req *http.Request) (sc trace.SpanContext, ok bool) {
|
||||
h := req.Header.Get(cloudTraceHeader)
|
||||
// See https://cloud.google.com/trace/docs/faq for the header HTTPFormat.
|
||||
// Return if the header is empty or missing, or if the header is unreasonably
|
||||
// large, to avoid making unnecessary copies of a large string.
|
||||
if h == "" || len(h) > httpHeaderMaxSize {
|
||||
return trace.SpanContext{}, false
|
||||
}
|
||||
|
||||
// Parse the trace id field.
|
||||
slash := strings.Index(h, `/`)
|
||||
if slash == -1 {
|
||||
return trace.SpanContext{}, false
|
||||
}
|
||||
tid, h := h[:slash], h[slash+1:]
|
||||
|
||||
buf, err := hex.DecodeString(tid)
|
||||
if err != nil {
|
||||
return trace.SpanContext{}, false
|
||||
}
|
||||
copy(sc.TraceID[:], buf)
|
||||
|
||||
// Parse the span id field.
|
||||
spanstr := h
|
||||
semicolon := strings.Index(h, `;`)
|
||||
if semicolon != -1 {
|
||||
spanstr, h = h[:semicolon], h[semicolon+1:]
|
||||
}
|
||||
sid, err := strconv.ParseUint(spanstr, 10, 64)
|
||||
if err != nil {
|
||||
return trace.SpanContext{}, false
|
||||
}
|
||||
binary.BigEndian.PutUint64(sc.SpanID[:], sid)
|
||||
|
||||
// Parse the options field, options field is optional.
|
||||
if !strings.HasPrefix(h, "o=") {
|
||||
return sc, true
|
||||
}
|
||||
o, err := strconv.ParseUint(h[2:], 10, 32)
|
||||
if err != nil {
|
||||
return trace.SpanContext{}, false
|
||||
}
|
||||
sc.TraceOptions = trace.TraceOptions(o)
|
||||
return sc, true
|
||||
}
|
||||
|
||||
// SpanContextToRequest modifies the given request to include a Cloud Trace header.
|
||||
func (f *httpFormat) SpanContextToRequest(sc trace.SpanContext, req *http.Request) {
|
||||
sid := binary.BigEndian.Uint64(sc.SpanID[:])
|
||||
header := fmt.Sprintf("%s/%d;o=%d", hex.EncodeToString(sc.TraceID[:]), sid, int64(sc.TraceOptions))
|
||||
req.Header.Set(cloudTraceHeader, header)
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
// 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"
|
||||
"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.opencensus.io/plugin/ochttp"
|
||||
"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 = addOCTransport(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:
|
||||
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)
|
||||
}
|
||||
headers.Set(quotaProjectHeaderKey, qp)
|
||||
}
|
||||
|
||||
if opts.Credentials != nil {
|
||||
creds = opts.Credentials
|
||||
}
|
||||
creds.TokenProvider = auth.NewCachedTokenProvider(creds.TokenProvider, nil)
|
||||
trans = &authTransport{
|
||||
base: trans,
|
||||
creds: creds,
|
||||
clientUniverseDomain: opts.UniverseDomain,
|
||||
}
|
||||
}
|
||||
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 {
|
||||
trans := http.DefaultTransport.(*http.Transport).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 addOCTransport(trans http.RoundTripper, opts *Options) http.RoundTripper {
|
||||
if opts.DisableTelemetry {
|
||||
return trans
|
||||
}
|
||||
return &ochttp.Transport{
|
||||
Base: trans,
|
||||
Propagation: &httpFormat{},
|
||||
}
|
||||
}
|
||||
|
||||
type authTransport struct {
|
||||
creds *auth.Credentials
|
||||
base http.RoundTripper
|
||||
clientUniverseDomain string
|
||||
}
|
||||
|
||||
// getClientUniverseDomain returns the universe domain configured for the client.
|
||||
// The default value is "googleapis.com".
|
||||
func (t *authTransport) getClientUniverseDomain() string {
|
||||
if t.clientUniverseDomain == "" {
|
||||
return internal.DefaultUniverseDomain
|
||||
}
|
||||
return t.clientUniverseDomain
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}()
|
||||
}
|
||||
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
|
||||
}
|
||||
token, err := t.creds.Token(req.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req2 := req.Clone(req.Context())
|
||||
SetAuthHeader(token, req2)
|
||||
reqBodyClosed = true
|
||||
return t.base.RoundTrip(req2)
|
||||
}
|
||||
|
|
@ -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 ""
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
// 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, 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"`
|
||||
EnvironmentID string `json:"environment_id"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
// 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/rsa"
|
||||
"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"
|
||||
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"
|
||||
)
|
||||
|
||||
// CloneDefaultClient returns a [http.Client] with some good defaults.
|
||||
func CloneDefaultClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: http.DefaultTransport.(*http.Transport).Clone(),
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseKey converts the binary contents of a private key file
|
||||
// to an *rsa.PrivateKey. 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) (*rsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode(key)
|
||||
if block != nil {
|
||||
key = block.Bytes
|
||||
}
|
||||
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.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("private key is invalid")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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)
|
||||
})
|
||||
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) (string, error) {
|
||||
client := metadata.NewClient(&http.Client{Timeout: time.Second})
|
||||
// TODO(quartzmo): set ctx on request
|
||||
return client.Get("universe/universe_domain")
|
||||
}
|
||||
|
||||
func getMetadataUniverseDomain(ctx context.Context) (string, error) {
|
||||
universeDomain, err := httpGetMetadataUniverseDomain(ctx)
|
||||
if err == nil {
|
||||
return universeDomain, nil
|
||||
}
|
||||
if _, ok := err.(metadata.NotDefinedError); ok {
|
||||
// http.StatusNotFound (404)
|
||||
return DefaultUniverseDomain, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -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, key *rsa.PrivateKey) (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 := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, h.Sum(nil))
|
||||
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)
|
||||
}
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
// 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"
|
||||
"errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
mdsMTLSAutoConfigSource mtlsConfigSource
|
||||
errUniverseNotSupportedMTLS = errors.New("mTLS is not supported in any universe other than googleapis.com")
|
||||
)
|
||||
|
||||
// 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
|
||||
DefaultMTLSEndpoint string
|
||||
DefaultEndpointTemplate string
|
||||
ClientCertProvider cert.Provider
|
||||
Client *http.Client
|
||||
UniverseDomain string
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
if config.s2aAddress == "" {
|
||||
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: config.s2aAddress,
|
||||
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
|
||||
}
|
||||
|
||||
if config.s2aAddress == "" {
|
||||
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: config.s2aAddress,
|
||||
FallbackOpts: fallbackOpts,
|
||||
})
|
||||
return nil, dialTLSContextFunc, nil
|
||||
}
|
||||
|
||||
func getTransportConfig(opts *Options) (*transportConfig, error) {
|
||||
clientCertSource, err := getClientCertificateSource(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
|
||||
}
|
||||
if !opts.isUniverseDomainGDU() {
|
||||
return nil, errUniverseNotSupportedMTLS
|
||||
}
|
||||
|
||||
s2aMTLSEndpoint := opts.DefaultMTLSEndpoint
|
||||
// If there is endpoint override, honor it.
|
||||
if opts.Endpoint != "" {
|
||||
s2aMTLSEndpoint = endpoint
|
||||
}
|
||||
s2aAddress := GetS2AAddress()
|
||||
if s2aAddress == "" {
|
||||
return &defaultTransportConfig, nil
|
||||
}
|
||||
return &transportConfig{
|
||||
clientCertSource: clientCertSource,
|
||||
endpoint: endpoint,
|
||||
s2aAddress: s2aAddress,
|
||||
s2aMTLSEndpoint: s2aMTLSEndpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getClientCertificateSource 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).
|
||||
//
|
||||
// Important Note: For now, the environment variable GOOGLE_API_USE_CLIENT_CERTIFICATE
|
||||
// must be set to "true" to allow certificate to be used (including user provided
|
||||
// certificates). For details, see AIP-4114.
|
||||
func getClientCertificateSource(opts *Options) (cert.Provider, error) {
|
||||
if !isClientCertificateEnabled() {
|
||||
return nil, nil
|
||||
} else if opts.ClientCertProvider != nil {
|
||||
return opts.ClientCertProvider, nil
|
||||
}
|
||||
return cert.DefaultProvider()
|
||||
|
||||
}
|
||||
|
||||
func isClientCertificateEnabled() bool {
|
||||
// TODO(andyrzhao): Update default to return "true" after DCA feature is fully released.
|
||||
// error as false is a good default
|
||||
b, _ := strconv.ParseBool(os.Getenv(googleAPIUseCertSource))
|
||||
return b
|
||||
}
|
||||
|
||||
type transportConfig struct {
|
||||
// The client certificate source.
|
||||
clientCertSource cert.Provider
|
||||
// The corresponding endpoint to use based on client certificate source.
|
||||
endpoint string
|
||||
// The S2A address if it can be used, otherwise an empty string.
|
||||
s2aAddress 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"
|
||||
func getEndpoint(opts *Options, clientCertSource cert.Provider) (string, error) {
|
||||
if opts.Endpoint == "" {
|
||||
mtlsMode := getMTLSMode()
|
||||
if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) {
|
||||
if !opts.isUniverseDomainGDU() {
|
||||
return "", errUniverseNotSupportedMTLS
|
||||
}
|
||||
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)
|
||||
}
|
||||
62
vendor/cloud.google.com/go/auth/internal/transport/cert/default_cert.go
generated
vendored
Normal file
62
vendor/cloud.google.com/go/auth/internal/transport/cert/default_cert.go
generated
vendored
Normal file
|
|
@ -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.
|
||||
|
||||
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 = 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
|
||||
}
|
||||
56
vendor/cloud.google.com/go/auth/internal/transport/cert/enterprise_cert.go
generated
vendored
Normal file
56
vendor/cloud.google.com/go/auth/internal/transport/cert/enterprise_cert.go
generated
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// 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"
|
||||
|
||||
"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 {
|
||||
if errors.Is(err, client.ErrCredUnavailable) {
|
||||
return nil, errSourceUnavailable
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
vendor/cloud.google.com/go/auth/internal/transport/cert/secureconnect_cert.go
generated
vendored
Normal file
124
vendor/cloud.google.com/go/auth/internal/transport/cert/secureconnect_cert.go
generated
vendored
Normal file
|
|
@ -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 {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Config file missing means Secure Connect is not supported.
|
||||
return nil, errSourceUnavailable
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
// 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 (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth/internal/transport/cert"
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
configEndpointSuffix = "instance/platform-security/auto-mtls-configuration"
|
||||
)
|
||||
|
||||
var (
|
||||
// The period an MTLS config can be reused before needing refresh.
|
||||
configExpiry = time.Hour
|
||||
|
||||
// mtlsEndpointEnabledForS2A checks if the endpoint is indeed MTLS-enabled, so that we can use S2A for MTLS connection.
|
||||
mtlsEndpointEnabledForS2A = func() bool {
|
||||
// TODO(xmenxk): determine this via discovery config.
|
||||
return true
|
||||
}
|
||||
|
||||
// mdsMTLSAutoConfigSource is an instance of reuseMTLSConfigSource, with metadataMTLSAutoConfig as its config source.
|
||||
mtlsOnce sync.Once
|
||||
)
|
||||
|
||||
// GetS2AAddress returns the S2A address to be reached via plaintext connection.
|
||||
// Returns empty string if not set or invalid.
|
||||
func GetS2AAddress() string {
|
||||
c, err := getMetadataMTLSAutoConfig().Config()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if !c.Valid() {
|
||||
return ""
|
||||
}
|
||||
return c.S2A.PlaintextAddress
|
||||
}
|
||||
|
||||
type mtlsConfigSource interface {
|
||||
Config() (*mtlsConfig, error)
|
||||
}
|
||||
|
||||
// mtlsConfig contains the configuration for establishing MTLS connections with Google APIs.
|
||||
type mtlsConfig struct {
|
||||
S2A *s2aAddresses `json:"s2a"`
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
func (c *mtlsConfig) Valid() bool {
|
||||
return c != nil && c.S2A != nil && !c.expired()
|
||||
}
|
||||
func (c *mtlsConfig) expired() bool {
|
||||
return c.Expiry.Before(time.Now())
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// getMetadataMTLSAutoConfig returns mdsMTLSAutoConfigSource, which is backed by config from MDS with auto-refresh.
|
||||
func getMetadataMTLSAutoConfig() mtlsConfigSource {
|
||||
mtlsOnce.Do(func() {
|
||||
mdsMTLSAutoConfigSource = &reuseMTLSConfigSource{
|
||||
src: &metadataMTLSAutoConfig{},
|
||||
}
|
||||
})
|
||||
return mdsMTLSAutoConfigSource
|
||||
}
|
||||
|
||||
// reuseMTLSConfigSource caches a valid version of mtlsConfig, and uses `src` to refresh upon config expiry.
|
||||
// It implements the mtlsConfigSource interface, so calling Config() on it returns an mtlsConfig.
|
||||
type reuseMTLSConfigSource struct {
|
||||
src mtlsConfigSource // src.Config() is called when config is expired
|
||||
mu sync.Mutex // mutex guards config
|
||||
config *mtlsConfig // cached config
|
||||
}
|
||||
|
||||
func (cs *reuseMTLSConfigSource) Config() (*mtlsConfig, error) {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
if cs.config.Valid() {
|
||||
return cs.config, nil
|
||||
}
|
||||
c, err := cs.src.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cs.config = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// metadataMTLSAutoConfig is an implementation of the interface mtlsConfigSource
|
||||
// It has the logic to query MDS and return an mtlsConfig
|
||||
type metadataMTLSAutoConfig struct{}
|
||||
|
||||
var httpGetMetadataMTLSConfig = func() (string, error) {
|
||||
return metadata.Get(configEndpointSuffix)
|
||||
}
|
||||
|
||||
func (cs *metadataMTLSAutoConfig) Config() (*mtlsConfig, error) {
|
||||
resp, err := httpGetMetadataMTLSConfig()
|
||||
if err != nil {
|
||||
log.Printf("querying MTLS config from MDS endpoint failed: %v", err)
|
||||
return defaultMTLSConfig(), nil
|
||||
}
|
||||
var config mtlsConfig
|
||||
err = json.Unmarshal([]byte(resp), &config)
|
||||
if err != nil {
|
||||
log.Printf("unmarshalling MTLS config from MDS endpoint failed: %v", err)
|
||||
return defaultMTLSConfig(), nil
|
||||
}
|
||||
|
||||
if config.S2A == nil {
|
||||
log.Printf("returned MTLS config from MDS endpoint is invalid: %v", config)
|
||||
return defaultMTLSConfig(), nil
|
||||
}
|
||||
|
||||
// set new expiry
|
||||
config.Expiry = time.Now().Add(configExpiry)
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func defaultMTLSConfig() *mtlsConfig {
|
||||
return &mtlsConfig{
|
||||
S2A: &s2aAddresses{
|
||||
PlaintextAddress: "",
|
||||
MTLSAddress: "",
|
||||
},
|
||||
Expiry: time.Now().Add(configExpiry),
|
||||
}
|
||||
}
|
||||
|
||||
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 and no endpoint override, skip S2A.
|
||||
if opts.DefaultMTLSEndpoint == "" && opts.Endpoint == "" {
|
||||
return false
|
||||
}
|
||||
// If MTLS is not enabled for this endpoint, skip S2A.
|
||||
if !mtlsEndpointEnabledForS2A() {
|
||||
return false
|
||||
}
|
||||
// If custom HTTP client is provided, skip S2A.
|
||||
if opts.Client != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isGoogleS2AEnabled() bool {
|
||||
b, err := strconv.ParseBool(os.Getenv(googleAPIUseS2AEnv))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"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,
|
||||
AuthHandlerOptions: oldDo.AuthHandlerOptions,
|
||||
}
|
||||
|
||||
// Smartly size this memory and copy below.
|
||||
if oldDo.CredentialsJSON != nil {
|
||||
newDo.CredentialsJSON = make([]byte, len(oldDo.CredentialsJSON))
|
||||
copy(newDo.CredentialsJSON, oldDo.CredentialsJSON)
|
||||
}
|
||||
if oldDo.Scopes != nil {
|
||||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# Changelog
|
||||
|
||||
## [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))
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
return &auth.Token{
|
||||
Value: tok.AccessToken,
|
||||
Type: tok.Type(),
|
||||
Expiry: tok.Expiry,
|
||||
}, 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
|
||||
}
|
||||
return &oauth2.Token{
|
||||
AccessToken: tok.Value,
|
||||
TokenType: tok.Type,
|
||||
Expiry: tok.Expiry,
|
||||
}, 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
// 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"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth/internal"
|
||||
)
|
||||
|
||||
// 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 10 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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.CloneDefaultClient()
|
||||
}
|
||||
|
||||
// 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.NewRequest("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))
|
||||
}
|
||||
|
||||
// Make request
|
||||
r, err := o.client().Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, refreshToken, err
|
||||
}
|
||||
body, err := internal.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
if err != nil {
|
||||
return nil, refreshToken, fmt.Errorf("auth: cannot fetch token: %w", err)
|
||||
}
|
||||
|
||||
failureStatus := r.StatusCode < 200 || r.StatusCode > 299
|
||||
tokError := &Error{
|
||||
Response: r,
|
||||
Body: body,
|
||||
}
|
||||
|
||||
var token *Token
|
||||
// errors ignored because of default switch on content
|
||||
content, _, _ := mime.ParseMediaType(r.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
|
||||
}
|
||||
|
|
@ -1,5 +1,12 @@
|
|||
# Changes
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
|
@ -95,9 +95,9 @@ func (c *cachedValue) get(cl *Client) (v string, err error) {
|
|||
return c.v, nil
|
||||
}
|
||||
if c.trim {
|
||||
v, err = cl.getTrimmed(c.k)
|
||||
v, err = cl.getTrimmed(context.Background(), c.k)
|
||||
} else {
|
||||
v, err = cl.Get(c.k)
|
||||
v, err = cl.GetWithContext(context.Background(), c.k)
|
||||
}
|
||||
if err == nil {
|
||||
c.v = v
|
||||
|
|
@ -197,18 +197,32 @@ func systemInfoSuggestsGCE() bool {
|
|||
// We don't have any non-Linux clues available, at least yet.
|
||||
return false
|
||||
}
|
||||
slurp, _ := ioutil.ReadFile("/sys/class/dmi/id/product_name")
|
||||
slurp, _ := os.ReadFile("/sys/class/dmi/id/product_name")
|
||||
name := strings.TrimSpace(string(slurp))
|
||||
return name == "Google" || name == "Google Compute Engine"
|
||||
}
|
||||
|
||||
// Subscribe calls Client.Subscribe on the default client.
|
||||
// Subscribe calls Client.SubscribeWithContext on the default client.
|
||||
func Subscribe(suffix string, fn func(v string, ok bool) error) error {
|
||||
return defaultClient.Subscribe(suffix, fn)
|
||||
return defaultClient.SubscribeWithContext(context.Background(), suffix, func(ctx context.Context, v string, ok bool) error { return fn(v, ok) })
|
||||
}
|
||||
|
||||
// Get calls Client.Get on the default client.
|
||||
func Get(suffix string) (string, error) { return defaultClient.Get(suffix) }
|
||||
// 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.
|
||||
func ProjectID() (string, error) { return defaultClient.ProjectID() }
|
||||
|
|
@ -288,8 +302,7 @@ func NewClient(c *http.Client) *Client {
|
|||
|
||||
// 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(suffix string) (value, etag string, err error) {
|
||||
ctx := context.TODO()
|
||||
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
|
||||
|
|
@ -306,7 +319,7 @@ func (c *Client) getETag(suffix string) (value, etag string, err error) {
|
|||
}
|
||||
suffix = strings.TrimLeft(suffix, "/")
|
||||
u := "http://" + host + "/computeMetadata/v1/" + suffix
|
||||
req, err := http.NewRequest("GET", u, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
|
@ -336,7 +349,7 @@ func (c *Client) getETag(suffix string) (value, etag string, err error) {
|
|||
if res.StatusCode == http.StatusNotFound {
|
||||
return "", "", NotDefinedError(suffix)
|
||||
}
|
||||
all, err := ioutil.ReadAll(res.Body)
|
||||
all, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
|
@ -354,19 +367,33 @@ func (c *Client) getETag(suffix string) (value, etag string, err error) {
|
|||
//
|
||||
// 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) {
|
||||
val, _, err := c.getETag(suffix)
|
||||
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.
|
||||
func (c *Client) GetWithContext(ctx context.Context, suffix string) (string, error) {
|
||||
val, _, err := c.getETag(ctx, suffix)
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (c *Client) getTrimmed(suffix string) (s string, err error) {
|
||||
s, err = c.Get(suffix)
|
||||
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(suffix string) ([]string, error) {
|
||||
j, err := c.Get(suffix)
|
||||
j, err := c.GetWithContext(context.Background(), suffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -388,7 +415,7 @@ func (c *Client) InstanceID() (string, error) { return instID.get(c) }
|
|||
|
||||
// InternalIP returns the instance's primary internal IP address.
|
||||
func (c *Client) InternalIP() (string, error) {
|
||||
return c.getTrimmed("instance/network-interfaces/0/ip")
|
||||
return c.getTrimmed(context.Background(), "instance/network-interfaces/0/ip")
|
||||
}
|
||||
|
||||
// Email returns the email address associated with the service account.
|
||||
|
|
@ -398,25 +425,25 @@ func (c *Client) Email(serviceAccount string) (string, error) {
|
|||
if serviceAccount == "" {
|
||||
serviceAccount = "default"
|
||||
}
|
||||
return c.getTrimmed("instance/service-accounts/" + serviceAccount + "/email")
|
||||
return c.getTrimmed(context.Background(), "instance/service-accounts/"+serviceAccount+"/email")
|
||||
}
|
||||
|
||||
// ExternalIP returns the instance's primary external (public) IP address.
|
||||
func (c *Client) ExternalIP() (string, error) {
|
||||
return c.getTrimmed("instance/network-interfaces/0/access-configs/0/external-ip")
|
||||
return c.getTrimmed(context.Background(), "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".
|
||||
func (c *Client) Hostname() (string, error) {
|
||||
return c.getTrimmed("instance/hostname")
|
||||
return c.getTrimmed(context.Background(), "instance/hostname")
|
||||
}
|
||||
|
||||
// InstanceTags returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
func (c *Client) InstanceTags() ([]string, error) {
|
||||
var s []string
|
||||
j, err := c.Get("instance/tags")
|
||||
j, err := c.GetWithContext(context.Background(), "instance/tags")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -428,12 +455,12 @@ func (c *Client) InstanceTags() ([]string, error) {
|
|||
|
||||
// InstanceName returns the current VM's instance ID string.
|
||||
func (c *Client) InstanceName() (string, error) {
|
||||
return c.getTrimmed("instance/name")
|
||||
return c.getTrimmed(context.Background(), "instance/name")
|
||||
}
|
||||
|
||||
// Zone returns the current VM's zone, such as "us-central1-b".
|
||||
func (c *Client) Zone() (string, error) {
|
||||
zone, err := c.getTrimmed("instance/zone")
|
||||
zone, err := c.getTrimmed(context.Background(), "instance/zone")
|
||||
// zone is of the form "projects/<projNum>/zones/<zoneName>".
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
@ -460,7 +487,7 @@ func (c *Client) ProjectAttributes() ([]string, error) { return c.lines("project
|
|||
// InstanceAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
func (c *Client) InstanceAttributeValue(attr string) (string, error) {
|
||||
return c.Get("instance/attributes/" + attr)
|
||||
return c.GetWithContext(context.Background(), "instance/attributes/"+attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValue returns the value of the provided
|
||||
|
|
@ -472,7 +499,7 @@ func (c *Client) InstanceAttributeValue(attr string) (string, error) {
|
|||
// ProjectAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
func (c *Client) ProjectAttributeValue(attr string) (string, error) {
|
||||
return c.Get("project/attributes/" + attr)
|
||||
return c.GetWithContext(context.Background(), "project/attributes/"+attr)
|
||||
}
|
||||
|
||||
// Scopes returns the service account scopes for the given account.
|
||||
|
|
@ -489,21 +516,30 @@ func (c *Client) Scopes(serviceAccount string) ([]string, error) {
|
|||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
// The suffix may contain query parameters.
|
||||
//
|
||||
// Subscribe 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.
|
||||
// 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(suffix)
|
||||
val, lastETag, err := c.getETag(ctx, suffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fn(val, true); err != nil {
|
||||
if err := fn(ctx, val, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -514,7 +550,7 @@ func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) erro
|
|||
suffix += "?wait_for_change=true&last_etag="
|
||||
}
|
||||
for {
|
||||
val, etag, err := c.getETag(suffix + url.QueryEscape(lastETag))
|
||||
val, etag, err := c.getETag(ctx, suffix+url.QueryEscape(lastETag))
|
||||
if err != nil {
|
||||
if _, deleted := err.(NotDefinedError); !deleted {
|
||||
time.Sleep(failedSubscribeSleep)
|
||||
|
|
@ -524,7 +560,7 @@ func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) erro
|
|||
}
|
||||
lastETag = etag
|
||||
|
||||
if err := fn(val, ok); err != nil || !ok {
|
||||
if err := fn(ctx, val, ok); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
syscallRetryable = func(err error) bool { return false }
|
||||
syscallRetryable = func(error) bool { return false }
|
||||
)
|
||||
|
||||
// defaultBackoff is basically equivalent to gax.Backoff without the need for
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
// 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.
|
||||
|
||||
// This file, and the {{.RootMod}} import, won't actually become part of
|
||||
// the resultant binary.
|
||||
//go:build modhack
|
||||
// +build modhack
|
||||
|
||||
package metadata
|
||||
|
||||
// Necessary for safely adding multi-module repo. See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository
|
||||
import _ "cloud.google.com/go/compute/internal"
|
||||
|
|
@ -15,6 +15,8 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth/credentials"
|
||||
"cloud.google.com/go/auth/oauth2adapt"
|
||||
"golang.org/x/oauth2"
|
||||
"google.golang.org/api/internal/cert"
|
||||
"google.golang.org/api/internal/impersonate"
|
||||
|
|
@ -27,6 +29,9 @@ const quotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
|
|||
// Creds returns credential information obtained from DialSettings, or if none, then
|
||||
// it returns default credential information.
|
||||
func Creds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
|
||||
if ds.IsNewAuthLibraryEnabled() {
|
||||
return credsNewAuth(ctx, ds)
|
||||
}
|
||||
creds, err := baseCreds(ctx, ds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -37,6 +42,59 @@ func Creds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
|
|||
return creds, nil
|
||||
}
|
||||
|
||||
func credsNewAuth(ctx context.Context, settings *DialSettings) (*google.Credentials, error) {
|
||||
// Preserve old options behavior
|
||||
if settings.InternalCredentials != nil {
|
||||
return settings.InternalCredentials, nil
|
||||
} else if settings.Credentials != nil {
|
||||
return settings.Credentials, nil
|
||||
} else if settings.TokenSource != nil {
|
||||
return &google.Credentials{TokenSource: settings.TokenSource}, nil
|
||||
}
|
||||
|
||||
if settings.AuthCredentials != nil {
|
||||
return oauth2adapt.Oauth2CredentialsFromAuthCredentials(settings.AuthCredentials), nil
|
||||
}
|
||||
|
||||
var useSelfSignedJWT bool
|
||||
var aud string
|
||||
var scopes []string
|
||||
// If scoped JWTs are enabled user provided an aud, allow self-signed JWT.
|
||||
if settings.EnableJwtWithScope || len(settings.Audiences) > 0 {
|
||||
useSelfSignedJWT = true
|
||||
}
|
||||
|
||||
if len(settings.Scopes) > 0 {
|
||||
scopes = make([]string, len(settings.Scopes))
|
||||
copy(scopes, settings.Scopes)
|
||||
}
|
||||
if len(settings.Audiences) > 0 {
|
||||
aud = settings.Audiences[0]
|
||||
}
|
||||
// Only default scopes if user did not also set an audience.
|
||||
if len(settings.Scopes) == 0 && aud == "" && len(settings.DefaultScopes) > 0 {
|
||||
scopes = make([]string, len(settings.DefaultScopes))
|
||||
copy(scopes, settings.DefaultScopes)
|
||||
}
|
||||
if len(scopes) == 0 && aud == "" {
|
||||
aud = settings.DefaultAudience
|
||||
}
|
||||
|
||||
creds, err := credentials.DetectDefault(&credentials.DetectOptions{
|
||||
Scopes: scopes,
|
||||
Audience: aud,
|
||||
CredentialsFile: settings.CredentialsFile,
|
||||
CredentialsJSON: settings.CredentialsJSON,
|
||||
UseSelfSignedJWT: useSelfSignedJWT,
|
||||
Client: oauth2.NewClient(ctx, nil),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return oauth2adapt.Oauth2CredentialsFromAuthCredentials(creds), nil
|
||||
}
|
||||
|
||||
func baseCreds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
|
||||
if ds.InternalCredentials != nil {
|
||||
return ds.InternalCredentials, nil
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"google.golang.org/api/internal/impersonate"
|
||||
|
|
@ -20,8 +21,10 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
newAuthLibEnVar = "GOOGLE_API_GO_EXPERIMENTAL_USE_NEW_AUTH_LIB"
|
||||
universeDomainDefault = "googleapis.com"
|
||||
newAuthLibEnvVar = "GOOGLE_API_GO_EXPERIMENTAL_ENABLE_NEW_AUTH_LIB"
|
||||
newAuthLibDisabledEnVar = "GOOGLE_API_GO_EXPERIMENTAL_DISABLE_NEW_AUTH_LIB"
|
||||
universeDomainEnvVar = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
|
||||
defaultUniverseDomain = "googleapis.com"
|
||||
)
|
||||
|
||||
// DialSettings holds information needed to establish a connection with a
|
||||
|
|
@ -56,15 +59,17 @@ type DialSettings struct {
|
|||
ImpersonationConfig *impersonate.Config
|
||||
EnableDirectPath bool
|
||||
EnableDirectPathXds bool
|
||||
EnableNewAuthLibrary bool
|
||||
AllowNonDefaultServiceAccount bool
|
||||
UniverseDomain string
|
||||
DefaultUniverseDomain string
|
||||
|
||||
UniverseDomain string
|
||||
// Google API system parameters. For more information please read:
|
||||
// https://cloud.google.com/apis/docs/system-parameters
|
||||
QuotaProject string
|
||||
RequestReason string
|
||||
|
||||
// New Auth library Options
|
||||
AuthCredentials *auth.Credentials
|
||||
EnableNewAuthLibrary bool
|
||||
}
|
||||
|
||||
// GetScopes returns the user-provided scopes, if set, or else falls back to the
|
||||
|
|
@ -91,10 +96,15 @@ func (ds *DialSettings) HasCustomAudience() bool {
|
|||
|
||||
// IsNewAuthLibraryEnabled returns true if the new auth library should be used.
|
||||
func (ds *DialSettings) IsNewAuthLibraryEnabled() bool {
|
||||
// Disabled env is for future rollouts to make sure there is a way to easily
|
||||
// disable this behaviour once we switch in on by default.
|
||||
if b, err := strconv.ParseBool(os.Getenv(newAuthLibDisabledEnVar)); err == nil && b {
|
||||
return false
|
||||
}
|
||||
if ds.EnableNewAuthLibrary {
|
||||
return true
|
||||
}
|
||||
if b, err := strconv.ParseBool(os.Getenv(newAuthLibEnVar)); err == nil {
|
||||
if b, err := strconv.ParseBool(os.Getenv(newAuthLibEnvVar)); err == nil {
|
||||
return b
|
||||
}
|
||||
return false
|
||||
|
|
@ -165,31 +175,32 @@ func (ds *DialSettings) Validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetDefaultUniverseDomain returns the default service domain for a given Cloud
|
||||
// universe, as configured with internaloption.WithDefaultUniverseDomain.
|
||||
// The default value is "googleapis.com".
|
||||
// GetDefaultUniverseDomain returns the Google default universe domain
|
||||
// ("googleapis.com").
|
||||
func (ds *DialSettings) GetDefaultUniverseDomain() string {
|
||||
if ds.DefaultUniverseDomain == "" {
|
||||
return universeDomainDefault
|
||||
}
|
||||
return ds.DefaultUniverseDomain
|
||||
return defaultUniverseDomain
|
||||
}
|
||||
|
||||
// GetUniverseDomain returns the default service domain for a given Cloud
|
||||
// universe, as configured with option.WithUniverseDomain.
|
||||
// The default value is the value of GetDefaultUniverseDomain, as configured
|
||||
// with internaloption.WithDefaultUniverseDomain.
|
||||
// universe, with the following precedence:
|
||||
//
|
||||
// 1. A non-empty option.WithUniverseDomain.
|
||||
// 2. A non-empty environment variable GOOGLE_CLOUD_UNIVERSE_DOMAIN.
|
||||
// 3. The default value "googleapis.com".
|
||||
func (ds *DialSettings) GetUniverseDomain() string {
|
||||
if ds.UniverseDomain == "" {
|
||||
return ds.GetDefaultUniverseDomain()
|
||||
if ds.UniverseDomain != "" {
|
||||
return ds.UniverseDomain
|
||||
}
|
||||
return ds.UniverseDomain
|
||||
if envUD := os.Getenv(universeDomainEnvVar); envUD != "" {
|
||||
return envUD
|
||||
}
|
||||
return defaultUniverseDomain
|
||||
}
|
||||
|
||||
// IsUniverseDomainGDU returns true if the universe domain is the default Google
|
||||
// universe.
|
||||
// universe ("googleapis.com").
|
||||
func (ds *DialSettings) IsUniverseDomainGDU() bool {
|
||||
return ds.GetUniverseDomain() == ds.GetDefaultUniverseDomain()
|
||||
return ds.GetUniverseDomain() == defaultUniverseDomain
|
||||
}
|
||||
|
||||
// GetUniverseDomain returns the default service domain for a given Cloud
|
||||
|
|
@ -215,7 +226,7 @@ func GetUniverseDomain(creds *google.Credentials) (string, error) {
|
|||
case <-errors:
|
||||
// An error that is returned before the timer expires is likely to be
|
||||
// connection refused. Temporarily (2024-03-21) return the GDU domain.
|
||||
return universeDomainDefault, nil
|
||||
return defaultUniverseDomain, nil
|
||||
case res := <-results:
|
||||
return res, nil
|
||||
case <-timer.C: // Timer is expired.
|
||||
|
|
@ -227,6 +238,6 @@ func GetUniverseDomain(creds *google.Credentials) (string, error) {
|
|||
// calls to creds.GetUniverseDomain() in grpc/dial.go and http/dial.go
|
||||
// and remove this method to close
|
||||
// https://github.com/googleapis/google-api-go-client/issues/2399.
|
||||
return universeDomainDefault, nil
|
||||
return defaultUniverseDomain, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@
|
|||
package internal
|
||||
|
||||
// Version is the current tagged release of the library.
|
||||
const Version = "0.172.0"
|
||||
const Version = "0.175.0"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"crypto/tls"
|
||||
"net/http"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"google.golang.org/api/internal"
|
||||
|
|
@ -344,6 +345,19 @@ func WithCredentials(creds *google.Credentials) ClientOption {
|
|||
return (*withCreds)(creds)
|
||||
}
|
||||
|
||||
// WithAuthCredentials returns a ClientOption that specifies an
|
||||
// [cloud.google.com/go/auth.Credentials] to be used as the basis for
|
||||
// authentication.
|
||||
func WithAuthCredentials(creds *auth.Credentials) ClientOption {
|
||||
return withAuthCredentials{creds}
|
||||
}
|
||||
|
||||
type withAuthCredentials struct{ creds *auth.Credentials }
|
||||
|
||||
func (w withAuthCredentials) Apply(o *internal.DialSettings) {
|
||||
o.AuthCredentials = w.creds
|
||||
}
|
||||
|
||||
// WithUniverseDomain returns a ClientOption that sets the universe domain.
|
||||
//
|
||||
// This is an EXPERIMENTAL API and may be changed or removed in the future.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/credentials"
|
||||
"cloud.google.com/go/auth/grpctransport"
|
||||
"cloud.google.com/go/auth/oauth2adapt"
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
"go.opencensus.io/plugin/ocgrpc"
|
||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||
|
|
@ -79,6 +83,13 @@ func Dial(ctx context.Context, opts ...option.ClientOption) (*grpc.ClientConn, e
|
|||
if o.GRPCConnPool != nil {
|
||||
return o.GRPCConnPool.Conn(), nil
|
||||
}
|
||||
if o.IsNewAuthLibraryEnabled() {
|
||||
pool, err := dialPoolNewAuth(ctx, true, 1, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pool.Connection(), nil
|
||||
}
|
||||
// NOTE(cbro): We removed support for option.WithGRPCConnPool (GRPCConnPoolSize)
|
||||
// on 2020-02-12 because RoundRobin and WithBalancer are deprecated and we need to remove usages of it.
|
||||
//
|
||||
|
|
@ -94,6 +105,13 @@ func DialInsecure(ctx context.Context, opts ...option.ClientOption) (*grpc.Clien
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if o.IsNewAuthLibraryEnabled() {
|
||||
pool, err := dialPoolNewAuth(ctx, false, 1, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pool.Connection(), nil
|
||||
}
|
||||
return dial(ctx, true, o)
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +130,18 @@ func DialPool(ctx context.Context, opts ...option.ClientOption) (ConnPool, error
|
|||
if o.GRPCConnPool != nil {
|
||||
return o.GRPCConnPool, nil
|
||||
}
|
||||
|
||||
if o.IsNewAuthLibraryEnabled() {
|
||||
if o.GRPCConn != nil {
|
||||
return &singleConnPool{o.GRPCConn}, nil
|
||||
}
|
||||
pool, err := dialPoolNewAuth(ctx, true, o.GRPCConnPoolSize, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &poolAdapter{pool}, nil
|
||||
}
|
||||
|
||||
poolSize := o.GRPCConnPoolSize
|
||||
if o.GRPCConn != nil {
|
||||
// WithGRPCConn is technically incompatible with WithGRPCConnectionPool.
|
||||
|
|
@ -141,6 +171,83 @@ func DialPool(ctx context.Context, opts ...option.ClientOption) (ConnPool, error
|
|||
return pool, nil
|
||||
}
|
||||
|
||||
// dialPoolNewAuth is an adapter to call new auth library.
|
||||
func dialPoolNewAuth(ctx context.Context, secure bool, poolSize int, ds *internal.DialSettings) (grpctransport.GRPCClientConnPool, error) {
|
||||
// honor options if set
|
||||
var creds *auth.Credentials
|
||||
if ds.InternalCredentials != nil {
|
||||
creds = oauth2adapt.AuthCredentialsFromOauth2Credentials(ds.InternalCredentials)
|
||||
} else if ds.Credentials != nil {
|
||||
creds = oauth2adapt.AuthCredentialsFromOauth2Credentials(ds.Credentials)
|
||||
} else if ds.AuthCredentials != nil {
|
||||
creds = ds.AuthCredentials
|
||||
} else if ds.TokenSource != nil {
|
||||
credOpts := &auth.CredentialsOptions{
|
||||
TokenProvider: oauth2adapt.TokenProviderFromTokenSource(ds.TokenSource),
|
||||
}
|
||||
if ds.QuotaProject != "" {
|
||||
credOpts.QuotaProjectIDProvider = auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
|
||||
return ds.QuotaProject, nil
|
||||
})
|
||||
}
|
||||
creds = auth.NewCredentials(credOpts)
|
||||
}
|
||||
|
||||
var skipValidation bool
|
||||
// If our clients explicitly setup the credential skip validation as it is
|
||||
// assumed correct
|
||||
if ds.SkipValidation || ds.InternalCredentials != nil {
|
||||
skipValidation = true
|
||||
}
|
||||
|
||||
var aud string
|
||||
if len(ds.Audiences) > 0 {
|
||||
aud = ds.Audiences[0]
|
||||
}
|
||||
metadata := map[string]string{}
|
||||
if ds.QuotaProject != "" {
|
||||
metadata["X-goog-user-project"] = ds.QuotaProject
|
||||
}
|
||||
if ds.RequestReason != "" {
|
||||
metadata["X-goog-request-reason"] = ds.RequestReason
|
||||
}
|
||||
|
||||
// Defaults for older clients that don't set this value yet
|
||||
var defaultEndpointTemplate string
|
||||
if ds.DefaultEndpointTemplate == "" {
|
||||
defaultEndpointTemplate = ds.DefaultEndpoint
|
||||
}
|
||||
|
||||
pool, err := grpctransport.Dial(ctx, secure, &grpctransport.Options{
|
||||
DisableTelemetry: ds.TelemetryDisabled,
|
||||
DisableAuthentication: ds.NoAuth,
|
||||
Endpoint: ds.Endpoint,
|
||||
Metadata: metadata,
|
||||
GRPCDialOpts: ds.GRPCDialOpts,
|
||||
PoolSize: poolSize,
|
||||
Credentials: creds,
|
||||
DetectOpts: &credentials.DetectOptions{
|
||||
Scopes: ds.Scopes,
|
||||
Audience: aud,
|
||||
CredentialsFile: ds.CredentialsFile,
|
||||
CredentialsJSON: ds.CredentialsJSON,
|
||||
Client: oauth2.NewClient(ctx, nil),
|
||||
},
|
||||
InternalOptions: &grpctransport.InternalOptions{
|
||||
EnableNonDefaultSAForDirectPath: ds.AllowNonDefaultServiceAccount,
|
||||
EnableDirectPath: ds.EnableDirectPath,
|
||||
EnableDirectPathXds: ds.EnableDirectPathXds,
|
||||
EnableJWTWithScope: ds.EnableJwtWithScope,
|
||||
DefaultAudience: ds.DefaultAudience,
|
||||
DefaultEndpointTemplate: defaultEndpointTemplate,
|
||||
DefaultMTLSEndpoint: ds.DefaultMTLSEndpoint,
|
||||
DefaultScopes: ds.DefaultScopes,
|
||||
SkipValidation: skipValidation,
|
||||
},
|
||||
})
|
||||
return pool, err
|
||||
}
|
||||
|
||||
func dial(ctx context.Context, insecure bool, o *internal.DialSettings) (*grpc.ClientConn, error) {
|
||||
if o.HTTPClient != nil {
|
||||
return nil, errors.New("unsupported HTTP client specified")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"cloud.google.com/go/auth/grpctransport"
|
||||
"google.golang.org/api/internal"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
|
@ -90,3 +91,27 @@ func (m multiError) Error() string {
|
|||
}
|
||||
return fmt.Sprintf("%s (and %d other errors)", s, n-1)
|
||||
}
|
||||
|
||||
type poolAdapter struct {
|
||||
pool grpctransport.GRPCClientConnPool
|
||||
}
|
||||
|
||||
func (p *poolAdapter) Conn() *grpc.ClientConn {
|
||||
return p.pool.Connection()
|
||||
}
|
||||
|
||||
func (p *poolAdapter) Num() int {
|
||||
return p.pool.Len()
|
||||
}
|
||||
|
||||
func (p *poolAdapter) Close() error {
|
||||
return p.pool.Close()
|
||||
}
|
||||
|
||||
func (p *poolAdapter) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error {
|
||||
return p.pool.Invoke(ctx, method, args, reply, opts...)
|
||||
}
|
||||
|
||||
func (p *poolAdapter) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
return p.pool.NewStream(ctx, desc, method, opts...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/auth"
|
||||
"cloud.google.com/go/auth/credentials"
|
||||
"cloud.google.com/go/auth/httptransport"
|
||||
"cloud.google.com/go/auth/oauth2adapt"
|
||||
"go.opencensus.io/plugin/ochttp"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"golang.org/x/net/http2"
|
||||
|
|
@ -43,6 +47,13 @@ func NewClient(ctx context.Context, opts ...option.ClientOption) (*http.Client,
|
|||
return settings.HTTPClient, endpoint, nil
|
||||
}
|
||||
|
||||
if settings.IsNewAuthLibraryEnabled() {
|
||||
client, err := newClientNewAuth(ctx, settings)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return client, endpoint, nil
|
||||
}
|
||||
trans, err := newTransport(ctx, defaultBaseTransport(ctx, clientCertSource, dialTLSContext), settings)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
|
|
@ -50,6 +61,82 @@ func NewClient(ctx context.Context, opts ...option.ClientOption) (*http.Client,
|
|||
return &http.Client{Transport: trans}, endpoint, nil
|
||||
}
|
||||
|
||||
// newClientNewAuth is an adapter to call new auth library.
|
||||
func newClientNewAuth(ctx context.Context, ds *internal.DialSettings) (*http.Client, error) {
|
||||
// honor options if set
|
||||
var creds *auth.Credentials
|
||||
if ds.InternalCredentials != nil {
|
||||
creds = oauth2adapt.AuthCredentialsFromOauth2Credentials(ds.InternalCredentials)
|
||||
} else if ds.Credentials != nil {
|
||||
creds = oauth2adapt.AuthCredentialsFromOauth2Credentials(ds.Credentials)
|
||||
} else if ds.AuthCredentials != nil {
|
||||
creds = ds.AuthCredentials
|
||||
} else if ds.TokenSource != nil {
|
||||
credOpts := &auth.CredentialsOptions{
|
||||
TokenProvider: oauth2adapt.TokenProviderFromTokenSource(ds.TokenSource),
|
||||
}
|
||||
if ds.QuotaProject != "" {
|
||||
credOpts.QuotaProjectIDProvider = auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
|
||||
return ds.QuotaProject, nil
|
||||
})
|
||||
}
|
||||
creds = auth.NewCredentials(credOpts)
|
||||
}
|
||||
|
||||
var skipValidation bool
|
||||
// If our clients explicitly setup the credential skip validation as it is
|
||||
// assumed correct
|
||||
if ds.SkipValidation || ds.InternalCredentials != nil {
|
||||
skipValidation = true
|
||||
}
|
||||
|
||||
// Defaults for older clients that don't set this value yet
|
||||
var defaultEndpointTemplate string
|
||||
if ds.DefaultEndpointTemplate == "" {
|
||||
defaultEndpointTemplate = ds.DefaultEndpoint
|
||||
}
|
||||
|
||||
var aud string
|
||||
if len(ds.Audiences) > 0 {
|
||||
aud = ds.Audiences[0]
|
||||
}
|
||||
headers := http.Header{}
|
||||
if ds.QuotaProject != "" {
|
||||
headers.Set("X-goog-user-project", ds.QuotaProject)
|
||||
}
|
||||
if ds.RequestReason != "" {
|
||||
headers.Set("X-goog-request-reason", ds.RequestReason)
|
||||
}
|
||||
client, err := httptransport.NewClient(&httptransport.Options{
|
||||
DisableTelemetry: ds.TelemetryDisabled,
|
||||
DisableAuthentication: ds.NoAuth,
|
||||
Headers: headers,
|
||||
Endpoint: ds.Endpoint,
|
||||
APIKey: ds.APIKey,
|
||||
Credentials: creds,
|
||||
ClientCertProvider: ds.ClientCertSource,
|
||||
DetectOpts: &credentials.DetectOptions{
|
||||
Scopes: ds.Scopes,
|
||||
Audience: aud,
|
||||
CredentialsFile: ds.CredentialsFile,
|
||||
CredentialsJSON: ds.CredentialsJSON,
|
||||
Client: oauth2.NewClient(ctx, nil),
|
||||
},
|
||||
InternalOptions: &httptransport.InternalOptions{
|
||||
EnableJWTWithScope: ds.EnableJwtWithScope,
|
||||
DefaultAudience: ds.DefaultAudience,
|
||||
DefaultEndpointTemplate: defaultEndpointTemplate,
|
||||
DefaultMTLSEndpoint: ds.DefaultMTLSEndpoint,
|
||||
DefaultScopes: ds.DefaultScopes,
|
||||
SkipValidation: skipValidation,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// NewTransport creates an http.RoundTripper for use communicating with a Google
|
||||
// cloud service, configured with the given ClientOptions. Its RoundTrip method delegates to base.
|
||||
func NewTransport(ctx context.Context, base http.RoundTripper, opts ...option.ClientOption) (http.RoundTripper, error) {
|
||||
|
|
@ -60,6 +147,14 @@ func NewTransport(ctx context.Context, base http.RoundTripper, opts ...option.Cl
|
|||
if settings.HTTPClient != nil {
|
||||
return nil, errors.New("transport/http: WithHTTPClient passed to NewTransport")
|
||||
}
|
||||
if settings.IsNewAuthLibraryEnabled() {
|
||||
// TODO, this is not wrapping the base, find a way...
|
||||
client, err := newClientNewAuth(ctx, settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.Transport, nil
|
||||
}
|
||||
return newTransport(ctx, base, settings)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,13 +54,14 @@ var (
|
|||
// an init() function), and is not thread-safe. If multiple Balancers are
|
||||
// registered with the same name, the one registered last will take effect.
|
||||
func Register(b Builder) {
|
||||
if strings.ToLower(b.Name()) != b.Name() {
|
||||
name := strings.ToLower(b.Name())
|
||||
if name != b.Name() {
|
||||
// TODO: Skip the use of strings.ToLower() to index the map after v1.59
|
||||
// is released to switch to case sensitive balancer registry. Also,
|
||||
// remove this warning and update the docstrings for Register and Get.
|
||||
logger.Warningf("Balancer registered with name %q. grpc-go will be switching to case sensitive balancer registries soon", b.Name())
|
||||
}
|
||||
m[strings.ToLower(b.Name())] = b
|
||||
m[name] = b
|
||||
}
|
||||
|
||||
// unregisterForTesting deletes the balancer with the given name from the
|
||||
|
|
@ -232,8 +233,8 @@ type BuildOptions struct {
|
|||
// implementations which do not communicate with a remote load balancer
|
||||
// server can ignore this field.
|
||||
Authority string
|
||||
// ChannelzParentID is the parent ClientConn's channelz ID.
|
||||
ChannelzParentID *channelz.Identifier
|
||||
// ChannelzParent is the parent ClientConn's channelz channel.
|
||||
ChannelzParent channelz.Identifier
|
||||
// CustomUserAgent is the custom user agent set on the parent ClientConn.
|
||||
// The balancer should set the same custom user agent if it creates a
|
||||
// ClientConn.
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ func (lb *lbBalancer) newRemoteBalancerCCWrapper() error {
|
|||
// Explicitly set pickfirst as the balancer.
|
||||
dopts = append(dopts, grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"pick_first"}`))
|
||||
dopts = append(dopts, grpc.WithResolvers(lb.manualResolver))
|
||||
dopts = append(dopts, grpc.WithChannelzParentID(lb.opt.ChannelzParentID))
|
||||
dopts = append(dopts, grpc.WithChannelzParentID(lb.opt.ChannelzParent))
|
||||
|
||||
// Enable Keepalive for grpclb client.
|
||||
dopts = append(dopts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ package grpc
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
|
|
@ -66,19 +65,20 @@ type ccBalancerWrapper struct {
|
|||
}
|
||||
|
||||
// newCCBalancerWrapper creates a new balancer wrapper in idle state. The
|
||||
// underlying balancer is not created until the switchTo() method is invoked.
|
||||
// underlying balancer is not created until the updateClientConnState() method
|
||||
// is invoked.
|
||||
func newCCBalancerWrapper(cc *ClientConn) *ccBalancerWrapper {
|
||||
ctx, cancel := context.WithCancel(cc.ctx)
|
||||
ccb := &ccBalancerWrapper{
|
||||
cc: cc,
|
||||
opts: balancer.BuildOptions{
|
||||
DialCreds: cc.dopts.copts.TransportCredentials,
|
||||
CredsBundle: cc.dopts.copts.CredsBundle,
|
||||
Dialer: cc.dopts.copts.Dialer,
|
||||
Authority: cc.authority,
|
||||
CustomUserAgent: cc.dopts.copts.UserAgent,
|
||||
ChannelzParentID: cc.channelzID,
|
||||
Target: cc.parsedTarget,
|
||||
DialCreds: cc.dopts.copts.TransportCredentials,
|
||||
CredsBundle: cc.dopts.copts.CredsBundle,
|
||||
Dialer: cc.dopts.copts.Dialer,
|
||||
Authority: cc.authority,
|
||||
CustomUserAgent: cc.dopts.copts.UserAgent,
|
||||
ChannelzParent: cc.channelz,
|
||||
Target: cc.parsedTarget,
|
||||
},
|
||||
serializer: grpcsync.NewCallbackSerializer(ctx),
|
||||
serializerCancel: cancel,
|
||||
|
|
@ -97,6 +97,11 @@ func (ccb *ccBalancerWrapper) updateClientConnState(ccs *balancer.ClientConnStat
|
|||
if ctx.Err() != nil || ccb.balancer == nil {
|
||||
return
|
||||
}
|
||||
name := gracefulswitch.ChildName(ccs.BalancerConfig)
|
||||
if ccb.curBalancerName != name {
|
||||
ccb.curBalancerName = name
|
||||
channelz.Infof(logger, ccb.cc.channelz, "Channel switches to new LB policy %q", name)
|
||||
}
|
||||
err := ccb.balancer.UpdateClientConnState(*ccs)
|
||||
if logger.V(2) && err != nil {
|
||||
logger.Infof("error from balancer.UpdateClientConnState: %v", err)
|
||||
|
|
@ -120,54 +125,6 @@ func (ccb *ccBalancerWrapper) resolverError(err error) {
|
|||
})
|
||||
}
|
||||
|
||||
// switchTo is invoked by grpc to instruct the balancer wrapper to switch to the
|
||||
// LB policy identified by name.
|
||||
//
|
||||
// ClientConn calls newCCBalancerWrapper() at creation time. Upon receipt of the
|
||||
// first good update from the name resolver, it determines the LB policy to use
|
||||
// and invokes the switchTo() method. Upon receipt of every subsequent update
|
||||
// from the name resolver, it invokes this method.
|
||||
//
|
||||
// the ccBalancerWrapper keeps track of the current LB policy name, and skips
|
||||
// the graceful balancer switching process if the name does not change.
|
||||
func (ccb *ccBalancerWrapper) switchTo(name string) {
|
||||
ccb.serializer.Schedule(func(ctx context.Context) {
|
||||
if ctx.Err() != nil || ccb.balancer == nil {
|
||||
return
|
||||
}
|
||||
// TODO: Other languages use case-sensitive balancer registries. We should
|
||||
// switch as well. See: https://github.com/grpc/grpc-go/issues/5288.
|
||||
if strings.EqualFold(ccb.curBalancerName, name) {
|
||||
return
|
||||
}
|
||||
ccb.buildLoadBalancingPolicy(name)
|
||||
})
|
||||
}
|
||||
|
||||
// buildLoadBalancingPolicy performs the following:
|
||||
// - retrieve a balancer builder for the given name. Use the default LB
|
||||
// policy, pick_first, if no LB policy with name is found in the registry.
|
||||
// - instruct the gracefulswitch balancer to switch to the above builder. This
|
||||
// will actually build the new balancer.
|
||||
// - update the `curBalancerName` field
|
||||
//
|
||||
// Must be called from a serializer callback.
|
||||
func (ccb *ccBalancerWrapper) buildLoadBalancingPolicy(name string) {
|
||||
builder := balancer.Get(name)
|
||||
if builder == nil {
|
||||
channelz.Warningf(logger, ccb.cc.channelzID, "Channel switches to new LB policy %q, since the specified LB policy %q was not registered", PickFirstBalancerName, name)
|
||||
builder = newPickfirstBuilder()
|
||||
} else {
|
||||
channelz.Infof(logger, ccb.cc.channelzID, "Channel switches to new LB policy %q", name)
|
||||
}
|
||||
|
||||
if err := ccb.balancer.SwitchTo(builder); err != nil {
|
||||
channelz.Errorf(logger, ccb.cc.channelzID, "Channel failed to build new LB policy %q: %v", name, err)
|
||||
return
|
||||
}
|
||||
ccb.curBalancerName = builder.Name()
|
||||
}
|
||||
|
||||
// close initiates async shutdown of the wrapper. cc.mu must be held when
|
||||
// calling this function. To determine the wrapper has finished shutting down,
|
||||
// the channel should block on ccb.serializer.Done() without cc.mu held.
|
||||
|
|
@ -175,7 +132,7 @@ func (ccb *ccBalancerWrapper) close() {
|
|||
ccb.mu.Lock()
|
||||
ccb.closed = true
|
||||
ccb.mu.Unlock()
|
||||
channelz.Info(logger, ccb.cc.channelzID, "ccBalancerWrapper: closing")
|
||||
channelz.Info(logger, ccb.cc.channelz, "ccBalancerWrapper: closing")
|
||||
ccb.serializer.Schedule(func(context.Context) {
|
||||
if ccb.balancer == nil {
|
||||
return
|
||||
|
|
@ -212,7 +169,7 @@ func (ccb *ccBalancerWrapper) NewSubConn(addrs []resolver.Address, opts balancer
|
|||
}
|
||||
ac, err := ccb.cc.newAddrConnLocked(addrs, opts)
|
||||
if err != nil {
|
||||
channelz.Warningf(logger, ccb.cc.channelzID, "acBalancerWrapper: NewSubConn: failed to newAddrConn: %v", err)
|
||||
channelz.Warningf(logger, ccb.cc.channelz, "acBalancerWrapper: NewSubConn: failed to newAddrConn: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
acbw := &acBalancerWrapper{
|
||||
|
|
@ -304,7 +261,7 @@ func (acbw *acBalancerWrapper) updateState(s connectivity.State, err error) {
|
|||
}
|
||||
|
||||
func (acbw *acBalancerWrapper) String() string {
|
||||
return fmt.Sprintf("SubConn(id:%d)", acbw.ac.channelzID.Int())
|
||||
return fmt.Sprintf("SubConn(id:%d)", acbw.ac.channelz.ID)
|
||||
}
|
||||
|
||||
func (acbw *acBalancerWrapper) UpdateAddresses(addrs []resolver.Address) {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ var (
|
|||
errConnDrain = errors.New("grpc: the connection is drained")
|
||||
// errConnClosing indicates that the connection is closing.
|
||||
errConnClosing = errors.New("grpc: the connection is closing")
|
||||
// errConnIdling indicates the the connection is being closed as the channel
|
||||
// errConnIdling indicates the connection is being closed as the channel
|
||||
// is moving to an idle mode due to inactivity.
|
||||
errConnIdling = errors.New("grpc: the connection is closing due to channel idleness")
|
||||
// invalidDefaultServiceConfigErrPrefix is used to prefix the json parsing error for the default
|
||||
|
|
@ -101,11 +101,6 @@ const (
|
|||
defaultReadBufSize = 32 * 1024
|
||||
)
|
||||
|
||||
// Dial creates a client connection to the given target.
|
||||
func Dial(target string, opts ...DialOption) (*ClientConn, error) {
|
||||
return DialContext(context.Background(), target, opts...)
|
||||
}
|
||||
|
||||
type defaultConfigSelector struct {
|
||||
sc *ServiceConfig
|
||||
}
|
||||
|
|
@ -117,13 +112,22 @@ func (dcs *defaultConfigSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*ires
|
|||
}, nil
|
||||
}
|
||||
|
||||
// newClient returns a new client in idle mode.
|
||||
func newClient(target string, opts ...DialOption) (conn *ClientConn, err error) {
|
||||
// NewClient creates a new gRPC "channel" for the target URI provided. No I/O
|
||||
// is performed. Use of the ClientConn for RPCs will automatically cause it to
|
||||
// connect. Connect may be used to manually create a connection, but for most
|
||||
// users this is unnecessary.
|
||||
//
|
||||
// The target name syntax is defined in
|
||||
// https://github.com/grpc/grpc/blob/master/doc/naming.md. e.g. to use dns
|
||||
// resolver, a "dns:///" prefix should be applied to the target.
|
||||
//
|
||||
// The DialOptions returned by WithBlock, WithTimeout, and
|
||||
// WithReturnConnectionError are ignored by this function.
|
||||
func NewClient(target string, opts ...DialOption) (conn *ClientConn, err error) {
|
||||
cc := &ClientConn{
|
||||
target: target,
|
||||
conns: make(map[*addrConn]struct{}),
|
||||
dopts: defaultDialOptions(),
|
||||
czData: new(channelzData),
|
||||
}
|
||||
|
||||
cc.retryThrottler.Store((*retryThrottler)(nil))
|
||||
|
|
@ -175,15 +179,15 @@ func newClient(target string, opts ...DialOption) (conn *ClientConn, err error)
|
|||
|
||||
// Determine the resolver to use.
|
||||
if err := cc.parseTargetAndFindResolver(); err != nil {
|
||||
channelz.RemoveEntry(cc.channelzID)
|
||||
channelz.RemoveEntry(cc.channelz.ID)
|
||||
return nil, err
|
||||
}
|
||||
if err = cc.determineAuthority(); err != nil {
|
||||
channelz.RemoveEntry(cc.channelzID)
|
||||
channelz.RemoveEntry(cc.channelz.ID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cc.csMgr = newConnectivityStateManager(cc.ctx, cc.channelzID)
|
||||
cc.csMgr = newConnectivityStateManager(cc.ctx, cc.channelz)
|
||||
cc.pickerWrapper = newPickerWrapper(cc.dopts.copts.StatsHandlers)
|
||||
|
||||
cc.initIdleStateLocked() // Safe to call without the lock, since nothing else has a reference to cc.
|
||||
|
|
@ -191,39 +195,32 @@ func newClient(target string, opts ...DialOption) (conn *ClientConn, err error)
|
|||
return cc, nil
|
||||
}
|
||||
|
||||
// DialContext creates a client connection to the given target. By default, it's
|
||||
// a non-blocking dial (the function won't wait for connections to be
|
||||
// established, and connecting happens in the background). To make it a blocking
|
||||
// dial, use WithBlock() dial option.
|
||||
// Dial calls DialContext(context.Background(), target, opts...).
|
||||
func Dial(target string, opts ...DialOption) (*ClientConn, error) {
|
||||
return DialContext(context.Background(), target, opts...)
|
||||
}
|
||||
|
||||
// DialContext calls NewClient and then exits idle mode. If WithBlock(true) is
|
||||
// used, it calls Connect and WaitForStateChange until either the context
|
||||
// expires or the state of the ClientConn is Ready.
|
||||
//
|
||||
// In the non-blocking case, the ctx does not act against the connection. It
|
||||
// only controls the setup steps.
|
||||
//
|
||||
// In the blocking case, ctx can be used to cancel or expire the pending
|
||||
// connection. Once this function returns, the cancellation and expiration of
|
||||
// ctx will be noop. Users should call ClientConn.Close to terminate all the
|
||||
// pending operations after this function returns.
|
||||
//
|
||||
// The target name syntax is defined in
|
||||
// https://github.com/grpc/grpc/blob/master/doc/naming.md.
|
||||
// e.g. to use dns resolver, a "dns:///" prefix should be applied to the target.
|
||||
// One subtle difference between NewClient and Dial and DialContext is that the
|
||||
// former uses "dns" as the default name resolver, while the latter use
|
||||
// "passthrough" for backward compatibility. This distinction should not matter
|
||||
// to most users, but could matter to legacy users that specify a custom dialer
|
||||
// and expect it to receive the target string directly.
|
||||
func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
|
||||
cc, err := newClient(target, opts...)
|
||||
// At the end of this method, we kick the channel out of idle, rather than
|
||||
// waiting for the first rpc.
|
||||
opts = append([]DialOption{withDefaultScheme("passthrough")}, opts...)
|
||||
cc, err := NewClient(target, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We start the channel off in idle mode, but kick it out of idle now,
|
||||
// instead of waiting for the first RPC. Other gRPC implementations do wait
|
||||
// for the first RPC to kick the channel out of idle. But doing so would be
|
||||
// a major behavior change for our users who are used to seeing the channel
|
||||
// active after Dial.
|
||||
//
|
||||
// Taking this approach of kicking it out of idle at the end of this method
|
||||
// allows us to share the code between channel creation and exiting idle
|
||||
// mode. This will also make it easy for us to switch to starting the
|
||||
// channel off in idle, i.e. by making newClient exported.
|
||||
|
||||
// instead of waiting for the first RPC. This is the legacy behavior of
|
||||
// Dial.
|
||||
defer func() {
|
||||
if err != nil {
|
||||
cc.Close()
|
||||
|
|
@ -291,17 +288,17 @@ func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *
|
|||
// addTraceEvent is a helper method to add a trace event on the channel. If the
|
||||
// channel is a nested one, the same event is also added on the parent channel.
|
||||
func (cc *ClientConn) addTraceEvent(msg string) {
|
||||
ted := &channelz.TraceEventDesc{
|
||||
ted := &channelz.TraceEvent{
|
||||
Desc: fmt.Sprintf("Channel %s", msg),
|
||||
Severity: channelz.CtInfo,
|
||||
}
|
||||
if cc.dopts.channelzParentID != nil {
|
||||
ted.Parent = &channelz.TraceEventDesc{
|
||||
Desc: fmt.Sprintf("Nested channel(id:%d) %s", cc.channelzID.Int(), msg),
|
||||
if cc.dopts.channelzParent != nil {
|
||||
ted.Parent = &channelz.TraceEvent{
|
||||
Desc: fmt.Sprintf("Nested channel(id:%d) %s", cc.channelz.ID, msg),
|
||||
Severity: channelz.CtInfo,
|
||||
}
|
||||
}
|
||||
channelz.AddTraceEvent(logger, cc.channelzID, 0, ted)
|
||||
channelz.AddTraceEvent(logger, cc.channelz, 0, ted)
|
||||
}
|
||||
|
||||
type idler ClientConn
|
||||
|
|
@ -418,14 +415,15 @@ func (cc *ClientConn) validateTransportCredentials() error {
|
|||
}
|
||||
|
||||
// channelzRegistration registers the newly created ClientConn with channelz and
|
||||
// stores the returned identifier in `cc.channelzID` and `cc.csMgr.channelzID`.
|
||||
// A channelz trace event is emitted for ClientConn creation. If the newly
|
||||
// created ClientConn is a nested one, i.e a valid parent ClientConn ID is
|
||||
// specified via a dial option, the trace event is also added to the parent.
|
||||
// stores the returned identifier in `cc.channelz`. A channelz trace event is
|
||||
// emitted for ClientConn creation. If the newly created ClientConn is a nested
|
||||
// one, i.e a valid parent ClientConn ID is specified via a dial option, the
|
||||
// trace event is also added to the parent.
|
||||
//
|
||||
// Doesn't grab cc.mu as this method is expected to be called only at Dial time.
|
||||
func (cc *ClientConn) channelzRegistration(target string) {
|
||||
cc.channelzID = channelz.RegisterChannel(&channelzChannel{cc}, cc.dopts.channelzParentID, target)
|
||||
parentChannel, _ := cc.dopts.channelzParent.(*channelz.Channel)
|
||||
cc.channelz = channelz.RegisterChannel(parentChannel, target)
|
||||
cc.addTraceEvent("created")
|
||||
}
|
||||
|
||||
|
|
@ -492,11 +490,11 @@ func getChainStreamer(interceptors []StreamClientInterceptor, curr int, finalStr
|
|||
}
|
||||
|
||||
// newConnectivityStateManager creates an connectivityStateManager with
|
||||
// the specified id.
|
||||
func newConnectivityStateManager(ctx context.Context, id *channelz.Identifier) *connectivityStateManager {
|
||||
// the specified channel.
|
||||
func newConnectivityStateManager(ctx context.Context, channel *channelz.Channel) *connectivityStateManager {
|
||||
return &connectivityStateManager{
|
||||
channelzID: id,
|
||||
pubSub: grpcsync.NewPubSub(ctx),
|
||||
channelz: channel,
|
||||
pubSub: grpcsync.NewPubSub(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -510,7 +508,7 @@ type connectivityStateManager struct {
|
|||
mu sync.Mutex
|
||||
state connectivity.State
|
||||
notifyChan chan struct{}
|
||||
channelzID *channelz.Identifier
|
||||
channelz *channelz.Channel
|
||||
pubSub *grpcsync.PubSub
|
||||
}
|
||||
|
||||
|
|
@ -527,9 +525,10 @@ func (csm *connectivityStateManager) updateState(state connectivity.State) {
|
|||
return
|
||||
}
|
||||
csm.state = state
|
||||
csm.channelz.ChannelMetrics.State.Store(&state)
|
||||
csm.pubSub.Publish(state)
|
||||
|
||||
channelz.Infof(logger, csm.channelzID, "Channel Connectivity change to %v", state)
|
||||
channelz.Infof(logger, csm.channelz, "Channel Connectivity change to %v", state)
|
||||
if csm.notifyChan != nil {
|
||||
// There are other goroutines waiting on this channel.
|
||||
close(csm.notifyChan)
|
||||
|
|
@ -583,12 +582,12 @@ type ClientConn struct {
|
|||
cancel context.CancelFunc // Cancelled on close.
|
||||
|
||||
// The following are initialized at dial time, and are read-only after that.
|
||||
target string // User's dial target.
|
||||
parsedTarget resolver.Target // See parseTargetAndFindResolver().
|
||||
authority string // See determineAuthority().
|
||||
dopts dialOptions // Default and user specified dial options.
|
||||
channelzID *channelz.Identifier // Channelz identifier for the channel.
|
||||
resolverBuilder resolver.Builder // See parseTargetAndFindResolver().
|
||||
target string // User's dial target.
|
||||
parsedTarget resolver.Target // See parseTargetAndFindResolver().
|
||||
authority string // See determineAuthority().
|
||||
dopts dialOptions // Default and user specified dial options.
|
||||
channelz *channelz.Channel // Channelz object.
|
||||
resolverBuilder resolver.Builder // See parseTargetAndFindResolver().
|
||||
idlenessMgr *idle.Manager
|
||||
|
||||
// The following provide their own synchronization, and therefore don't
|
||||
|
|
@ -596,7 +595,6 @@ type ClientConn struct {
|
|||
csMgr *connectivityStateManager
|
||||
pickerWrapper *pickerWrapper
|
||||
safeConfigSelector iresolver.SafeConfigSelector
|
||||
czData *channelzData
|
||||
retryThrottler atomic.Value // Updated from service config.
|
||||
|
||||
// mu protects the following fields.
|
||||
|
|
@ -690,6 +688,7 @@ func (cc *ClientConn) waitForResolvedAddrs(ctx context.Context) error {
|
|||
var emptyServiceConfig *ServiceConfig
|
||||
|
||||
func init() {
|
||||
balancer.Register(pickfirstBuilder{})
|
||||
cfg := parseServiceConfig("{}")
|
||||
if cfg.Err != nil {
|
||||
panic(fmt.Sprintf("impossible error parsing empty service config: %v", cfg.Err))
|
||||
|
|
@ -707,15 +706,15 @@ func init() {
|
|||
}
|
||||
}
|
||||
|
||||
func (cc *ClientConn) maybeApplyDefaultServiceConfig(addrs []resolver.Address) {
|
||||
func (cc *ClientConn) maybeApplyDefaultServiceConfig() {
|
||||
if cc.sc != nil {
|
||||
cc.applyServiceConfigAndBalancer(cc.sc, nil, addrs)
|
||||
cc.applyServiceConfigAndBalancer(cc.sc, nil)
|
||||
return
|
||||
}
|
||||
if cc.dopts.defaultServiceConfig != nil {
|
||||
cc.applyServiceConfigAndBalancer(cc.dopts.defaultServiceConfig, &defaultConfigSelector{cc.dopts.defaultServiceConfig}, addrs)
|
||||
cc.applyServiceConfigAndBalancer(cc.dopts.defaultServiceConfig, &defaultConfigSelector{cc.dopts.defaultServiceConfig})
|
||||
} else {
|
||||
cc.applyServiceConfigAndBalancer(emptyServiceConfig, &defaultConfigSelector{emptyServiceConfig}, addrs)
|
||||
cc.applyServiceConfigAndBalancer(emptyServiceConfig, &defaultConfigSelector{emptyServiceConfig})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -733,7 +732,7 @@ func (cc *ClientConn) updateResolverStateAndUnlock(s resolver.State, err error)
|
|||
// May need to apply the initial service config in case the resolver
|
||||
// doesn't support service configs, or doesn't provide a service config
|
||||
// with the new addresses.
|
||||
cc.maybeApplyDefaultServiceConfig(nil)
|
||||
cc.maybeApplyDefaultServiceConfig()
|
||||
|
||||
cc.balancerWrapper.resolverError(err)
|
||||
|
||||
|
|
@ -744,10 +743,10 @@ func (cc *ClientConn) updateResolverStateAndUnlock(s resolver.State, err error)
|
|||
|
||||
var ret error
|
||||
if cc.dopts.disableServiceConfig {
|
||||
channelz.Infof(logger, cc.channelzID, "ignoring service config from resolver (%v) and applying the default because service config is disabled", s.ServiceConfig)
|
||||
cc.maybeApplyDefaultServiceConfig(s.Addresses)
|
||||
channelz.Infof(logger, cc.channelz, "ignoring service config from resolver (%v) and applying the default because service config is disabled", s.ServiceConfig)
|
||||
cc.maybeApplyDefaultServiceConfig()
|
||||
} else if s.ServiceConfig == nil {
|
||||
cc.maybeApplyDefaultServiceConfig(s.Addresses)
|
||||
cc.maybeApplyDefaultServiceConfig()
|
||||
// TODO: do we need to apply a failing LB policy if there is no
|
||||
// default, per the error handling design?
|
||||
} else {
|
||||
|
|
@ -755,12 +754,12 @@ func (cc *ClientConn) updateResolverStateAndUnlock(s resolver.State, err error)
|
|||
configSelector := iresolver.GetConfigSelector(s)
|
||||
if configSelector != nil {
|
||||
if len(s.ServiceConfig.Config.(*ServiceConfig).Methods) != 0 {
|
||||
channelz.Infof(logger, cc.channelzID, "method configs in service config will be ignored due to presence of config selector")
|
||||
channelz.Infof(logger, cc.channelz, "method configs in service config will be ignored due to presence of config selector")
|
||||
}
|
||||
} else {
|
||||
configSelector = &defaultConfigSelector{sc}
|
||||
}
|
||||
cc.applyServiceConfigAndBalancer(sc, configSelector, s.Addresses)
|
||||
cc.applyServiceConfigAndBalancer(sc, configSelector)
|
||||
} else {
|
||||
ret = balancer.ErrBadResolverState
|
||||
if cc.sc == nil {
|
||||
|
|
@ -775,7 +774,7 @@ func (cc *ClientConn) updateResolverStateAndUnlock(s resolver.State, err error)
|
|||
|
||||
var balCfg serviceconfig.LoadBalancingConfig
|
||||
if cc.sc != nil && cc.sc.lbConfig != nil {
|
||||
balCfg = cc.sc.lbConfig.cfg
|
||||
balCfg = cc.sc.lbConfig
|
||||
}
|
||||
bw := cc.balancerWrapper
|
||||
cc.mu.Unlock()
|
||||
|
|
@ -834,22 +833,17 @@ func (cc *ClientConn) newAddrConnLocked(addrs []resolver.Address, opts balancer.
|
|||
addrs: copyAddressesWithoutBalancerAttributes(addrs),
|
||||
scopts: opts,
|
||||
dopts: cc.dopts,
|
||||
czData: new(channelzData),
|
||||
channelz: channelz.RegisterSubChannel(cc.channelz, ""),
|
||||
resetBackoff: make(chan struct{}),
|
||||
stateChan: make(chan struct{}),
|
||||
}
|
||||
ac.ctx, ac.cancel = context.WithCancel(cc.ctx)
|
||||
|
||||
var err error
|
||||
ac.channelzID, err = channelz.RegisterSubChannel(ac, cc.channelzID, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
channelz.AddTraceEvent(logger, ac.channelzID, 0, &channelz.TraceEventDesc{
|
||||
channelz.AddTraceEvent(logger, ac.channelz, 0, &channelz.TraceEvent{
|
||||
Desc: "Subchannel created",
|
||||
Severity: channelz.CtInfo,
|
||||
Parent: &channelz.TraceEventDesc{
|
||||
Desc: fmt.Sprintf("Subchannel(id:%d) created", ac.channelzID.Int()),
|
||||
Parent: &channelz.TraceEvent{
|
||||
Desc: fmt.Sprintf("Subchannel(id:%d) created", ac.channelz.ID),
|
||||
Severity: channelz.CtInfo,
|
||||
},
|
||||
})
|
||||
|
|
@ -872,38 +866,27 @@ func (cc *ClientConn) removeAddrConn(ac *addrConn, err error) {
|
|||
ac.tearDown(err)
|
||||
}
|
||||
|
||||
func (cc *ClientConn) channelzMetric() *channelz.ChannelInternalMetric {
|
||||
return &channelz.ChannelInternalMetric{
|
||||
State: cc.GetState(),
|
||||
Target: cc.target,
|
||||
CallsStarted: atomic.LoadInt64(&cc.czData.callsStarted),
|
||||
CallsSucceeded: atomic.LoadInt64(&cc.czData.callsSucceeded),
|
||||
CallsFailed: atomic.LoadInt64(&cc.czData.callsFailed),
|
||||
LastCallStartedTimestamp: time.Unix(0, atomic.LoadInt64(&cc.czData.lastCallStartedTime)),
|
||||
}
|
||||
}
|
||||
|
||||
// Target returns the target string of the ClientConn.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
|
||||
// later release.
|
||||
func (cc *ClientConn) Target() string {
|
||||
return cc.target
|
||||
}
|
||||
|
||||
// CanonicalTarget returns the canonical target string of the ClientConn.
|
||||
func (cc *ClientConn) CanonicalTarget() string {
|
||||
return cc.parsedTarget.String()
|
||||
}
|
||||
|
||||
func (cc *ClientConn) incrCallsStarted() {
|
||||
atomic.AddInt64(&cc.czData.callsStarted, 1)
|
||||
atomic.StoreInt64(&cc.czData.lastCallStartedTime, time.Now().UnixNano())
|
||||
cc.channelz.ChannelMetrics.CallsStarted.Add(1)
|
||||
cc.channelz.ChannelMetrics.LastCallStartedTimestamp.Store(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (cc *ClientConn) incrCallsSucceeded() {
|
||||
atomic.AddInt64(&cc.czData.callsSucceeded, 1)
|
||||
cc.channelz.ChannelMetrics.CallsSucceeded.Add(1)
|
||||
}
|
||||
|
||||
func (cc *ClientConn) incrCallsFailed() {
|
||||
atomic.AddInt64(&cc.czData.callsFailed, 1)
|
||||
cc.channelz.ChannelMetrics.CallsFailed.Add(1)
|
||||
}
|
||||
|
||||
// connect starts creating a transport.
|
||||
|
|
@ -947,7 +930,7 @@ func equalAddresses(a, b []resolver.Address) bool {
|
|||
// connections or connection attempts.
|
||||
func (ac *addrConn) updateAddrs(addrs []resolver.Address) {
|
||||
ac.mu.Lock()
|
||||
channelz.Infof(logger, ac.channelzID, "addrConn: updateAddrs curAddr: %v, addrs: %v", pretty.ToJSON(ac.curAddr), pretty.ToJSON(addrs))
|
||||
channelz.Infof(logger, ac.channelz, "addrConn: updateAddrs curAddr: %v, addrs: %v", pretty.ToJSON(ac.curAddr), pretty.ToJSON(addrs))
|
||||
|
||||
addrs = copyAddressesWithoutBalancerAttributes(addrs)
|
||||
if equalAddresses(ac.addrs, addrs) {
|
||||
|
|
@ -1067,7 +1050,7 @@ func (cc *ClientConn) getTransport(ctx context.Context, failfast bool, method st
|
|||
})
|
||||
}
|
||||
|
||||
func (cc *ClientConn) applyServiceConfigAndBalancer(sc *ServiceConfig, configSelector iresolver.ConfigSelector, addrs []resolver.Address) {
|
||||
func (cc *ClientConn) applyServiceConfigAndBalancer(sc *ServiceConfig, configSelector iresolver.ConfigSelector) {
|
||||
if sc == nil {
|
||||
// should never reach here.
|
||||
return
|
||||
|
|
@ -1088,17 +1071,6 @@ func (cc *ClientConn) applyServiceConfigAndBalancer(sc *ServiceConfig, configSel
|
|||
} else {
|
||||
cc.retryThrottler.Store((*retryThrottler)(nil))
|
||||
}
|
||||
|
||||
var newBalancerName string
|
||||
if cc.sc == nil || (cc.sc.lbConfig == nil && cc.sc.LB == nil) {
|
||||
// No service config or no LB policy specified in config.
|
||||
newBalancerName = PickFirstBalancerName
|
||||
} else if cc.sc.lbConfig != nil {
|
||||
newBalancerName = cc.sc.lbConfig.name
|
||||
} else { // cc.sc.LB != nil
|
||||
newBalancerName = *cc.sc.LB
|
||||
}
|
||||
cc.balancerWrapper.switchTo(newBalancerName)
|
||||
}
|
||||
|
||||
func (cc *ClientConn) resolveNow(o resolver.ResolveNowOptions) {
|
||||
|
|
@ -1174,7 +1146,7 @@ func (cc *ClientConn) Close() error {
|
|||
// TraceEvent needs to be called before RemoveEntry, as TraceEvent may add
|
||||
// trace reference to the entity being deleted, and thus prevent it from being
|
||||
// deleted right away.
|
||||
channelz.RemoveEntry(cc.channelzID)
|
||||
channelz.RemoveEntry(cc.channelz.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1206,8 +1178,7 @@ type addrConn struct {
|
|||
backoffIdx int // Needs to be stateful for resetConnectBackoff.
|
||||
resetBackoff chan struct{}
|
||||
|
||||
channelzID *channelz.Identifier
|
||||
czData *channelzData
|
||||
channelz *channelz.SubChannel
|
||||
}
|
||||
|
||||
// Note: this requires a lock on ac.mu.
|
||||
|
|
@ -1219,10 +1190,11 @@ func (ac *addrConn) updateConnectivityState(s connectivity.State, lastErr error)
|
|||
close(ac.stateChan)
|
||||
ac.stateChan = make(chan struct{})
|
||||
ac.state = s
|
||||
ac.channelz.ChannelMetrics.State.Store(&s)
|
||||
if lastErr == nil {
|
||||
channelz.Infof(logger, ac.channelzID, "Subchannel Connectivity change to %v", s)
|
||||
channelz.Infof(logger, ac.channelz, "Subchannel Connectivity change to %v", s)
|
||||
} else {
|
||||
channelz.Infof(logger, ac.channelzID, "Subchannel Connectivity change to %v, last error: %s", s, lastErr)
|
||||
channelz.Infof(logger, ac.channelz, "Subchannel Connectivity change to %v, last error: %s", s, lastErr)
|
||||
}
|
||||
ac.acbw.updateState(s, lastErr)
|
||||
}
|
||||
|
|
@ -1335,7 +1307,7 @@ func (ac *addrConn) tryAllAddrs(ctx context.Context, addrs []resolver.Address, c
|
|||
}
|
||||
ac.mu.Unlock()
|
||||
|
||||
channelz.Infof(logger, ac.channelzID, "Subchannel picks a new address %q to connect", addr.Addr)
|
||||
channelz.Infof(logger, ac.channelz, "Subchannel picks a new address %q to connect", addr.Addr)
|
||||
|
||||
err := ac.createTransport(ctx, addr, copts, connectDeadline)
|
||||
if err == nil {
|
||||
|
|
@ -1388,7 +1360,7 @@ func (ac *addrConn) createTransport(ctx context.Context, addr resolver.Address,
|
|||
|
||||
connectCtx, cancel := context.WithDeadline(ctx, connectDeadline)
|
||||
defer cancel()
|
||||
copts.ChannelzParentID = ac.channelzID
|
||||
copts.ChannelzParent = ac.channelz
|
||||
|
||||
newTr, err := transport.NewClientTransport(connectCtx, ac.cc.ctx, addr, copts, onClose)
|
||||
if err != nil {
|
||||
|
|
@ -1397,7 +1369,7 @@ func (ac *addrConn) createTransport(ctx context.Context, addr resolver.Address,
|
|||
}
|
||||
// newTr is either nil, or closed.
|
||||
hcancel()
|
||||
channelz.Warningf(logger, ac.channelzID, "grpc: addrConn.createTransport failed to connect to %s. Err: %v", addr, err)
|
||||
channelz.Warningf(logger, ac.channelz, "grpc: addrConn.createTransport failed to connect to %s. Err: %v", addr, err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -1469,7 +1441,7 @@ func (ac *addrConn) startHealthCheck(ctx context.Context) {
|
|||
// The health package is not imported to set health check function.
|
||||
//
|
||||
// TODO: add a link to the health check doc in the error message.
|
||||
channelz.Error(logger, ac.channelzID, "Health check is requested but health check function is not set.")
|
||||
channelz.Error(logger, ac.channelz, "Health check is requested but health check function is not set.")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1499,9 +1471,9 @@ func (ac *addrConn) startHealthCheck(ctx context.Context) {
|
|||
err := ac.cc.dopts.healthCheckFunc(ctx, newStream, setConnectivityState, healthCheckConfig.ServiceName)
|
||||
if err != nil {
|
||||
if status.Code(err) == codes.Unimplemented {
|
||||
channelz.Error(logger, ac.channelzID, "Subchannel health check is unimplemented at server side, thus health check is disabled")
|
||||
channelz.Error(logger, ac.channelz, "Subchannel health check is unimplemented at server side, thus health check is disabled")
|
||||
} else {
|
||||
channelz.Errorf(logger, ac.channelzID, "Health checking failed: %v", err)
|
||||
channelz.Errorf(logger, ac.channelz, "Health checking failed: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
|
@ -1566,18 +1538,18 @@ func (ac *addrConn) tearDown(err error) {
|
|||
ac.cancel()
|
||||
ac.curAddr = resolver.Address{}
|
||||
|
||||
channelz.AddTraceEvent(logger, ac.channelzID, 0, &channelz.TraceEventDesc{
|
||||
channelz.AddTraceEvent(logger, ac.channelz, 0, &channelz.TraceEvent{
|
||||
Desc: "Subchannel deleted",
|
||||
Severity: channelz.CtInfo,
|
||||
Parent: &channelz.TraceEventDesc{
|
||||
Desc: fmt.Sprintf("Subchannel(id:%d) deleted", ac.channelzID.Int()),
|
||||
Parent: &channelz.TraceEvent{
|
||||
Desc: fmt.Sprintf("Subchannel(id:%d) deleted", ac.channelz.ID),
|
||||
Severity: channelz.CtInfo,
|
||||
},
|
||||
})
|
||||
// TraceEvent needs to be called before RemoveEntry, as TraceEvent may add
|
||||
// trace reference to the entity being deleted, and thus prevent it from
|
||||
// being deleted right away.
|
||||
channelz.RemoveEntry(ac.channelzID)
|
||||
channelz.RemoveEntry(ac.channelz.ID)
|
||||
ac.mu.Unlock()
|
||||
|
||||
// We have to release the lock before the call to GracefulClose/Close here
|
||||
|
|
@ -1604,39 +1576,6 @@ func (ac *addrConn) tearDown(err error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (ac *addrConn) getState() connectivity.State {
|
||||
ac.mu.Lock()
|
||||
defer ac.mu.Unlock()
|
||||
return ac.state
|
||||
}
|
||||
|
||||
func (ac *addrConn) ChannelzMetric() *channelz.ChannelInternalMetric {
|
||||
ac.mu.Lock()
|
||||
addr := ac.curAddr.Addr
|
||||
ac.mu.Unlock()
|
||||
return &channelz.ChannelInternalMetric{
|
||||
State: ac.getState(),
|
||||
Target: addr,
|
||||
CallsStarted: atomic.LoadInt64(&ac.czData.callsStarted),
|
||||
CallsSucceeded: atomic.LoadInt64(&ac.czData.callsSucceeded),
|
||||
CallsFailed: atomic.LoadInt64(&ac.czData.callsFailed),
|
||||
LastCallStartedTimestamp: time.Unix(0, atomic.LoadInt64(&ac.czData.lastCallStartedTime)),
|
||||
}
|
||||
}
|
||||
|
||||
func (ac *addrConn) incrCallsStarted() {
|
||||
atomic.AddInt64(&ac.czData.callsStarted, 1)
|
||||
atomic.StoreInt64(&ac.czData.lastCallStartedTime, time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (ac *addrConn) incrCallsSucceeded() {
|
||||
atomic.AddInt64(&ac.czData.callsSucceeded, 1)
|
||||
}
|
||||
|
||||
func (ac *addrConn) incrCallsFailed() {
|
||||
atomic.AddInt64(&ac.czData.callsFailed, 1)
|
||||
}
|
||||
|
||||
type retryThrottler struct {
|
||||
max float64
|
||||
thresh float64
|
||||
|
|
@ -1674,12 +1613,17 @@ func (rt *retryThrottler) successfulRPC() {
|
|||
}
|
||||
}
|
||||
|
||||
type channelzChannel struct {
|
||||
cc *ClientConn
|
||||
func (ac *addrConn) incrCallsStarted() {
|
||||
ac.channelz.ChannelMetrics.CallsStarted.Add(1)
|
||||
ac.channelz.ChannelMetrics.LastCallStartedTimestamp.Store(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (c *channelzChannel) ChannelzMetric() *channelz.ChannelInternalMetric {
|
||||
return c.cc.channelzMetric()
|
||||
func (ac *addrConn) incrCallsSucceeded() {
|
||||
ac.channelz.ChannelMetrics.CallsSucceeded.Add(1)
|
||||
}
|
||||
|
||||
func (ac *addrConn) incrCallsFailed() {
|
||||
ac.channelz.ChannelMetrics.CallsFailed.Add(1)
|
||||
}
|
||||
|
||||
// ErrClientConnTimeout indicates that the ClientConn cannot establish the
|
||||
|
|
@ -1721,14 +1665,14 @@ func (cc *ClientConn) connectionError() error {
|
|||
//
|
||||
// Doesn't grab cc.mu as this method is expected to be called only at Dial time.
|
||||
func (cc *ClientConn) parseTargetAndFindResolver() error {
|
||||
channelz.Infof(logger, cc.channelzID, "original dial target is: %q", cc.target)
|
||||
channelz.Infof(logger, cc.channelz, "original dial target is: %q", cc.target)
|
||||
|
||||
var rb resolver.Builder
|
||||
parsedTarget, err := parseTarget(cc.target)
|
||||
if err != nil {
|
||||
channelz.Infof(logger, cc.channelzID, "dial target %q parse failed: %v", cc.target, err)
|
||||
channelz.Infof(logger, cc.channelz, "dial target %q parse failed: %v", cc.target, err)
|
||||
} else {
|
||||
channelz.Infof(logger, cc.channelzID, "parsed dial target is: %#v", parsedTarget)
|
||||
channelz.Infof(logger, cc.channelz, "parsed dial target is: %#v", parsedTarget)
|
||||
rb = cc.getResolver(parsedTarget.URL.Scheme)
|
||||
if rb != nil {
|
||||
cc.parsedTarget = parsedTarget
|
||||
|
|
@ -1740,17 +1684,22 @@ func (cc *ClientConn) parseTargetAndFindResolver() error {
|
|||
// We are here because the user's dial target did not contain a scheme or
|
||||
// specified an unregistered scheme. We should fallback to the default
|
||||
// scheme, except when a custom dialer is specified in which case, we should
|
||||
// always use passthrough scheme.
|
||||
defScheme := resolver.GetDefaultScheme()
|
||||
channelz.Infof(logger, cc.channelzID, "fallback to scheme %q", defScheme)
|
||||
// always use passthrough scheme. For either case, we need to respect any overridden
|
||||
// global defaults set by the user.
|
||||
defScheme := cc.dopts.defaultScheme
|
||||
if internal.UserSetDefaultScheme {
|
||||
defScheme = resolver.GetDefaultScheme()
|
||||
}
|
||||
|
||||
channelz.Infof(logger, cc.channelz, "fallback to scheme %q", defScheme)
|
||||
canonicalTarget := defScheme + ":///" + cc.target
|
||||
|
||||
parsedTarget, err = parseTarget(canonicalTarget)
|
||||
if err != nil {
|
||||
channelz.Infof(logger, cc.channelzID, "dial target %q parse failed: %v", canonicalTarget, err)
|
||||
channelz.Infof(logger, cc.channelz, "dial target %q parse failed: %v", canonicalTarget, err)
|
||||
return err
|
||||
}
|
||||
channelz.Infof(logger, cc.channelzID, "parsed dial target is: %+v", parsedTarget)
|
||||
channelz.Infof(logger, cc.channelz, "parsed dial target is: %+v", parsedTarget)
|
||||
rb = cc.getResolver(parsedTarget.URL.Scheme)
|
||||
if rb == nil {
|
||||
return fmt.Errorf("could not get resolver for default scheme: %q", parsedTarget.URL.Scheme)
|
||||
|
|
@ -1873,6 +1822,6 @@ func (cc *ClientConn) determineAuthority() error {
|
|||
} else {
|
||||
cc.authority = encodeAuthority(endpoint)
|
||||
}
|
||||
channelz.Infof(logger, cc.channelzID, "Channel authority set to %q", cc.authority)
|
||||
channelz.Infof(logger, cc.channelz, "Channel authority set to %q", cc.authority)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
285
vendor/google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp/handshaker.pb.go
generated
vendored
285
vendor/google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp/handshaker.pb.go
generated
vendored
|
|
@ -331,6 +331,11 @@ type StartClientHandshakeReq struct {
|
|||
RpcVersions *RpcProtocolVersions `protobuf:"bytes,9,opt,name=rpc_versions,json=rpcVersions,proto3" json:"rpc_versions,omitempty"`
|
||||
// (Optional) Maximum frame size supported by the client.
|
||||
MaxFrameSize uint32 `protobuf:"varint,10,opt,name=max_frame_size,json=maxFrameSize,proto3" json:"max_frame_size,omitempty"`
|
||||
// (Optional) An access token created by the caller only intended for use in
|
||||
// ALTS connections. The access token that should be used to authenticate to
|
||||
// the peer. The access token MUST be strongly bound to the ALTS credentials
|
||||
// used to establish the connection that the token is sent over.
|
||||
AccessToken string `protobuf:"bytes,11,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
|
||||
}
|
||||
|
||||
func (x *StartClientHandshakeReq) Reset() {
|
||||
|
|
@ -435,6 +440,13 @@ func (x *StartClientHandshakeReq) GetMaxFrameSize() uint32 {
|
|||
return 0
|
||||
}
|
||||
|
||||
func (x *StartClientHandshakeReq) GetAccessToken() string {
|
||||
if x != nil {
|
||||
return x.AccessToken
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ServerHandshakeParameters struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
|
@ -446,6 +458,11 @@ type ServerHandshakeParameters struct {
|
|||
// (Optional) A list of local identities supported by the server, if
|
||||
// specified. Otherwise, the handshaker chooses a default local identity.
|
||||
LocalIdentities []*Identity `protobuf:"bytes,2,rep,name=local_identities,json=localIdentities,proto3" json:"local_identities,omitempty"`
|
||||
// (Optional) An access token created by the caller only intended for use in
|
||||
// ALTS connections. The access token that should be used to authenticate to
|
||||
// the peer. The access token MUST be strongly bound to the ALTS credentials
|
||||
// used to establish the connection that the token is sent over.
|
||||
AccessToken *string `protobuf:"bytes,3,opt,name=access_token,json=accessToken,proto3,oneof" json:"access_token,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ServerHandshakeParameters) Reset() {
|
||||
|
|
@ -494,6 +511,13 @@ func (x *ServerHandshakeParameters) GetLocalIdentities() []*Identity {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *ServerHandshakeParameters) GetAccessToken() string {
|
||||
if x != nil && x.AccessToken != nil {
|
||||
return *x.AccessToken
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type StartServerHandshakeReq struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
|
@ -1047,7 +1071,7 @@ var file_grpc_gcp_handshaker_proto_rawDesc = []byte{
|
|||
0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x10,
|
||||
0x0a, 0x0e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x6f, 0x6e, 0x65, 0x6f, 0x66,
|
||||
0x22, 0xd3, 0x04, 0x0a, 0x17, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
|
||||
0x22, 0xf6, 0x04, 0x0a, 0x17, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
|
||||
0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x12, 0x5b, 0x0a, 0x1b,
|
||||
0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x5f, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69,
|
||||
0x74, 0x79, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
|
|
@ -1084,135 +1108,141 @@ var file_grpc_gcp_handshaker_proto_rawDesc = []byte{
|
|||
0x6f, 0x6e, 0x73, 0x52, 0x0b, 0x72, 0x70, 0x63, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73,
|
||||
0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x69,
|
||||
0x7a, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x61,
|
||||
0x6d, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x19, 0x53, 0x65, 0x72, 0x76, 0x65,
|
||||
0x72, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65,
|
||||
0x74, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x5f, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f,
|
||||
0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x12,
|
||||
0x3d, 0x0a, 0x10, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74,
|
||||
0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x67, 0x63, 0x70, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x0f, 0x6c,
|
||||
0x6f, 0x63, 0x61, 0x6c, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x22, 0xa5,
|
||||
0x04, 0x0a, 0x17, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x61,
|
||||
0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x12, 0x33, 0x0a, 0x15, 0x61, 0x70,
|
||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
|
||||
0x6f, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x61, 0x70, 0x70, 0x6c, 0x69,
|
||||
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x12,
|
||||
0x6d, 0x0a, 0x14, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x5f, 0x70, 0x61, 0x72,
|
||||
0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e,
|
||||
0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65,
|
||||
0x72, 0x76, 0x65, 0x72, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x65, 0x71,
|
||||
0x2e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65,
|
||||
0x74, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x13, 0x68, 0x61, 0x6e, 0x64, 0x73,
|
||||
0x68, 0x61, 0x6b, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x19,
|
||||
0x0a, 0x08, 0x69, 0x6e, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c,
|
||||
0x52, 0x07, 0x69, 0x6e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0e, 0x6c, 0x6f, 0x63,
|
||||
0x61, 0x6c, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x12, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x45, 0x6e, 0x64,
|
||||
0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x45, 0x6e, 0x64, 0x70,
|
||||
0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3b, 0x0a, 0x0f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x65,
|
||||
0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
|
||||
0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
|
||||
0x74, 0x52, 0x0e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
|
||||
0x74, 0x12, 0x40, 0x0a, 0x0c, 0x72, 0x70, 0x63, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67,
|
||||
0x63, 0x70, 0x2e, 0x52, 0x70, 0x63, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65,
|
||||
0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0b, 0x72, 0x70, 0x63, 0x56, 0x65, 0x72, 0x73, 0x69,
|
||||
0x6f, 0x6e, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65,
|
||||
0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6d, 0x61, 0x78,
|
||||
0x46, 0x72, 0x61, 0x6d, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x1a, 0x6b, 0x0a, 0x18, 0x48, 0x61, 0x6e,
|
||||
0x6d, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73,
|
||||
0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63,
|
||||
0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xbe, 0x01, 0x0a, 0x19, 0x53, 0x65,
|
||||
0x72, 0x76, 0x65, 0x72, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x50, 0x61, 0x72,
|
||||
0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x63, 0x6f, 0x72,
|
||||
0x64, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
|
||||
0x09, 0x52, 0x0f, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
|
||||
0x6c, 0x73, 0x12, 0x3d, 0x0a, 0x10, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67,
|
||||
0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79,
|
||||
0x52, 0x0f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65,
|
||||
0x73, 0x12, 0x26, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65,
|
||||
0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73,
|
||||
0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x61, 0x63,
|
||||
0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xa5, 0x04, 0x0a, 0x17, 0x53,
|
||||
0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68,
|
||||
0x61, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x12, 0x33, 0x0a, 0x15, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x18,
|
||||
0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x12, 0x6d, 0x0a, 0x14, 0x68,
|
||||
0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74,
|
||||
0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x67, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x67, 0x63, 0x70, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
|
||||
0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x2e, 0x48, 0x61, 0x6e,
|
||||
0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73,
|
||||
0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x05, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63,
|
||||
0x70, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b,
|
||||
0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x62, 0x0a, 0x17, 0x4e, 0x65, 0x78, 0x74, 0x48, 0x61,
|
||||
0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65,
|
||||
0x71, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x6e, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x0c, 0x52, 0x07, 0x69, 0x6e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x12,
|
||||
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x5f,
|
||||
0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
|
||||
0x6b, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x4d, 0x73, 0x22, 0xe5, 0x01, 0x0a, 0x0d, 0x48,
|
||||
0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x71, 0x12, 0x46, 0x0a, 0x0c,
|
||||
0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x21, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x53, 0x74,
|
||||
0x61, 0x72, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61,
|
||||
0x6b, 0x65, 0x52, 0x65, 0x71, 0x48, 0x00, 0x52, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53,
|
||||
0x74, 0x61, 0x72, 0x74, 0x12, 0x46, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73,
|
||||
0x74, 0x61, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x67, 0x72, 0x70,
|
||||
0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65,
|
||||
0x72, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x48, 0x00, 0x52,
|
||||
0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x37, 0x0a, 0x04,
|
||||
0x6e, 0x65, 0x78, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x67, 0x72, 0x70,
|
||||
0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x4e, 0x65, 0x78, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68,
|
||||
0x61, 0x6b, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x48, 0x00, 0x52,
|
||||
0x04, 0x6e, 0x65, 0x78, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x72, 0x65, 0x71, 0x5f, 0x6f, 0x6e, 0x65,
|
||||
0x6f, 0x66, 0x22, 0x9a, 0x03, 0x0a, 0x10, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65,
|
||||
0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x31, 0x0a, 0x14, 0x61, 0x70, 0x70, 0x6c, 0x69,
|
||||
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65,
|
||||
0x63, 0x6f, 0x72, 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x63, 0x6f, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18,
|
||||
0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x44, 0x61, 0x74, 0x61, 0x12, 0x37,
|
||||
0x0a, 0x0d, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70,
|
||||
0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x49,
|
||||
0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c,
|
||||
0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x12, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74,
|
||||
0x69, 0x74, 0x79, 0x52, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69,
|
||||
0x74, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e,
|
||||
0x65, 0x6c, 0x5f, 0x6f, 0x70, 0x65, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b,
|
||||
0x65, 0x65, 0x70, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x4f, 0x70, 0x65, 0x6e, 0x12, 0x49,
|
||||
0x0a, 0x11, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x72, 0x70, 0x63, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69,
|
||||
0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x67, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x67, 0x63, 0x70, 0x2e, 0x52, 0x70, 0x63, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
|
||||
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0f, 0x70, 0x65, 0x65, 0x72, 0x52, 0x70,
|
||||
0x63, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x78,
|
||||
0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28,
|
||||
0x0d, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22,
|
||||
0x40, 0x0a, 0x10, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72, 0x53, 0x74, 0x61,
|
||||
0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x0d, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69,
|
||||
0x6c, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c,
|
||||
0x73, 0x22, 0xbe, 0x01, 0x0a, 0x0e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72,
|
||||
0x52, 0x65, 0x73, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x75, 0x74, 0x5f, 0x66, 0x72, 0x61, 0x6d,
|
||||
0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x6f, 0x75, 0x74, 0x46, 0x72, 0x61,
|
||||
0x6d, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x63, 0x6f, 0x6e,
|
||||
0x73, 0x75, 0x6d, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x62, 0x79, 0x74,
|
||||
0x65, 0x73, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x06, 0x72, 0x65,
|
||||
0x73, 0x75, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x72, 0x70,
|
||||
0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x13, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65,
|
||||
0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x6e,
|
||||
0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x69, 0x6e,
|
||||
0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x65,
|
||||
0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
|
||||
0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
|
||||
0x74, 0x52, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
|
||||
0x12, 0x3b, 0x0a, 0x0f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f,
|
||||
0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x67, 0x63, 0x70, 0x2e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0e, 0x72,
|
||||
0x65, 0x6d, 0x6f, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x40, 0x0a,
|
||||
0x0c, 0x72, 0x70, 0x63, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x52,
|
||||
0x70, 0x63, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x73, 0x52, 0x0b, 0x72, 0x70, 0x63, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12,
|
||||
0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x5f, 0x73, 0x69, 0x7a,
|
||||
0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x61, 0x6d,
|
||||
0x65, 0x53, 0x69, 0x7a, 0x65, 0x1a, 0x6b, 0x0a, 0x18, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61,
|
||||
0x6b, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72,
|
||||
0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03,
|
||||
0x6b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x23, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x53, 0x65,
|
||||
0x72, 0x76, 0x65, 0x72, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x50, 0x61, 0x72,
|
||||
0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
|
||||
0x38, 0x01, 0x22, 0x62, 0x0a, 0x17, 0x4e, 0x65, 0x78, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68,
|
||||
0x61, 0x6b, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x12, 0x19, 0x0a,
|
||||
0x08, 0x69, 0x6e, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
||||
0x07, 0x69, 0x6e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6e, 0x65, 0x74, 0x77,
|
||||
0x6f, 0x72, 0x6b, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6d, 0x73, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4c, 0x61, 0x74,
|
||||
0x65, 0x6e, 0x63, 0x79, 0x4d, 0x73, 0x22, 0xe5, 0x01, 0x0a, 0x0d, 0x48, 0x61, 0x6e, 0x64, 0x73,
|
||||
0x68, 0x61, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x71, 0x12, 0x46, 0x0a, 0x0c, 0x63, 0x6c, 0x69, 0x65,
|
||||
0x6e, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21,
|
||||
0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43,
|
||||
0x6c, 0x69, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x65,
|
||||
0x71, 0x48, 0x00, 0x52, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x72, 0x74,
|
||||
0x12, 0x46, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63,
|
||||
0x70, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x61, 0x6e,
|
||||
0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x65, 0x72,
|
||||
0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x37, 0x0a, 0x04, 0x6e, 0x65, 0x78, 0x74,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63,
|
||||
0x70, 0x2e, 0x4e, 0x65, 0x78, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x4d,
|
||||
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x65, 0x78,
|
||||
0x74, 0x42, 0x0b, 0x0a, 0x09, 0x72, 0x65, 0x71, 0x5f, 0x6f, 0x6e, 0x65, 0x6f, 0x66, 0x22, 0x9a,
|
||||
0x03, 0x0a, 0x10, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x73,
|
||||
0x75, 0x6c, 0x74, 0x12, 0x31, 0x0a, 0x14, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x13, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
||||
0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0e, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
|
||||
0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x0c, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x44, 0x61, 0x74, 0x61, 0x12, 0x37, 0x0a, 0x0d, 0x70, 0x65,
|
||||
0x65, 0x72, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x12, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x49, 0x64, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74,
|
||||
0x69, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x72,
|
||||
0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52,
|
||||
0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x2a,
|
||||
0x0a, 0x11, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x6f,
|
||||
0x70, 0x65, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x65, 0x70, 0x43,
|
||||
0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x4f, 0x70, 0x65, 0x6e, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x65,
|
||||
0x65, 0x72, 0x5f, 0x72, 0x70, 0x63, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18,
|
||||
0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70,
|
||||
0x2e, 0x52, 0x70, 0x63, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0f, 0x70, 0x65, 0x65, 0x72, 0x52, 0x70, 0x63, 0x56, 0x65, 0x72,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x66, 0x72, 0x61,
|
||||
0x6d, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6d,
|
||||
0x61, 0x78, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x40, 0x0a, 0x10, 0x48,
|
||||
0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63,
|
||||
0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0xbe, 0x01,
|
||||
0x0a, 0x0e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
|
||||
0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x75, 0x74, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x6f, 0x75, 0x74, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x12,
|
||||
0x25, 0x0a, 0x0e, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65,
|
||||
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x62, 0x79, 0x74, 0x65, 0x73, 0x43, 0x6f,
|
||||
0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63,
|
||||
0x70, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x73, 0x75,
|
||||
0x6c, 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x32, 0x0a, 0x06, 0x73, 0x74,
|
||||
0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x72, 0x70,
|
||||
0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72,
|
||||
0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x32,
|
||||
0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
|
||||
0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2a, 0x4a,
|
||||
0x0a, 0x11, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x63, 0x6f, 0x6c, 0x12, 0x22, 0x0a, 0x1e, 0x48, 0x41, 0x4e, 0x44, 0x53, 0x48, 0x41, 0x4b, 0x45,
|
||||
0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43,
|
||||
0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x4c, 0x53, 0x10, 0x01,
|
||||
0x12, 0x08, 0x0a, 0x04, 0x41, 0x4c, 0x54, 0x53, 0x10, 0x02, 0x2a, 0x45, 0x0a, 0x0f, 0x4e, 0x65,
|
||||
0x74, 0x77, 0x6f, 0x72, 0x6b, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x20, 0x0a,
|
||||
0x1c, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f,
|
||||
0x4c, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12,
|
||||
0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10,
|
||||
0x02, 0x32, 0x5b, 0x0a, 0x11, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72, 0x53,
|
||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x46, 0x0a, 0x0b, 0x44, 0x6f, 0x48, 0x61, 0x6e, 0x64,
|
||||
0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x17, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70,
|
||||
0x2e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x18,
|
||||
0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68,
|
||||
0x61, 0x6b, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74,
|
||||
0x75, 0x73, 0x2a, 0x4a, 0x0a, 0x11, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x50,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x22, 0x0a, 0x1e, 0x48, 0x41, 0x4e, 0x44, 0x53,
|
||||
0x48, 0x41, 0x4b, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x55, 0x4e,
|
||||
0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54,
|
||||
0x4c, 0x53, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x4c, 0x54, 0x53, 0x10, 0x02, 0x2a, 0x45,
|
||||
0x0a, 0x0f, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
|
||||
0x6c, 0x12, 0x20, 0x0a, 0x1c, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x50, 0x52, 0x4f,
|
||||
0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45,
|
||||
0x44, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03,
|
||||
0x55, 0x44, 0x50, 0x10, 0x02, 0x32, 0x5b, 0x0a, 0x11, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61,
|
||||
0x6b, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x46, 0x0a, 0x0b, 0x44, 0x6f,
|
||||
0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x17, 0x2e, 0x67, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x67, 0x63, 0x70, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72, 0x52,
|
||||
0x65, 0x71, 0x1a, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x63, 0x70, 0x2e, 0x48, 0x61,
|
||||
0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x28, 0x01,
|
||||
0x30, 0x01, 0x42, 0x6b, 0x0a, 0x15, 0x69, 0x6f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x6c,
|
||||
0x74, 0x73, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x42, 0x0f, 0x48, 0x61, 0x6e,
|
||||
0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3f,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72,
|
||||
0x67, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61,
|
||||
0x6c, 0x73, 0x2f, 0x61, 0x6c, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
|
||||
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x5f, 0x67, 0x63, 0x70, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x61, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x6b,
|
||||
0x0a, 0x15, 0x69, 0x6f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x6c, 0x74, 0x73, 0x2e, 0x69,
|
||||
0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x42, 0x0f, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61,
|
||||
0x6b, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3f, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x67, 0x72,
|
||||
0x70, 0x63, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2f, 0x61,
|
||||
0x6c, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x5f, 0x67, 0x63, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -1410,6 +1440,7 @@ func file_grpc_gcp_handshaker_proto_init() {
|
|||
(*Identity_ServiceAccount)(nil),
|
||||
(*Identity_Hostname)(nil),
|
||||
}
|
||||
file_grpc_gcp_handshaker_proto_msgTypes[3].OneofWrappers = []interface{}{}
|
||||
file_grpc_gcp_handshaker_proto_msgTypes[6].OneofWrappers = []interface{}{
|
||||
(*HandshakerReq_ClientStart)(nil),
|
||||
(*HandshakerReq_ServerStart)(nil),
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"google.golang.org/grpc/attributes"
|
||||
icredentials "google.golang.org/grpc/internal/credentials"
|
||||
"google.golang.org/protobuf/protoadapt"
|
||||
)
|
||||
|
||||
// PerRPCCredentials defines the common interface for the credentials which need to
|
||||
|
|
@ -287,5 +287,5 @@ type ChannelzSecurityValue interface {
|
|||
type OtherChannelzSecurityValue struct {
|
||||
ChannelzSecurityValue
|
||||
Name string
|
||||
Value proto.Message
|
||||
Value protoadapt.MessageV1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/internal/xds"
|
||||
)
|
||||
|
||||
const cfeClusterNamePrefix = "google_cfe_"
|
||||
|
|
@ -63,7 +63,7 @@ func clusterName(ctx context.Context) string {
|
|||
if chi.Attributes == nil {
|
||||
return ""
|
||||
}
|
||||
cluster, _ := internal.GetXDSHandshakeClusterName(chi.Attributes)
|
||||
cluster, _ := xds.GetXDSHandshakeClusterName(chi.Attributes)
|
||||
return cluster
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ type dialOptions struct {
|
|||
binaryLogger binarylog.Logger
|
||||
copts transport.ConnectOptions
|
||||
callOptions []CallOption
|
||||
channelzParentID *channelz.Identifier
|
||||
channelzParent channelz.Identifier
|
||||
disableServiceConfig bool
|
||||
disableRetry bool
|
||||
disableHealthCheck bool
|
||||
|
|
@ -79,6 +79,7 @@ type dialOptions struct {
|
|||
resolvers []resolver.Builder
|
||||
idleTimeout time.Duration
|
||||
recvBufferPool SharedBufferPool
|
||||
defaultScheme string
|
||||
}
|
||||
|
||||
// DialOption configures how we set up the connection.
|
||||
|
|
@ -154,9 +155,7 @@ func WithSharedWriteBuffer(val bool) DialOption {
|
|||
}
|
||||
|
||||
// WithWriteBufferSize determines how much data can be batched before doing a
|
||||
// write on the wire. The corresponding memory allocation for this buffer will
|
||||
// be twice the size to keep syscalls low. The default value for this buffer is
|
||||
// 32KB.
|
||||
// write on the wire. The default value for this buffer is 32KB.
|
||||
//
|
||||
// Zero or negative values will disable the write buffer such that each write
|
||||
// will be on underlying connection. Note: A Send call may not directly
|
||||
|
|
@ -555,9 +554,9 @@ func WithAuthority(a string) DialOption {
|
|||
//
|
||||
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
|
||||
// later release.
|
||||
func WithChannelzParentID(id *channelz.Identifier) DialOption {
|
||||
func WithChannelzParentID(c channelz.Identifier) DialOption {
|
||||
return newFuncDialOption(func(o *dialOptions) {
|
||||
o.channelzParentID = id
|
||||
o.channelzParent = c
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -645,6 +644,7 @@ func defaultDialOptions() dialOptions {
|
|||
healthCheckFunc: internal.HealthCheckFunc,
|
||||
idleTimeout: 30 * time.Minute,
|
||||
recvBufferPool: nopBufferPool{},
|
||||
defaultScheme: "dns",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -659,6 +659,14 @@ func withMinConnectDeadline(f func() time.Duration) DialOption {
|
|||
})
|
||||
}
|
||||
|
||||
// withDefaultScheme is used to allow Dial to use "passthrough" as the default
|
||||
// name resolver, while NewClient uses "dns" otherwise.
|
||||
func withDefaultScheme(s string) DialOption {
|
||||
return newFuncDialOption(func(o *dialOptions) {
|
||||
o.defaultScheme = s
|
||||
})
|
||||
}
|
||||
|
||||
// WithResolvers allows a list of resolver implementations to be registered
|
||||
// locally with the ClientConn without needing to be globally registered via
|
||||
// resolver.Register. They will be matched against the scheme used for the
|
||||
|
|
|
|||
83
vendor/google.golang.org/grpc/internal/balancer/gracefulswitch/config.go
generated
vendored
Normal file
83
vendor/google.golang.org/grpc/internal/balancer/gracefulswitch/config.go
generated
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
*
|
||||
* Copyright 2024 gRPC authors.
|
||||
*
|
||||
* 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 gracefulswitch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
)
|
||||
|
||||
type lbConfig struct {
|
||||
serviceconfig.LoadBalancingConfig
|
||||
|
||||
childBuilder balancer.Builder
|
||||
childConfig serviceconfig.LoadBalancingConfig
|
||||
}
|
||||
|
||||
func ChildName(l serviceconfig.LoadBalancingConfig) string {
|
||||
return l.(*lbConfig).childBuilder.Name()
|
||||
}
|
||||
|
||||
// ParseConfig parses a child config list and returns a LB config for the
|
||||
// gracefulswitch Balancer.
|
||||
//
|
||||
// cfg is expected to be a json.RawMessage containing a JSON array of LB policy
|
||||
// names + configs as the format of the "loadBalancingConfig" field in
|
||||
// ServiceConfig. It returns a type that should be passed to
|
||||
// UpdateClientConnState in the BalancerConfig field.
|
||||
func ParseConfig(cfg json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||||
var lbCfg []map[string]json.RawMessage
|
||||
if err := json.Unmarshal(cfg, &lbCfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, e := range lbCfg {
|
||||
if len(e) != 1 {
|
||||
return nil, fmt.Errorf("expected a JSON struct with one entry; received entry %v at index %d", e, i)
|
||||
}
|
||||
|
||||
var name string
|
||||
var jsonCfg json.RawMessage
|
||||
for name, jsonCfg = range e {
|
||||
}
|
||||
|
||||
builder := balancer.Get(name)
|
||||
if builder == nil {
|
||||
// Skip unregistered balancer names.
|
||||
continue
|
||||
}
|
||||
|
||||
parser, ok := builder.(balancer.ConfigParser)
|
||||
if !ok {
|
||||
// This is a valid child with no config.
|
||||
return &lbConfig{childBuilder: builder}, nil
|
||||
}
|
||||
|
||||
cfg, err := parser.ParseConfig(jsonCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing config for policy %q: %v", name, err)
|
||||
}
|
||||
|
||||
return &lbConfig{childBuilder: builder, childConfig: cfg}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no supported policies found in config: %v", string(cfg))
|
||||
}
|
||||
45
vendor/google.golang.org/grpc/internal/balancer/gracefulswitch/gracefulswitch.go
generated
vendored
45
vendor/google.golang.org/grpc/internal/balancer/gracefulswitch/gracefulswitch.go
generated
vendored
|
|
@ -94,14 +94,23 @@ func (gsb *Balancer) balancerCurrentOrPending(bw *balancerWrapper) bool {
|
|||
// process is not complete when this method returns. This method must be called
|
||||
// synchronously alongside the rest of the balancer.Balancer methods this
|
||||
// Graceful Switch Balancer implements.
|
||||
//
|
||||
// Deprecated: use ParseConfig and pass a parsed config to UpdateClientConnState
|
||||
// to cause the Balancer to automatically change to the new child when necessary.
|
||||
func (gsb *Balancer) SwitchTo(builder balancer.Builder) error {
|
||||
_, err := gsb.switchTo(builder)
|
||||
return err
|
||||
}
|
||||
|
||||
func (gsb *Balancer) switchTo(builder balancer.Builder) (*balancerWrapper, error) {
|
||||
gsb.mu.Lock()
|
||||
if gsb.closed {
|
||||
gsb.mu.Unlock()
|
||||
return errBalancerClosed
|
||||
return nil, errBalancerClosed
|
||||
}
|
||||
bw := &balancerWrapper{
|
||||
gsb: gsb,
|
||||
builder: builder,
|
||||
gsb: gsb,
|
||||
lastState: balancer.State{
|
||||
ConnectivityState: connectivity.Connecting,
|
||||
Picker: base.NewErrPicker(balancer.ErrNoSubConnAvailable),
|
||||
|
|
@ -129,7 +138,7 @@ func (gsb *Balancer) SwitchTo(builder balancer.Builder) error {
|
|||
gsb.balancerCurrent = nil
|
||||
}
|
||||
gsb.mu.Unlock()
|
||||
return balancer.ErrBadResolverState
|
||||
return nil, balancer.ErrBadResolverState
|
||||
}
|
||||
|
||||
// This write doesn't need to take gsb.mu because this field never gets read
|
||||
|
|
@ -138,7 +147,7 @@ func (gsb *Balancer) SwitchTo(builder balancer.Builder) error {
|
|||
// bw.Balancer field will never be forwarded to until this SwitchTo()
|
||||
// function returns.
|
||||
bw.Balancer = newBalancer
|
||||
return nil
|
||||
return bw, nil
|
||||
}
|
||||
|
||||
// Returns nil if the graceful switch balancer is closed.
|
||||
|
|
@ -152,12 +161,33 @@ func (gsb *Balancer) latestBalancer() *balancerWrapper {
|
|||
}
|
||||
|
||||
// UpdateClientConnState forwards the update to the latest balancer created.
|
||||
//
|
||||
// If the state's BalancerConfig is the config returned by a call to
|
||||
// gracefulswitch.ParseConfig, then this function will automatically SwitchTo
|
||||
// the balancer indicated by the config before forwarding its config to it, if
|
||||
// necessary.
|
||||
func (gsb *Balancer) UpdateClientConnState(state balancer.ClientConnState) error {
|
||||
// The resolver data is only relevant to the most recent LB Policy.
|
||||
balToUpdate := gsb.latestBalancer()
|
||||
|
||||
gsbCfg, ok := state.BalancerConfig.(*lbConfig)
|
||||
if ok {
|
||||
// Switch to the child in the config unless it is already active.
|
||||
if balToUpdate == nil || gsbCfg.childBuilder.Name() != balToUpdate.builder.Name() {
|
||||
var err error
|
||||
balToUpdate, err = gsb.switchTo(gsbCfg.childBuilder)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not switch to new child balancer: %w", err)
|
||||
}
|
||||
}
|
||||
// Unwrap the child balancer's config.
|
||||
state.BalancerConfig = gsbCfg.childConfig
|
||||
}
|
||||
|
||||
if balToUpdate == nil {
|
||||
return errBalancerClosed
|
||||
}
|
||||
|
||||
// Perform this call without gsb.mu to prevent deadlocks if the child calls
|
||||
// back into the channel. The latest balancer can never be closed during a
|
||||
// call from the channel, even without gsb.mu held.
|
||||
|
|
@ -169,6 +199,10 @@ func (gsb *Balancer) ResolverError(err error) {
|
|||
// The resolver data is only relevant to the most recent LB Policy.
|
||||
balToUpdate := gsb.latestBalancer()
|
||||
if balToUpdate == nil {
|
||||
gsb.cc.UpdateState(balancer.State{
|
||||
ConnectivityState: connectivity.TransientFailure,
|
||||
Picker: base.NewErrPicker(err),
|
||||
})
|
||||
return
|
||||
}
|
||||
// Perform this call without gsb.mu to prevent deadlocks if the child calls
|
||||
|
|
@ -261,7 +295,8 @@ func (gsb *Balancer) Close() {
|
|||
// graceful switch logic.
|
||||
type balancerWrapper struct {
|
||||
balancer.Balancer
|
||||
gsb *Balancer
|
||||
gsb *Balancer
|
||||
builder balancer.Builder
|
||||
|
||||
lastState balancer.State
|
||||
subconns map[balancer.SubConn]bool // subconns created by this balancer
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
*
|
||||
* Copyright 2024 gRPC authors.
|
||||
*
|
||||
* 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 channelz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"google.golang.org/grpc/connectivity"
|
||||
)
|
||||
|
||||
// Channel represents a channel within channelz, which includes metrics and
|
||||
// internal channelz data, such as channelz id, child list, etc.
|
||||
type Channel struct {
|
||||
Entity
|
||||
// ID is the channelz id of this channel.
|
||||
ID int64
|
||||
// RefName is the human readable reference string of this channel.
|
||||
RefName string
|
||||
|
||||
closeCalled bool
|
||||
nestedChans map[int64]string
|
||||
subChans map[int64]string
|
||||
Parent *Channel
|
||||
trace *ChannelTrace
|
||||
// traceRefCount is the number of trace events that reference this channel.
|
||||
// Non-zero traceRefCount means the trace of this channel cannot be deleted.
|
||||
traceRefCount int32
|
||||
|
||||
ChannelMetrics ChannelMetrics
|
||||
}
|
||||
|
||||
// Implemented to make Channel implement the Identifier interface used for
|
||||
// nesting.
|
||||
func (c *Channel) channelzIdentifier() {}
|
||||
|
||||
func (c *Channel) String() string {
|
||||
if c.Parent == nil {
|
||||
return fmt.Sprintf("Channel #%d", c.ID)
|
||||
}
|
||||
return fmt.Sprintf("%s Channel #%d", c.Parent, c.ID)
|
||||
}
|
||||
|
||||
func (c *Channel) id() int64 {
|
||||
return c.ID
|
||||
}
|
||||
|
||||
func (c *Channel) SubChans() map[int64]string {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return copyMap(c.subChans)
|
||||
}
|
||||
|
||||
func (c *Channel) NestedChans() map[int64]string {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return copyMap(c.nestedChans)
|
||||
}
|
||||
|
||||
func (c *Channel) Trace() *ChannelTrace {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return c.trace.copy()
|
||||
}
|
||||
|
||||
type ChannelMetrics struct {
|
||||
// The current connectivity state of the channel.
|
||||
State atomic.Pointer[connectivity.State]
|
||||
// The target this channel originally tried to connect to. May be absent
|
||||
Target atomic.Pointer[string]
|
||||
// The number of calls started on the channel.
|
||||
CallsStarted atomic.Int64
|
||||
// The number of calls that have completed with an OK status.
|
||||
CallsSucceeded atomic.Int64
|
||||
// The number of calls that have a completed with a non-OK status.
|
||||
CallsFailed atomic.Int64
|
||||
// The last time a call was started on the channel.
|
||||
LastCallStartedTimestamp atomic.Int64
|
||||
}
|
||||
|
||||
// CopyFrom copies the metrics in o to c. For testing only.
|
||||
func (c *ChannelMetrics) CopyFrom(o *ChannelMetrics) {
|
||||
c.State.Store(o.State.Load())
|
||||
c.Target.Store(o.Target.Load())
|
||||
c.CallsStarted.Store(o.CallsStarted.Load())
|
||||
c.CallsSucceeded.Store(o.CallsSucceeded.Load())
|
||||
c.CallsFailed.Store(o.CallsFailed.Load())
|
||||
c.LastCallStartedTimestamp.Store(o.LastCallStartedTimestamp.Load())
|
||||
}
|
||||
|
||||
// Equal returns true iff the metrics of c are the same as the metrics of o.
|
||||
// For testing only.
|
||||
func (c *ChannelMetrics) Equal(o any) bool {
|
||||
oc, ok := o.(*ChannelMetrics)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if (c.State.Load() == nil) != (oc.State.Load() == nil) {
|
||||
return false
|
||||
}
|
||||
if c.State.Load() != nil && *c.State.Load() != *oc.State.Load() {
|
||||
return false
|
||||
}
|
||||
if (c.Target.Load() == nil) != (oc.Target.Load() == nil) {
|
||||
return false
|
||||
}
|
||||
if c.Target.Load() != nil && *c.Target.Load() != *oc.Target.Load() {
|
||||
return false
|
||||
}
|
||||
return c.CallsStarted.Load() == oc.CallsStarted.Load() &&
|
||||
c.CallsFailed.Load() == oc.CallsFailed.Load() &&
|
||||
c.CallsSucceeded.Load() == oc.CallsSucceeded.Load() &&
|
||||
c.LastCallStartedTimestamp.Load() == oc.LastCallStartedTimestamp.Load()
|
||||
}
|
||||
|
||||
func strFromPointer(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func (c *ChannelMetrics) String() string {
|
||||
return fmt.Sprintf("State: %v, Target: %s, CallsStarted: %v, CallsSucceeded: %v, CallsFailed: %v, LastCallStartedTimestamp: %v",
|
||||
c.State.Load(), strFromPointer(c.Target.Load()), c.CallsStarted.Load(), c.CallsSucceeded.Load(), c.CallsFailed.Load(), c.LastCallStartedTimestamp.Load(),
|
||||
)
|
||||
}
|
||||
|
||||
func NewChannelMetricForTesting(state connectivity.State, target string, started, succeeded, failed, timestamp int64) *ChannelMetrics {
|
||||
c := &ChannelMetrics{}
|
||||
c.State.Store(&state)
|
||||
c.Target.Store(&target)
|
||||
c.CallsStarted.Store(started)
|
||||
c.CallsSucceeded.Store(succeeded)
|
||||
c.CallsFailed.Store(failed)
|
||||
c.LastCallStartedTimestamp.Store(timestamp)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Channel) addChild(id int64, e entry) {
|
||||
switch v := e.(type) {
|
||||
case *SubChannel:
|
||||
c.subChans[id] = v.RefName
|
||||
case *Channel:
|
||||
c.nestedChans[id] = v.RefName
|
||||
default:
|
||||
logger.Errorf("cannot add a child (id = %d) of type %T to a channel", id, e)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Channel) deleteChild(id int64) {
|
||||
delete(c.subChans, id)
|
||||
delete(c.nestedChans, id)
|
||||
c.deleteSelfIfReady()
|
||||
}
|
||||
|
||||
func (c *Channel) triggerDelete() {
|
||||
c.closeCalled = true
|
||||
c.deleteSelfIfReady()
|
||||
}
|
||||
|
||||
func (c *Channel) getParentID() int64 {
|
||||
if c.Parent == nil {
|
||||
return -1
|
||||
}
|
||||
return c.Parent.ID
|
||||
}
|
||||
|
||||
// deleteSelfFromTree tries to delete the channel from the channelz entry relation tree, which means
|
||||
// deleting the channel reference from its parent's child list.
|
||||
//
|
||||
// In order for a channel to be deleted from the tree, it must meet the criteria that, removal of the
|
||||
// corresponding grpc object has been invoked, and the channel does not have any children left.
|
||||
//
|
||||
// The returned boolean value indicates whether the channel has been successfully deleted from tree.
|
||||
func (c *Channel) deleteSelfFromTree() (deleted bool) {
|
||||
if !c.closeCalled || len(c.subChans)+len(c.nestedChans) != 0 {
|
||||
return false
|
||||
}
|
||||
// not top channel
|
||||
if c.Parent != nil {
|
||||
c.Parent.deleteChild(c.ID)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// deleteSelfFromMap checks whether it is valid to delete the channel from the map, which means
|
||||
// deleting the channel from channelz's tracking entirely. Users can no longer use id to query the
|
||||
// channel, and its memory will be garbage collected.
|
||||
//
|
||||
// The trace reference count of the channel must be 0 in order to be deleted from the map. This is
|
||||
// specified in the channel tracing gRFC that as long as some other trace has reference to an entity,
|
||||
// the trace of the referenced entity must not be deleted. In order to release the resource allocated
|
||||
// by grpc, the reference to the grpc object is reset to a dummy object.
|
||||
//
|
||||
// deleteSelfFromMap must be called after deleteSelfFromTree returns true.
|
||||
//
|
||||
// It returns a bool to indicate whether the channel can be safely deleted from map.
|
||||
func (c *Channel) deleteSelfFromMap() (delete bool) {
|
||||
return c.getTraceRefCount() == 0
|
||||
}
|
||||
|
||||
// deleteSelfIfReady tries to delete the channel itself from the channelz database.
|
||||
// The delete process includes two steps:
|
||||
// 1. delete the channel from the entry relation tree, i.e. delete the channel reference from its
|
||||
// parent's child list.
|
||||
// 2. delete the channel from the map, i.e. delete the channel entirely from channelz. Lookup by id
|
||||
// will return entry not found error.
|
||||
func (c *Channel) deleteSelfIfReady() {
|
||||
if !c.deleteSelfFromTree() {
|
||||
return
|
||||
}
|
||||
if !c.deleteSelfFromMap() {
|
||||
return
|
||||
}
|
||||
db.deleteEntry(c.ID)
|
||||
c.trace.clear()
|
||||
}
|
||||
|
||||
func (c *Channel) getChannelTrace() *ChannelTrace {
|
||||
return c.trace
|
||||
}
|
||||
|
||||
func (c *Channel) incrTraceRefCount() {
|
||||
atomic.AddInt32(&c.traceRefCount, 1)
|
||||
}
|
||||
|
||||
func (c *Channel) decrTraceRefCount() {
|
||||
atomic.AddInt32(&c.traceRefCount, -1)
|
||||
}
|
||||
|
||||
func (c *Channel) getTraceRefCount() int {
|
||||
i := atomic.LoadInt32(&c.traceRefCount)
|
||||
return int(i)
|
||||
}
|
||||
|
||||
func (c *Channel) getRefName() string {
|
||||
return c.RefName
|
||||
}
|
||||
|
|
@ -0,0 +1,402 @@
|
|||
/*
|
||||
*
|
||||
* Copyright 2018 gRPC authors.
|
||||
*
|
||||
* 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 channelz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// entry represents a node in the channelz database.
|
||||
type entry interface {
|
||||
// addChild adds a child e, whose channelz id is id to child list
|
||||
addChild(id int64, e entry)
|
||||
// deleteChild deletes a child with channelz id to be id from child list
|
||||
deleteChild(id int64)
|
||||
// triggerDelete tries to delete self from channelz database. However, if
|
||||
// child list is not empty, then deletion from the database is on hold until
|
||||
// the last child is deleted from database.
|
||||
triggerDelete()
|
||||
// deleteSelfIfReady check whether triggerDelete() has been called before,
|
||||
// and whether child list is now empty. If both conditions are met, then
|
||||
// delete self from database.
|
||||
deleteSelfIfReady()
|
||||
// getParentID returns parent ID of the entry. 0 value parent ID means no parent.
|
||||
getParentID() int64
|
||||
Entity
|
||||
}
|
||||
|
||||
// channelMap is the storage data structure for channelz.
|
||||
//
|
||||
// Methods of channelMap can be divided in two two categories with respect to
|
||||
// locking.
|
||||
//
|
||||
// 1. Methods acquire the global lock.
|
||||
// 2. Methods that can only be called when global lock is held.
|
||||
//
|
||||
// A second type of method need always to be called inside a first type of method.
|
||||
type channelMap struct {
|
||||
mu sync.RWMutex
|
||||
topLevelChannels map[int64]struct{}
|
||||
channels map[int64]*Channel
|
||||
subChannels map[int64]*SubChannel
|
||||
sockets map[int64]*Socket
|
||||
servers map[int64]*Server
|
||||
}
|
||||
|
||||
func newChannelMap() *channelMap {
|
||||
return &channelMap{
|
||||
topLevelChannels: make(map[int64]struct{}),
|
||||
channels: make(map[int64]*Channel),
|
||||
subChannels: make(map[int64]*SubChannel),
|
||||
sockets: make(map[int64]*Socket),
|
||||
servers: make(map[int64]*Server),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *channelMap) addServer(id int64, s *Server) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
s.cm = c
|
||||
c.servers[id] = s
|
||||
}
|
||||
|
||||
func (c *channelMap) addChannel(id int64, cn *Channel, isTopChannel bool, pid int64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
cn.trace.cm = c
|
||||
c.channels[id] = cn
|
||||
if isTopChannel {
|
||||
c.topLevelChannels[id] = struct{}{}
|
||||
} else if p := c.channels[pid]; p != nil {
|
||||
p.addChild(id, cn)
|
||||
} else {
|
||||
logger.Infof("channel %d references invalid parent ID %d", id, pid)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *channelMap) addSubChannel(id int64, sc *SubChannel, pid int64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
sc.trace.cm = c
|
||||
c.subChannels[id] = sc
|
||||
if p := c.channels[pid]; p != nil {
|
||||
p.addChild(id, sc)
|
||||
} else {
|
||||
logger.Infof("subchannel %d references invalid parent ID %d", id, pid)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *channelMap) addSocket(s *Socket) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
s.cm = c
|
||||
c.sockets[s.ID] = s
|
||||
if s.Parent == nil {
|
||||
logger.Infof("normal socket %d has no parent", s.ID)
|
||||
}
|
||||
s.Parent.(entry).addChild(s.ID, s)
|
||||
}
|
||||
|
||||
// removeEntry triggers the removal of an entry, which may not indeed delete the
|
||||
// entry, if it has to wait on the deletion of its children and until no other
|
||||
// entity's channel trace references it. It may lead to a chain of entry
|
||||
// deletion. For example, deleting the last socket of a gracefully shutting down
|
||||
// server will lead to the server being also deleted.
|
||||
func (c *channelMap) removeEntry(id int64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.findEntry(id).triggerDelete()
|
||||
}
|
||||
|
||||
// tracedChannel represents tracing operations which are present on both
|
||||
// channels and subChannels.
|
||||
type tracedChannel interface {
|
||||
getChannelTrace() *ChannelTrace
|
||||
incrTraceRefCount()
|
||||
decrTraceRefCount()
|
||||
getRefName() string
|
||||
}
|
||||
|
||||
// c.mu must be held by the caller
|
||||
func (c *channelMap) decrTraceRefCount(id int64) {
|
||||
e := c.findEntry(id)
|
||||
if v, ok := e.(tracedChannel); ok {
|
||||
v.decrTraceRefCount()
|
||||
e.deleteSelfIfReady()
|
||||
}
|
||||
}
|
||||
|
||||
// c.mu must be held by the caller.
|
||||
func (c *channelMap) findEntry(id int64) entry {
|
||||
if v, ok := c.channels[id]; ok {
|
||||
return v
|
||||
}
|
||||
if v, ok := c.subChannels[id]; ok {
|
||||
return v
|
||||
}
|
||||
if v, ok := c.servers[id]; ok {
|
||||
return v
|
||||
}
|
||||
if v, ok := c.sockets[id]; ok {
|
||||
return v
|
||||
}
|
||||
return &dummyEntry{idNotFound: id}
|
||||
}
|
||||
|
||||
// c.mu must be held by the caller
|
||||
//
|
||||
// deleteEntry deletes an entry from the channelMap. Before calling this method,
|
||||
// caller must check this entry is ready to be deleted, i.e removeEntry() has
|
||||
// been called on it, and no children still exist.
|
||||
func (c *channelMap) deleteEntry(id int64) entry {
|
||||
if v, ok := c.sockets[id]; ok {
|
||||
delete(c.sockets, id)
|
||||
return v
|
||||
}
|
||||
if v, ok := c.subChannels[id]; ok {
|
||||
delete(c.subChannels, id)
|
||||
return v
|
||||
}
|
||||
if v, ok := c.channels[id]; ok {
|
||||
delete(c.channels, id)
|
||||
delete(c.topLevelChannels, id)
|
||||
return v
|
||||
}
|
||||
if v, ok := c.servers[id]; ok {
|
||||
delete(c.servers, id)
|
||||
return v
|
||||
}
|
||||
return &dummyEntry{idNotFound: id}
|
||||
}
|
||||
|
||||
func (c *channelMap) traceEvent(id int64, desc *TraceEvent) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
child := c.findEntry(id)
|
||||
childTC, ok := child.(tracedChannel)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
childTC.getChannelTrace().append(&traceEvent{Desc: desc.Desc, Severity: desc.Severity, Timestamp: time.Now()})
|
||||
if desc.Parent != nil {
|
||||
parent := c.findEntry(child.getParentID())
|
||||
var chanType RefChannelType
|
||||
switch child.(type) {
|
||||
case *Channel:
|
||||
chanType = RefChannel
|
||||
case *SubChannel:
|
||||
chanType = RefSubChannel
|
||||
}
|
||||
if parentTC, ok := parent.(tracedChannel); ok {
|
||||
parentTC.getChannelTrace().append(&traceEvent{
|
||||
Desc: desc.Parent.Desc,
|
||||
Severity: desc.Parent.Severity,
|
||||
Timestamp: time.Now(),
|
||||
RefID: id,
|
||||
RefName: childTC.getRefName(),
|
||||
RefType: chanType,
|
||||
})
|
||||
childTC.incrTraceRefCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type int64Slice []int64
|
||||
|
||||
func (s int64Slice) Len() int { return len(s) }
|
||||
func (s int64Slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s int64Slice) Less(i, j int) bool { return s[i] < s[j] }
|
||||
|
||||
func copyMap(m map[int64]string) map[int64]string {
|
||||
n := make(map[int64]string)
|
||||
for k, v := range m {
|
||||
n[k] = v
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *channelMap) getTopChannels(id int64, maxResults int) ([]*Channel, bool) {
|
||||
if maxResults <= 0 {
|
||||
maxResults = EntriesPerPage
|
||||
}
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
l := int64(len(c.topLevelChannels))
|
||||
ids := make([]int64, 0, l)
|
||||
|
||||
for k := range c.topLevelChannels {
|
||||
ids = append(ids, k)
|
||||
}
|
||||
sort.Sort(int64Slice(ids))
|
||||
idx := sort.Search(len(ids), func(i int) bool { return ids[i] >= id })
|
||||
end := true
|
||||
var t []*Channel
|
||||
for _, v := range ids[idx:] {
|
||||
if len(t) == maxResults {
|
||||
end = false
|
||||
break
|
||||
}
|
||||
if cn, ok := c.channels[v]; ok {
|
||||
t = append(t, cn)
|
||||
}
|
||||
}
|
||||
return t, end
|
||||
}
|
||||
|
||||
func (c *channelMap) getServers(id int64, maxResults int) ([]*Server, bool) {
|
||||
if maxResults <= 0 {
|
||||
maxResults = EntriesPerPage
|
||||
}
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
ids := make([]int64, 0, len(c.servers))
|
||||
for k := range c.servers {
|
||||
ids = append(ids, k)
|
||||
}
|
||||
sort.Sort(int64Slice(ids))
|
||||
idx := sort.Search(len(ids), func(i int) bool { return ids[i] >= id })
|
||||
end := true
|
||||
var s []*Server
|
||||
for _, v := range ids[idx:] {
|
||||
if len(s) == maxResults {
|
||||
end = false
|
||||
break
|
||||
}
|
||||
if svr, ok := c.servers[v]; ok {
|
||||
s = append(s, svr)
|
||||
}
|
||||
}
|
||||
return s, end
|
||||
}
|
||||
|
||||
func (c *channelMap) getServerSockets(id int64, startID int64, maxResults int) ([]*Socket, bool) {
|
||||
if maxResults <= 0 {
|
||||
maxResults = EntriesPerPage
|
||||
}
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
svr, ok := c.servers[id]
|
||||
if !ok {
|
||||
// server with id doesn't exist.
|
||||
return nil, true
|
||||
}
|
||||
svrskts := svr.sockets
|
||||
ids := make([]int64, 0, len(svrskts))
|
||||
sks := make([]*Socket, 0, min(len(svrskts), maxResults))
|
||||
for k := range svrskts {
|
||||
ids = append(ids, k)
|
||||
}
|
||||
sort.Sort(int64Slice(ids))
|
||||
idx := sort.Search(len(ids), func(i int) bool { return ids[i] >= startID })
|
||||
end := true
|
||||
for _, v := range ids[idx:] {
|
||||
if len(sks) == maxResults {
|
||||
end = false
|
||||
break
|
||||
}
|
||||
if ns, ok := c.sockets[v]; ok {
|
||||
sks = append(sks, ns)
|
||||
}
|
||||
}
|
||||
return sks, end
|
||||
}
|
||||
|
||||
func (c *channelMap) getChannel(id int64) *Channel {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.channels[id]
|
||||
}
|
||||
|
||||
func (c *channelMap) getSubChannel(id int64) *SubChannel {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.subChannels[id]
|
||||
}
|
||||
|
||||
func (c *channelMap) getSocket(id int64) *Socket {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.sockets[id]
|
||||
}
|
||||
|
||||
func (c *channelMap) getServer(id int64) *Server {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.servers[id]
|
||||
}
|
||||
|
||||
type dummyEntry struct {
|
||||
// dummyEntry is a fake entry to handle entry not found case.
|
||||
idNotFound int64
|
||||
Entity
|
||||
}
|
||||
|
||||
func (d *dummyEntry) String() string {
|
||||
return fmt.Sprintf("non-existent entity #%d", d.idNotFound)
|
||||
}
|
||||
|
||||
func (d *dummyEntry) ID() int64 { return d.idNotFound }
|
||||
|
||||
func (d *dummyEntry) addChild(id int64, e entry) {
|
||||
// Note: It is possible for a normal program to reach here under race
|
||||
// condition. For example, there could be a race between ClientConn.Close()
|
||||
// info being propagated to addrConn and http2Client. ClientConn.Close()
|
||||
// cancel the context and result in http2Client to error. The error info is
|
||||
// then caught by transport monitor and before addrConn.tearDown() is called
|
||||
// in side ClientConn.Close(). Therefore, the addrConn will create a new
|
||||
// transport. And when registering the new transport in channelz, its parent
|
||||
// addrConn could have already been torn down and deleted from channelz
|
||||
// tracking, and thus reach the code here.
|
||||
logger.Infof("attempt to add child of type %T with id %d to a parent (id=%d) that doesn't currently exist", e, id, d.idNotFound)
|
||||
}
|
||||
|
||||
func (d *dummyEntry) deleteChild(id int64) {
|
||||
// It is possible for a normal program to reach here under race condition.
|
||||
// Refer to the example described in addChild().
|
||||
logger.Infof("attempt to delete child with id %d from a parent (id=%d) that doesn't currently exist", id, d.idNotFound)
|
||||
}
|
||||
|
||||
func (d *dummyEntry) triggerDelete() {
|
||||
logger.Warningf("attempt to delete an entry (id=%d) that doesn't currently exist", d.idNotFound)
|
||||
}
|
||||
|
||||
func (*dummyEntry) deleteSelfIfReady() {
|
||||
// code should not reach here. deleteSelfIfReady is always called on an existing entry.
|
||||
}
|
||||
|
||||
func (*dummyEntry) getParentID() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Entity is implemented by all channelz types.
|
||||
type Entity interface {
|
||||
isEntity()
|
||||
fmt.Stringer
|
||||
id() int64
|
||||
}
|
||||
|
|
@ -16,47 +16,32 @@
|
|||
*
|
||||
*/
|
||||
|
||||
// Package channelz defines APIs for enabling channelz service, entry
|
||||
// Package channelz defines internal APIs for enabling channelz service, entry
|
||||
// registration/deletion, and accessing channelz data. It also defines channelz
|
||||
// metric struct formats.
|
||||
//
|
||||
// All APIs in this package are experimental.
|
||||
package channelz
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMaxTraceEntry int32 = 30
|
||||
)
|
||||
|
||||
var (
|
||||
// IDGen is the global channelz entity ID generator. It should not be used
|
||||
// outside this package except by tests.
|
||||
IDGen IDGenerator
|
||||
|
||||
db dbWrapper
|
||||
// EntryPerPage defines the number of channelz entries to be shown on a web page.
|
||||
EntryPerPage = int64(50)
|
||||
curState int32
|
||||
maxTraceEntry = defaultMaxTraceEntry
|
||||
db *channelMap = newChannelMap()
|
||||
// EntriesPerPage defines the number of channelz entries to be shown on a web page.
|
||||
EntriesPerPage = 50
|
||||
curState int32
|
||||
)
|
||||
|
||||
// TurnOn turns on channelz data collection.
|
||||
func TurnOn() {
|
||||
if !IsOn() {
|
||||
db.set(newChannelMap())
|
||||
IDGen.Reset()
|
||||
atomic.StoreInt32(&curState, 1)
|
||||
}
|
||||
atomic.StoreInt32(&curState, 1)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -70,49 +55,15 @@ func IsOn() bool {
|
|||
return atomic.LoadInt32(&curState) == 1
|
||||
}
|
||||
|
||||
// SetMaxTraceEntry sets maximum number of trace entry per entity (i.e. channel/subchannel).
|
||||
// Setting it to 0 will disable channel tracing.
|
||||
func SetMaxTraceEntry(i int32) {
|
||||
atomic.StoreInt32(&maxTraceEntry, i)
|
||||
}
|
||||
|
||||
// ResetMaxTraceEntryToDefault resets the maximum number of trace entry per entity to default.
|
||||
func ResetMaxTraceEntryToDefault() {
|
||||
atomic.StoreInt32(&maxTraceEntry, defaultMaxTraceEntry)
|
||||
}
|
||||
|
||||
func getMaxTraceEntry() int {
|
||||
i := atomic.LoadInt32(&maxTraceEntry)
|
||||
return int(i)
|
||||
}
|
||||
|
||||
// dbWarpper wraps around a reference to internal channelz data storage, and
|
||||
// provide synchronized functionality to set and get the reference.
|
||||
type dbWrapper struct {
|
||||
mu sync.RWMutex
|
||||
DB *channelMap
|
||||
}
|
||||
|
||||
func (d *dbWrapper) set(db *channelMap) {
|
||||
d.mu.Lock()
|
||||
d.DB = db
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
func (d *dbWrapper) get() *channelMap {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
return d.DB
|
||||
}
|
||||
|
||||
// GetTopChannels returns a slice of top channel's ChannelMetric, along with a
|
||||
// boolean indicating whether there's more top channels to be queried for.
|
||||
//
|
||||
// The arg id specifies that only top channel with id at or above it will be included
|
||||
// in the result. The returned slice is up to a length of the arg maxResults or
|
||||
// EntryPerPage if maxResults is zero, and is sorted in ascending id order.
|
||||
func GetTopChannels(id int64, maxResults int64) ([]*ChannelMetric, bool) {
|
||||
return db.get().GetTopChannels(id, maxResults)
|
||||
// The arg id specifies that only top channel with id at or above it will be
|
||||
// included in the result. The returned slice is up to a length of the arg
|
||||
// maxResults or EntriesPerPage if maxResults is zero, and is sorted in ascending
|
||||
// id order.
|
||||
func GetTopChannels(id int64, maxResults int) ([]*Channel, bool) {
|
||||
return db.getTopChannels(id, maxResults)
|
||||
}
|
||||
|
||||
// GetServers returns a slice of server's ServerMetric, along with a
|
||||
|
|
@ -120,73 +71,69 @@ func GetTopChannels(id int64, maxResults int64) ([]*ChannelMetric, bool) {
|
|||
//
|
||||
// The arg id specifies that only server with id at or above it will be included
|
||||
// in the result. The returned slice is up to a length of the arg maxResults or
|
||||
// EntryPerPage if maxResults is zero, and is sorted in ascending id order.
|
||||
func GetServers(id int64, maxResults int64) ([]*ServerMetric, bool) {
|
||||
return db.get().GetServers(id, maxResults)
|
||||
// EntriesPerPage if maxResults is zero, and is sorted in ascending id order.
|
||||
func GetServers(id int64, maxResults int) ([]*Server, bool) {
|
||||
return db.getServers(id, maxResults)
|
||||
}
|
||||
|
||||
// GetServerSockets returns a slice of server's (identified by id) normal socket's
|
||||
// SocketMetric, along with a boolean indicating whether there's more sockets to
|
||||
// SocketMetrics, along with a boolean indicating whether there's more sockets to
|
||||
// be queried for.
|
||||
//
|
||||
// The arg startID specifies that only sockets with id at or above it will be
|
||||
// included in the result. The returned slice is up to a length of the arg maxResults
|
||||
// or EntryPerPage if maxResults is zero, and is sorted in ascending id order.
|
||||
func GetServerSockets(id int64, startID int64, maxResults int64) ([]*SocketMetric, bool) {
|
||||
return db.get().GetServerSockets(id, startID, maxResults)
|
||||
// or EntriesPerPage if maxResults is zero, and is sorted in ascending id order.
|
||||
func GetServerSockets(id int64, startID int64, maxResults int) ([]*Socket, bool) {
|
||||
return db.getServerSockets(id, startID, maxResults)
|
||||
}
|
||||
|
||||
// GetChannel returns the ChannelMetric for the channel (identified by id).
|
||||
func GetChannel(id int64) *ChannelMetric {
|
||||
return db.get().GetChannel(id)
|
||||
// GetChannel returns the Channel for the channel (identified by id).
|
||||
func GetChannel(id int64) *Channel {
|
||||
return db.getChannel(id)
|
||||
}
|
||||
|
||||
// GetSubChannel returns the SubChannelMetric for the subchannel (identified by id).
|
||||
func GetSubChannel(id int64) *SubChannelMetric {
|
||||
return db.get().GetSubChannel(id)
|
||||
// GetSubChannel returns the SubChannel for the subchannel (identified by id).
|
||||
func GetSubChannel(id int64) *SubChannel {
|
||||
return db.getSubChannel(id)
|
||||
}
|
||||
|
||||
// GetSocket returns the SocketInternalMetric for the socket (identified by id).
|
||||
func GetSocket(id int64) *SocketMetric {
|
||||
return db.get().GetSocket(id)
|
||||
// GetSocket returns the Socket for the socket (identified by id).
|
||||
func GetSocket(id int64) *Socket {
|
||||
return db.getSocket(id)
|
||||
}
|
||||
|
||||
// GetServer returns the ServerMetric for the server (identified by id).
|
||||
func GetServer(id int64) *ServerMetric {
|
||||
return db.get().GetServer(id)
|
||||
func GetServer(id int64) *Server {
|
||||
return db.getServer(id)
|
||||
}
|
||||
|
||||
// RegisterChannel registers the given channel c in the channelz database with
|
||||
// ref as its reference name, and adds it to the child list of its parent
|
||||
// (identified by pid). pid == nil means no parent.
|
||||
// target as its target and reference name, and adds it to the child list of its
|
||||
// parent. parent == nil means no parent.
|
||||
//
|
||||
// Returns a unique channelz identifier assigned to this channel.
|
||||
//
|
||||
// If channelz is not turned ON, the channelz database is not mutated.
|
||||
func RegisterChannel(c Channel, pid *Identifier, ref string) *Identifier {
|
||||
func RegisterChannel(parent *Channel, target string) *Channel {
|
||||
id := IDGen.genID()
|
||||
var parent int64
|
||||
isTopChannel := true
|
||||
if pid != nil {
|
||||
isTopChannel = false
|
||||
parent = pid.Int()
|
||||
}
|
||||
|
||||
if !IsOn() {
|
||||
return newIdentifer(RefChannel, id, pid)
|
||||
return &Channel{ID: id}
|
||||
}
|
||||
|
||||
cn := &channel{
|
||||
refName: ref,
|
||||
c: c,
|
||||
subChans: make(map[int64]string),
|
||||
isTopChannel := parent == nil
|
||||
|
||||
cn := &Channel{
|
||||
ID: id,
|
||||
RefName: target,
|
||||
nestedChans: make(map[int64]string),
|
||||
id: id,
|
||||
pid: parent,
|
||||
trace: &channelTrace{createdTime: time.Now(), events: make([]*TraceEvent, 0, getMaxTraceEntry())},
|
||||
subChans: make(map[int64]string),
|
||||
Parent: parent,
|
||||
trace: &ChannelTrace{CreationTime: time.Now(), Events: make([]*traceEvent, 0, getMaxTraceEntry())},
|
||||
}
|
||||
db.get().addChannel(id, cn, isTopChannel, parent)
|
||||
return newIdentifer(RefChannel, id, pid)
|
||||
cn.ChannelMetrics.Target.Store(&target)
|
||||
db.addChannel(id, cn, isTopChannel, cn.getParentID())
|
||||
return cn
|
||||
}
|
||||
|
||||
// RegisterSubChannel registers the given subChannel c in the channelz database
|
||||
|
|
@ -196,555 +143,67 @@ func RegisterChannel(c Channel, pid *Identifier, ref string) *Identifier {
|
|||
// Returns a unique channelz identifier assigned to this subChannel.
|
||||
//
|
||||
// If channelz is not turned ON, the channelz database is not mutated.
|
||||
func RegisterSubChannel(c Channel, pid *Identifier, ref string) (*Identifier, error) {
|
||||
if pid == nil {
|
||||
return nil, errors.New("a SubChannel's parent id cannot be nil")
|
||||
}
|
||||
func RegisterSubChannel(parent *Channel, ref string) *SubChannel {
|
||||
id := IDGen.genID()
|
||||
if !IsOn() {
|
||||
return newIdentifer(RefSubChannel, id, pid), nil
|
||||
sc := &SubChannel{
|
||||
ID: id,
|
||||
RefName: ref,
|
||||
parent: parent,
|
||||
}
|
||||
|
||||
sc := &subChannel{
|
||||
refName: ref,
|
||||
c: c,
|
||||
sockets: make(map[int64]string),
|
||||
id: id,
|
||||
pid: pid.Int(),
|
||||
trace: &channelTrace{createdTime: time.Now(), events: make([]*TraceEvent, 0, getMaxTraceEntry())},
|
||||
if !IsOn() {
|
||||
return sc
|
||||
}
|
||||
db.get().addSubChannel(id, sc, pid.Int())
|
||||
return newIdentifer(RefSubChannel, id, pid), nil
|
||||
|
||||
sc.sockets = make(map[int64]string)
|
||||
sc.trace = &ChannelTrace{CreationTime: time.Now(), Events: make([]*traceEvent, 0, getMaxTraceEntry())}
|
||||
db.addSubChannel(id, sc, parent.ID)
|
||||
return sc
|
||||
}
|
||||
|
||||
// RegisterServer registers the given server s in channelz database. It returns
|
||||
// the unique channelz tracking id assigned to this server.
|
||||
//
|
||||
// If channelz is not turned ON, the channelz database is not mutated.
|
||||
func RegisterServer(s Server, ref string) *Identifier {
|
||||
func RegisterServer(ref string) *Server {
|
||||
id := IDGen.genID()
|
||||
if !IsOn() {
|
||||
return newIdentifer(RefServer, id, nil)
|
||||
return &Server{ID: id}
|
||||
}
|
||||
|
||||
svr := &server{
|
||||
refName: ref,
|
||||
s: s,
|
||||
svr := &Server{
|
||||
RefName: ref,
|
||||
sockets: make(map[int64]string),
|
||||
listenSockets: make(map[int64]string),
|
||||
id: id,
|
||||
ID: id,
|
||||
}
|
||||
db.get().addServer(id, svr)
|
||||
return newIdentifer(RefServer, id, nil)
|
||||
db.addServer(id, svr)
|
||||
return svr
|
||||
}
|
||||
|
||||
// RegisterListenSocket registers the given listen socket s in channelz database
|
||||
// with ref as its reference name, and add it to the child list of its parent
|
||||
// (identified by pid). It returns the unique channelz tracking id assigned to
|
||||
// this listen socket.
|
||||
//
|
||||
// If channelz is not turned ON, the channelz database is not mutated.
|
||||
func RegisterListenSocket(s Socket, pid *Identifier, ref string) (*Identifier, error) {
|
||||
if pid == nil {
|
||||
return nil, errors.New("a ListenSocket's parent id cannot be 0")
|
||||
}
|
||||
id := IDGen.genID()
|
||||
if !IsOn() {
|
||||
return newIdentifer(RefListenSocket, id, pid), nil
|
||||
}
|
||||
|
||||
ls := &listenSocket{refName: ref, s: s, id: id, pid: pid.Int()}
|
||||
db.get().addListenSocket(id, ls, pid.Int())
|
||||
return newIdentifer(RefListenSocket, id, pid), nil
|
||||
}
|
||||
|
||||
// RegisterNormalSocket registers the given normal socket s in channelz database
|
||||
// RegisterSocket registers the given normal socket s in channelz database
|
||||
// with ref as its reference name, and adds it to the child list of its parent
|
||||
// (identified by pid). It returns the unique channelz tracking id assigned to
|
||||
// this normal socket.
|
||||
// (identified by skt.Parent, which must be set). It returns the unique channelz
|
||||
// tracking id assigned to this normal socket.
|
||||
//
|
||||
// If channelz is not turned ON, the channelz database is not mutated.
|
||||
func RegisterNormalSocket(s Socket, pid *Identifier, ref string) (*Identifier, error) {
|
||||
if pid == nil {
|
||||
return nil, errors.New("a NormalSocket's parent id cannot be 0")
|
||||
func RegisterSocket(skt *Socket) *Socket {
|
||||
skt.ID = IDGen.genID()
|
||||
if IsOn() {
|
||||
db.addSocket(skt)
|
||||
}
|
||||
id := IDGen.genID()
|
||||
if !IsOn() {
|
||||
return newIdentifer(RefNormalSocket, id, pid), nil
|
||||
}
|
||||
|
||||
ns := &normalSocket{refName: ref, s: s, id: id, pid: pid.Int()}
|
||||
db.get().addNormalSocket(id, ns, pid.Int())
|
||||
return newIdentifer(RefNormalSocket, id, pid), nil
|
||||
return skt
|
||||
}
|
||||
|
||||
// RemoveEntry removes an entry with unique channelz tracking id to be id from
|
||||
// channelz database.
|
||||
//
|
||||
// If channelz is not turned ON, this function is a no-op.
|
||||
func RemoveEntry(id *Identifier) {
|
||||
func RemoveEntry(id int64) {
|
||||
if !IsOn() {
|
||||
return
|
||||
}
|
||||
db.get().removeEntry(id.Int())
|
||||
}
|
||||
|
||||
// TraceEventDesc is what the caller of AddTraceEvent should provide to describe
|
||||
// the event to be added to the channel trace.
|
||||
//
|
||||
// The Parent field is optional. It is used for an event that will be recorded
|
||||
// in the entity's parent trace.
|
||||
type TraceEventDesc struct {
|
||||
Desc string
|
||||
Severity Severity
|
||||
Parent *TraceEventDesc
|
||||
}
|
||||
|
||||
// AddTraceEvent adds trace related to the entity with specified id, using the
|
||||
// provided TraceEventDesc.
|
||||
//
|
||||
// If channelz is not turned ON, this will simply log the event descriptions.
|
||||
func AddTraceEvent(l grpclog.DepthLoggerV2, id *Identifier, depth int, desc *TraceEventDesc) {
|
||||
// Log only the trace description associated with the bottom most entity.
|
||||
switch desc.Severity {
|
||||
case CtUnknown, CtInfo:
|
||||
l.InfoDepth(depth+1, withParens(id)+desc.Desc)
|
||||
case CtWarning:
|
||||
l.WarningDepth(depth+1, withParens(id)+desc.Desc)
|
||||
case CtError:
|
||||
l.ErrorDepth(depth+1, withParens(id)+desc.Desc)
|
||||
}
|
||||
|
||||
if getMaxTraceEntry() == 0 {
|
||||
return
|
||||
}
|
||||
if IsOn() {
|
||||
db.get().traceEvent(id.Int(), desc)
|
||||
}
|
||||
}
|
||||
|
||||
// channelMap is the storage data structure for channelz.
|
||||
// Methods of channelMap can be divided in two two categories with respect to locking.
|
||||
// 1. Methods acquire the global lock.
|
||||
// 2. Methods that can only be called when global lock is held.
|
||||
// A second type of method need always to be called inside a first type of method.
|
||||
type channelMap struct {
|
||||
mu sync.RWMutex
|
||||
topLevelChannels map[int64]struct{}
|
||||
servers map[int64]*server
|
||||
channels map[int64]*channel
|
||||
subChannels map[int64]*subChannel
|
||||
listenSockets map[int64]*listenSocket
|
||||
normalSockets map[int64]*normalSocket
|
||||
}
|
||||
|
||||
func newChannelMap() *channelMap {
|
||||
return &channelMap{
|
||||
topLevelChannels: make(map[int64]struct{}),
|
||||
channels: make(map[int64]*channel),
|
||||
listenSockets: make(map[int64]*listenSocket),
|
||||
normalSockets: make(map[int64]*normalSocket),
|
||||
servers: make(map[int64]*server),
|
||||
subChannels: make(map[int64]*subChannel),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *channelMap) addServer(id int64, s *server) {
|
||||
c.mu.Lock()
|
||||
s.cm = c
|
||||
c.servers[id] = s
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *channelMap) addChannel(id int64, cn *channel, isTopChannel bool, pid int64) {
|
||||
c.mu.Lock()
|
||||
cn.cm = c
|
||||
cn.trace.cm = c
|
||||
c.channels[id] = cn
|
||||
if isTopChannel {
|
||||
c.topLevelChannels[id] = struct{}{}
|
||||
} else {
|
||||
c.findEntry(pid).addChild(id, cn)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *channelMap) addSubChannel(id int64, sc *subChannel, pid int64) {
|
||||
c.mu.Lock()
|
||||
sc.cm = c
|
||||
sc.trace.cm = c
|
||||
c.subChannels[id] = sc
|
||||
c.findEntry(pid).addChild(id, sc)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *channelMap) addListenSocket(id int64, ls *listenSocket, pid int64) {
|
||||
c.mu.Lock()
|
||||
ls.cm = c
|
||||
c.listenSockets[id] = ls
|
||||
c.findEntry(pid).addChild(id, ls)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *channelMap) addNormalSocket(id int64, ns *normalSocket, pid int64) {
|
||||
c.mu.Lock()
|
||||
ns.cm = c
|
||||
c.normalSockets[id] = ns
|
||||
c.findEntry(pid).addChild(id, ns)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// removeEntry triggers the removal of an entry, which may not indeed delete the entry, if it has to
|
||||
// wait on the deletion of its children and until no other entity's channel trace references it.
|
||||
// It may lead to a chain of entry deletion. For example, deleting the last socket of a gracefully
|
||||
// shutting down server will lead to the server being also deleted.
|
||||
func (c *channelMap) removeEntry(id int64) {
|
||||
c.mu.Lock()
|
||||
c.findEntry(id).triggerDelete()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// c.mu must be held by the caller
|
||||
func (c *channelMap) decrTraceRefCount(id int64) {
|
||||
e := c.findEntry(id)
|
||||
if v, ok := e.(tracedChannel); ok {
|
||||
v.decrTraceRefCount()
|
||||
e.deleteSelfIfReady()
|
||||
}
|
||||
}
|
||||
|
||||
// c.mu must be held by the caller.
|
||||
func (c *channelMap) findEntry(id int64) entry {
|
||||
var v entry
|
||||
var ok bool
|
||||
if v, ok = c.channels[id]; ok {
|
||||
return v
|
||||
}
|
||||
if v, ok = c.subChannels[id]; ok {
|
||||
return v
|
||||
}
|
||||
if v, ok = c.servers[id]; ok {
|
||||
return v
|
||||
}
|
||||
if v, ok = c.listenSockets[id]; ok {
|
||||
return v
|
||||
}
|
||||
if v, ok = c.normalSockets[id]; ok {
|
||||
return v
|
||||
}
|
||||
return &dummyEntry{idNotFound: id}
|
||||
}
|
||||
|
||||
// c.mu must be held by the caller
|
||||
// deleteEntry simply deletes an entry from the channelMap. Before calling this
|
||||
// method, caller must check this entry is ready to be deleted, i.e removeEntry()
|
||||
// has been called on it, and no children still exist.
|
||||
// Conditionals are ordered by the expected frequency of deletion of each entity
|
||||
// type, in order to optimize performance.
|
||||
func (c *channelMap) deleteEntry(id int64) {
|
||||
var ok bool
|
||||
if _, ok = c.normalSockets[id]; ok {
|
||||
delete(c.normalSockets, id)
|
||||
return
|
||||
}
|
||||
if _, ok = c.subChannels[id]; ok {
|
||||
delete(c.subChannels, id)
|
||||
return
|
||||
}
|
||||
if _, ok = c.channels[id]; ok {
|
||||
delete(c.channels, id)
|
||||
delete(c.topLevelChannels, id)
|
||||
return
|
||||
}
|
||||
if _, ok = c.listenSockets[id]; ok {
|
||||
delete(c.listenSockets, id)
|
||||
return
|
||||
}
|
||||
if _, ok = c.servers[id]; ok {
|
||||
delete(c.servers, id)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c *channelMap) traceEvent(id int64, desc *TraceEventDesc) {
|
||||
c.mu.Lock()
|
||||
child := c.findEntry(id)
|
||||
childTC, ok := child.(tracedChannel)
|
||||
if !ok {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
childTC.getChannelTrace().append(&TraceEvent{Desc: desc.Desc, Severity: desc.Severity, Timestamp: time.Now()})
|
||||
if desc.Parent != nil {
|
||||
parent := c.findEntry(child.getParentID())
|
||||
var chanType RefChannelType
|
||||
switch child.(type) {
|
||||
case *channel:
|
||||
chanType = RefChannel
|
||||
case *subChannel:
|
||||
chanType = RefSubChannel
|
||||
}
|
||||
if parentTC, ok := parent.(tracedChannel); ok {
|
||||
parentTC.getChannelTrace().append(&TraceEvent{
|
||||
Desc: desc.Parent.Desc,
|
||||
Severity: desc.Parent.Severity,
|
||||
Timestamp: time.Now(),
|
||||
RefID: id,
|
||||
RefName: childTC.getRefName(),
|
||||
RefType: chanType,
|
||||
})
|
||||
childTC.incrTraceRefCount()
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
type int64Slice []int64
|
||||
|
||||
func (s int64Slice) Len() int { return len(s) }
|
||||
func (s int64Slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s int64Slice) Less(i, j int) bool { return s[i] < s[j] }
|
||||
|
||||
func copyMap(m map[int64]string) map[int64]string {
|
||||
n := make(map[int64]string)
|
||||
for k, v := range m {
|
||||
n[k] = v
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func min(a, b int64) int64 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *channelMap) GetTopChannels(id int64, maxResults int64) ([]*ChannelMetric, bool) {
|
||||
if maxResults <= 0 {
|
||||
maxResults = EntryPerPage
|
||||
}
|
||||
c.mu.RLock()
|
||||
l := int64(len(c.topLevelChannels))
|
||||
ids := make([]int64, 0, l)
|
||||
cns := make([]*channel, 0, min(l, maxResults))
|
||||
|
||||
for k := range c.topLevelChannels {
|
||||
ids = append(ids, k)
|
||||
}
|
||||
sort.Sort(int64Slice(ids))
|
||||
idx := sort.Search(len(ids), func(i int) bool { return ids[i] >= id })
|
||||
count := int64(0)
|
||||
var end bool
|
||||
var t []*ChannelMetric
|
||||
for i, v := range ids[idx:] {
|
||||
if count == maxResults {
|
||||
break
|
||||
}
|
||||
if cn, ok := c.channels[v]; ok {
|
||||
cns = append(cns, cn)
|
||||
t = append(t, &ChannelMetric{
|
||||
NestedChans: copyMap(cn.nestedChans),
|
||||
SubChans: copyMap(cn.subChans),
|
||||
})
|
||||
count++
|
||||
}
|
||||
if i == len(ids[idx:])-1 {
|
||||
end = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
if count == 0 {
|
||||
end = true
|
||||
}
|
||||
|
||||
for i, cn := range cns {
|
||||
t[i].ChannelData = cn.c.ChannelzMetric()
|
||||
t[i].ID = cn.id
|
||||
t[i].RefName = cn.refName
|
||||
t[i].Trace = cn.trace.dumpData()
|
||||
}
|
||||
return t, end
|
||||
}
|
||||
|
||||
func (c *channelMap) GetServers(id, maxResults int64) ([]*ServerMetric, bool) {
|
||||
if maxResults <= 0 {
|
||||
maxResults = EntryPerPage
|
||||
}
|
||||
c.mu.RLock()
|
||||
l := int64(len(c.servers))
|
||||
ids := make([]int64, 0, l)
|
||||
ss := make([]*server, 0, min(l, maxResults))
|
||||
for k := range c.servers {
|
||||
ids = append(ids, k)
|
||||
}
|
||||
sort.Sort(int64Slice(ids))
|
||||
idx := sort.Search(len(ids), func(i int) bool { return ids[i] >= id })
|
||||
count := int64(0)
|
||||
var end bool
|
||||
var s []*ServerMetric
|
||||
for i, v := range ids[idx:] {
|
||||
if count == maxResults {
|
||||
break
|
||||
}
|
||||
if svr, ok := c.servers[v]; ok {
|
||||
ss = append(ss, svr)
|
||||
s = append(s, &ServerMetric{
|
||||
ListenSockets: copyMap(svr.listenSockets),
|
||||
})
|
||||
count++
|
||||
}
|
||||
if i == len(ids[idx:])-1 {
|
||||
end = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
if count == 0 {
|
||||
end = true
|
||||
}
|
||||
|
||||
for i, svr := range ss {
|
||||
s[i].ServerData = svr.s.ChannelzMetric()
|
||||
s[i].ID = svr.id
|
||||
s[i].RefName = svr.refName
|
||||
}
|
||||
return s, end
|
||||
}
|
||||
|
||||
func (c *channelMap) GetServerSockets(id int64, startID int64, maxResults int64) ([]*SocketMetric, bool) {
|
||||
if maxResults <= 0 {
|
||||
maxResults = EntryPerPage
|
||||
}
|
||||
var svr *server
|
||||
var ok bool
|
||||
c.mu.RLock()
|
||||
if svr, ok = c.servers[id]; !ok {
|
||||
// server with id doesn't exist.
|
||||
c.mu.RUnlock()
|
||||
return nil, true
|
||||
}
|
||||
svrskts := svr.sockets
|
||||
l := int64(len(svrskts))
|
||||
ids := make([]int64, 0, l)
|
||||
sks := make([]*normalSocket, 0, min(l, maxResults))
|
||||
for k := range svrskts {
|
||||
ids = append(ids, k)
|
||||
}
|
||||
sort.Sort(int64Slice(ids))
|
||||
idx := sort.Search(len(ids), func(i int) bool { return ids[i] >= startID })
|
||||
count := int64(0)
|
||||
var end bool
|
||||
for i, v := range ids[idx:] {
|
||||
if count == maxResults {
|
||||
break
|
||||
}
|
||||
if ns, ok := c.normalSockets[v]; ok {
|
||||
sks = append(sks, ns)
|
||||
count++
|
||||
}
|
||||
if i == len(ids[idx:])-1 {
|
||||
end = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
if count == 0 {
|
||||
end = true
|
||||
}
|
||||
s := make([]*SocketMetric, 0, len(sks))
|
||||
for _, ns := range sks {
|
||||
sm := &SocketMetric{}
|
||||
sm.SocketData = ns.s.ChannelzMetric()
|
||||
sm.ID = ns.id
|
||||
sm.RefName = ns.refName
|
||||
s = append(s, sm)
|
||||
}
|
||||
return s, end
|
||||
}
|
||||
|
||||
func (c *channelMap) GetChannel(id int64) *ChannelMetric {
|
||||
cm := &ChannelMetric{}
|
||||
var cn *channel
|
||||
var ok bool
|
||||
c.mu.RLock()
|
||||
if cn, ok = c.channels[id]; !ok {
|
||||
// channel with id doesn't exist.
|
||||
c.mu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
cm.NestedChans = copyMap(cn.nestedChans)
|
||||
cm.SubChans = copyMap(cn.subChans)
|
||||
// cn.c can be set to &dummyChannel{} when deleteSelfFromMap is called. Save a copy of cn.c when
|
||||
// holding the lock to prevent potential data race.
|
||||
chanCopy := cn.c
|
||||
c.mu.RUnlock()
|
||||
cm.ChannelData = chanCopy.ChannelzMetric()
|
||||
cm.ID = cn.id
|
||||
cm.RefName = cn.refName
|
||||
cm.Trace = cn.trace.dumpData()
|
||||
return cm
|
||||
}
|
||||
|
||||
func (c *channelMap) GetSubChannel(id int64) *SubChannelMetric {
|
||||
cm := &SubChannelMetric{}
|
||||
var sc *subChannel
|
||||
var ok bool
|
||||
c.mu.RLock()
|
||||
if sc, ok = c.subChannels[id]; !ok {
|
||||
// subchannel with id doesn't exist.
|
||||
c.mu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
cm.Sockets = copyMap(sc.sockets)
|
||||
// sc.c can be set to &dummyChannel{} when deleteSelfFromMap is called. Save a copy of sc.c when
|
||||
// holding the lock to prevent potential data race.
|
||||
chanCopy := sc.c
|
||||
c.mu.RUnlock()
|
||||
cm.ChannelData = chanCopy.ChannelzMetric()
|
||||
cm.ID = sc.id
|
||||
cm.RefName = sc.refName
|
||||
cm.Trace = sc.trace.dumpData()
|
||||
return cm
|
||||
}
|
||||
|
||||
func (c *channelMap) GetSocket(id int64) *SocketMetric {
|
||||
sm := &SocketMetric{}
|
||||
c.mu.RLock()
|
||||
if ls, ok := c.listenSockets[id]; ok {
|
||||
c.mu.RUnlock()
|
||||
sm.SocketData = ls.s.ChannelzMetric()
|
||||
sm.ID = ls.id
|
||||
sm.RefName = ls.refName
|
||||
return sm
|
||||
}
|
||||
if ns, ok := c.normalSockets[id]; ok {
|
||||
c.mu.RUnlock()
|
||||
sm.SocketData = ns.s.ChannelzMetric()
|
||||
sm.ID = ns.id
|
||||
sm.RefName = ns.refName
|
||||
return sm
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *channelMap) GetServer(id int64) *ServerMetric {
|
||||
sm := &ServerMetric{}
|
||||
var svr *server
|
||||
var ok bool
|
||||
c.mu.RLock()
|
||||
if svr, ok = c.servers[id]; !ok {
|
||||
c.mu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
sm.ListenSockets = copyMap(svr.listenSockets)
|
||||
c.mu.RUnlock()
|
||||
sm.ID = svr.id
|
||||
sm.RefName = svr.refName
|
||||
sm.ServerData = svr.s.ChannelzMetric()
|
||||
return sm
|
||||
db.removeEntry(id)
|
||||
}
|
||||
|
||||
// IDGenerator is an incrementing atomic that tracks IDs for channelz entities.
|
||||
|
|
@ -761,3 +220,11 @@ func (i *IDGenerator) Reset() {
|
|||
func (i *IDGenerator) genID() int64 {
|
||||
return atomic.AddInt64(&i.id, 1)
|
||||
}
|
||||
|
||||
// Identifier is an opaque channelz identifier used to expose channelz symbols
|
||||
// outside of grpc. Currently only implemented by Channel since no other
|
||||
// types require exposure outside grpc.
|
||||
type Identifier interface {
|
||||
Entity
|
||||
channelzIdentifier()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC authors.
|
||||
*
|
||||
* 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 channelz
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Identifier is an opaque identifier which uniquely identifies an entity in the
|
||||
// channelz database.
|
||||
type Identifier struct {
|
||||
typ RefChannelType
|
||||
id int64
|
||||
str string
|
||||
pid *Identifier
|
||||
}
|
||||
|
||||
// Type returns the entity type corresponding to id.
|
||||
func (id *Identifier) Type() RefChannelType {
|
||||
return id.typ
|
||||
}
|
||||
|
||||
// Int returns the integer identifier corresponding to id.
|
||||
func (id *Identifier) Int() int64 {
|
||||
return id.id
|
||||
}
|
||||
|
||||
// String returns a string representation of the entity corresponding to id.
|
||||
//
|
||||
// This includes some information about the parent as well. Examples:
|
||||
// Top-level channel: [Channel #channel-number]
|
||||
// Nested channel: [Channel #parent-channel-number Channel #channel-number]
|
||||
// Sub channel: [Channel #parent-channel SubChannel #subchannel-number]
|
||||
func (id *Identifier) String() string {
|
||||
return id.str
|
||||
}
|
||||
|
||||
// Equal returns true if other is the same as id.
|
||||
func (id *Identifier) Equal(other *Identifier) bool {
|
||||
if (id != nil) != (other != nil) {
|
||||
return false
|
||||
}
|
||||
if id == nil && other == nil {
|
||||
return true
|
||||
}
|
||||
return id.typ == other.typ && id.id == other.id && id.pid == other.pid
|
||||
}
|
||||
|
||||
// NewIdentifierForTesting returns a new opaque identifier to be used only for
|
||||
// testing purposes.
|
||||
func NewIdentifierForTesting(typ RefChannelType, id int64, pid *Identifier) *Identifier {
|
||||
return newIdentifer(typ, id, pid)
|
||||
}
|
||||
|
||||
func newIdentifer(typ RefChannelType, id int64, pid *Identifier) *Identifier {
|
||||
str := fmt.Sprintf("%s #%d", typ, id)
|
||||
if pid != nil {
|
||||
str = fmt.Sprintf("%s %s", pid, str)
|
||||
}
|
||||
return &Identifier{typ: typ, id: id, str: str, pid: pid}
|
||||
}
|
||||
|
|
@ -26,53 +26,49 @@ import (
|
|||
|
||||
var logger = grpclog.Component("channelz")
|
||||
|
||||
func withParens(id *Identifier) string {
|
||||
return "[" + id.String() + "] "
|
||||
}
|
||||
|
||||
// Info logs and adds a trace event if channelz is on.
|
||||
func Info(l grpclog.DepthLoggerV2, id *Identifier, args ...any) {
|
||||
AddTraceEvent(l, id, 1, &TraceEventDesc{
|
||||
func Info(l grpclog.DepthLoggerV2, e Entity, args ...any) {
|
||||
AddTraceEvent(l, e, 1, &TraceEvent{
|
||||
Desc: fmt.Sprint(args...),
|
||||
Severity: CtInfo,
|
||||
})
|
||||
}
|
||||
|
||||
// Infof logs and adds a trace event if channelz is on.
|
||||
func Infof(l grpclog.DepthLoggerV2, id *Identifier, format string, args ...any) {
|
||||
AddTraceEvent(l, id, 1, &TraceEventDesc{
|
||||
func Infof(l grpclog.DepthLoggerV2, e Entity, format string, args ...any) {
|
||||
AddTraceEvent(l, e, 1, &TraceEvent{
|
||||
Desc: fmt.Sprintf(format, args...),
|
||||
Severity: CtInfo,
|
||||
})
|
||||
}
|
||||
|
||||
// Warning logs and adds a trace event if channelz is on.
|
||||
func Warning(l grpclog.DepthLoggerV2, id *Identifier, args ...any) {
|
||||
AddTraceEvent(l, id, 1, &TraceEventDesc{
|
||||
func Warning(l grpclog.DepthLoggerV2, e Entity, args ...any) {
|
||||
AddTraceEvent(l, e, 1, &TraceEvent{
|
||||
Desc: fmt.Sprint(args...),
|
||||
Severity: CtWarning,
|
||||
})
|
||||
}
|
||||
|
||||
// Warningf logs and adds a trace event if channelz is on.
|
||||
func Warningf(l grpclog.DepthLoggerV2, id *Identifier, format string, args ...any) {
|
||||
AddTraceEvent(l, id, 1, &TraceEventDesc{
|
||||
func Warningf(l grpclog.DepthLoggerV2, e Entity, format string, args ...any) {
|
||||
AddTraceEvent(l, e, 1, &TraceEvent{
|
||||
Desc: fmt.Sprintf(format, args...),
|
||||
Severity: CtWarning,
|
||||
})
|
||||
}
|
||||
|
||||
// Error logs and adds a trace event if channelz is on.
|
||||
func Error(l grpclog.DepthLoggerV2, id *Identifier, args ...any) {
|
||||
AddTraceEvent(l, id, 1, &TraceEventDesc{
|
||||
func Error(l grpclog.DepthLoggerV2, e Entity, args ...any) {
|
||||
AddTraceEvent(l, e, 1, &TraceEvent{
|
||||
Desc: fmt.Sprint(args...),
|
||||
Severity: CtError,
|
||||
})
|
||||
}
|
||||
|
||||
// Errorf logs and adds a trace event if channelz is on.
|
||||
func Errorf(l grpclog.DepthLoggerV2, id *Identifier, format string, args ...any) {
|
||||
AddTraceEvent(l, id, 1, &TraceEventDesc{
|
||||
func Errorf(l grpclog.DepthLoggerV2, e Entity, format string, args ...any) {
|
||||
AddTraceEvent(l, e, 1, &TraceEvent{
|
||||
Desc: fmt.Sprintf(format, args...),
|
||||
Severity: CtError,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
*
|
||||
* Copyright 2024 gRPC authors.
|
||||
*
|
||||
* 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 channelz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Server is the channelz representation of a server.
|
||||
type Server struct {
|
||||
Entity
|
||||
ID int64
|
||||
RefName string
|
||||
|
||||
ServerMetrics ServerMetrics
|
||||
|
||||
closeCalled bool
|
||||
sockets map[int64]string
|
||||
listenSockets map[int64]string
|
||||
cm *channelMap
|
||||
}
|
||||
|
||||
// ServerMetrics defines a struct containing metrics for servers.
|
||||
type ServerMetrics struct {
|
||||
// The number of incoming calls started on the server.
|
||||
CallsStarted atomic.Int64
|
||||
// The number of incoming calls that have completed with an OK status.
|
||||
CallsSucceeded atomic.Int64
|
||||
// The number of incoming calls that have a completed with a non-OK status.
|
||||
CallsFailed atomic.Int64
|
||||
// The last time a call was started on the server.
|
||||
LastCallStartedTimestamp atomic.Int64
|
||||
}
|
||||
|
||||
// NewServerMetricsForTesting returns an initialized ServerMetrics.
|
||||
func NewServerMetricsForTesting(started, succeeded, failed, timestamp int64) *ServerMetrics {
|
||||
sm := &ServerMetrics{}
|
||||
sm.CallsStarted.Store(started)
|
||||
sm.CallsSucceeded.Store(succeeded)
|
||||
sm.CallsFailed.Store(failed)
|
||||
sm.LastCallStartedTimestamp.Store(timestamp)
|
||||
return sm
|
||||
}
|
||||
|
||||
func (sm *ServerMetrics) CopyFrom(o *ServerMetrics) {
|
||||
sm.CallsStarted.Store(o.CallsStarted.Load())
|
||||
sm.CallsSucceeded.Store(o.CallsSucceeded.Load())
|
||||
sm.CallsFailed.Store(o.CallsFailed.Load())
|
||||
sm.LastCallStartedTimestamp.Store(o.LastCallStartedTimestamp.Load())
|
||||
}
|
||||
|
||||
// ListenSockets returns the listening sockets for s.
|
||||
func (s *Server) ListenSockets() map[int64]string {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return copyMap(s.listenSockets)
|
||||
}
|
||||
|
||||
// String returns a printable description of s.
|
||||
func (s *Server) String() string {
|
||||
return fmt.Sprintf("Server #%d", s.ID)
|
||||
}
|
||||
|
||||
func (s *Server) id() int64 {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
func (s *Server) addChild(id int64, e entry) {
|
||||
switch v := e.(type) {
|
||||
case *Socket:
|
||||
switch v.SocketType {
|
||||
case SocketTypeNormal:
|
||||
s.sockets[id] = v.RefName
|
||||
case SocketTypeListen:
|
||||
s.listenSockets[id] = v.RefName
|
||||
}
|
||||
default:
|
||||
logger.Errorf("cannot add a child (id = %d) of type %T to a server", id, e)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) deleteChild(id int64) {
|
||||
delete(s.sockets, id)
|
||||
delete(s.listenSockets, id)
|
||||
s.deleteSelfIfReady()
|
||||
}
|
||||
|
||||
func (s *Server) triggerDelete() {
|
||||
s.closeCalled = true
|
||||
s.deleteSelfIfReady()
|
||||
}
|
||||
|
||||
func (s *Server) deleteSelfIfReady() {
|
||||
if !s.closeCalled || len(s.sockets)+len(s.listenSockets) != 0 {
|
||||
return
|
||||
}
|
||||
s.cm.deleteEntry(s.ID)
|
||||
}
|
||||
|
||||
func (s *Server) getParentID() int64 {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
*
|
||||
* Copyright 2024 gRPC authors.
|
||||
*
|
||||
* 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 channelz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
// SocketMetrics defines the struct that the implementor of Socket interface
|
||||
// should return from ChannelzMetric().
|
||||
type SocketMetrics struct {
|
||||
// The number of streams that have been started.
|
||||
StreamsStarted atomic.Int64
|
||||
// The number of streams that have ended successfully:
|
||||
// On client side, receiving frame with eos bit set.
|
||||
// On server side, sending frame with eos bit set.
|
||||
StreamsSucceeded atomic.Int64
|
||||
// The number of streams that have ended unsuccessfully:
|
||||
// On client side, termination without receiving frame with eos bit set.
|
||||
// On server side, termination without sending frame with eos bit set.
|
||||
StreamsFailed atomic.Int64
|
||||
// The number of messages successfully sent on this socket.
|
||||
MessagesSent atomic.Int64
|
||||
MessagesReceived atomic.Int64
|
||||
// The number of keep alives sent. This is typically implemented with HTTP/2
|
||||
// ping messages.
|
||||
KeepAlivesSent atomic.Int64
|
||||
// The last time a stream was created by this endpoint. Usually unset for
|
||||
// servers.
|
||||
LastLocalStreamCreatedTimestamp atomic.Int64
|
||||
// The last time a stream was created by the remote endpoint. Usually unset
|
||||
// for clients.
|
||||
LastRemoteStreamCreatedTimestamp atomic.Int64
|
||||
// The last time a message was sent by this endpoint.
|
||||
LastMessageSentTimestamp atomic.Int64
|
||||
// The last time a message was received by this endpoint.
|
||||
LastMessageReceivedTimestamp atomic.Int64
|
||||
}
|
||||
|
||||
// EphemeralSocketMetrics are metrics that change rapidly and are tracked
|
||||
// outside of channelz.
|
||||
type EphemeralSocketMetrics struct {
|
||||
// The amount of window, granted to the local endpoint by the remote endpoint.
|
||||
// This may be slightly out of date due to network latency. This does NOT
|
||||
// include stream level or TCP level flow control info.
|
||||
LocalFlowControlWindow int64
|
||||
// The amount of window, granted to the remote endpoint by the local endpoint.
|
||||
// This may be slightly out of date due to network latency. This does NOT
|
||||
// include stream level or TCP level flow control info.
|
||||
RemoteFlowControlWindow int64
|
||||
}
|
||||
|
||||
type SocketType string
|
||||
|
||||
const (
|
||||
SocketTypeNormal = "NormalSocket"
|
||||
SocketTypeListen = "ListenSocket"
|
||||
)
|
||||
|
||||
type Socket struct {
|
||||
Entity
|
||||
SocketType SocketType
|
||||
ID int64
|
||||
Parent Entity
|
||||
cm *channelMap
|
||||
SocketMetrics SocketMetrics
|
||||
EphemeralMetrics func() *EphemeralSocketMetrics
|
||||
|
||||
RefName string
|
||||
// The locally bound address. Immutable.
|
||||
LocalAddr net.Addr
|
||||
// The remote bound address. May be absent. Immutable.
|
||||
RemoteAddr net.Addr
|
||||
// Optional, represents the name of the remote endpoint, if different than
|
||||
// the original target name. Immutable.
|
||||
RemoteName string
|
||||
// Immutable.
|
||||
SocketOptions *SocketOptionData
|
||||
// Immutable.
|
||||
Security credentials.ChannelzSecurityValue
|
||||
}
|
||||
|
||||
func (ls *Socket) String() string {
|
||||
return fmt.Sprintf("%s %s #%d", ls.Parent, ls.SocketType, ls.ID)
|
||||
}
|
||||
|
||||
func (ls *Socket) id() int64 {
|
||||
return ls.ID
|
||||
}
|
||||
|
||||
func (ls *Socket) addChild(id int64, e entry) {
|
||||
logger.Errorf("cannot add a child (id = %d) of type %T to a listen socket", id, e)
|
||||
}
|
||||
|
||||
func (ls *Socket) deleteChild(id int64) {
|
||||
logger.Errorf("cannot delete a child (id = %d) from a listen socket", id)
|
||||
}
|
||||
|
||||
func (ls *Socket) triggerDelete() {
|
||||
ls.cm.deleteEntry(ls.ID)
|
||||
ls.Parent.(entry).deleteChild(ls.ID)
|
||||
}
|
||||
|
||||
func (ls *Socket) deleteSelfIfReady() {
|
||||
logger.Errorf("cannot call deleteSelfIfReady on a listen socket")
|
||||
}
|
||||
|
||||
func (ls *Socket) getParentID() int64 {
|
||||
return ls.Parent.id()
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
*
|
||||
* Copyright 2024 gRPC authors.
|
||||
*
|
||||
* 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 channelz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// SubChannel is the channelz representation of a subchannel.
|
||||
type SubChannel struct {
|
||||
Entity
|
||||
// ID is the channelz id of this subchannel.
|
||||
ID int64
|
||||
// RefName is the human readable reference string of this subchannel.
|
||||
RefName string
|
||||
closeCalled bool
|
||||
sockets map[int64]string
|
||||
parent *Channel
|
||||
trace *ChannelTrace
|
||||
traceRefCount int32
|
||||
|
||||
ChannelMetrics ChannelMetrics
|
||||
}
|
||||
|
||||
func (sc *SubChannel) String() string {
|
||||
return fmt.Sprintf("%s SubChannel #%d", sc.parent, sc.ID)
|
||||
}
|
||||
|
||||
func (sc *SubChannel) id() int64 {
|
||||
return sc.ID
|
||||
}
|
||||
|
||||
func (sc *SubChannel) Sockets() map[int64]string {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return copyMap(sc.sockets)
|
||||
}
|
||||
|
||||
func (sc *SubChannel) Trace() *ChannelTrace {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return sc.trace.copy()
|
||||
}
|
||||
|
||||
func (sc *SubChannel) addChild(id int64, e entry) {
|
||||
if v, ok := e.(*Socket); ok && v.SocketType == SocketTypeNormal {
|
||||
sc.sockets[id] = v.RefName
|
||||
} else {
|
||||
logger.Errorf("cannot add a child (id = %d) of type %T to a subChannel", id, e)
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *SubChannel) deleteChild(id int64) {
|
||||
delete(sc.sockets, id)
|
||||
sc.deleteSelfIfReady()
|
||||
}
|
||||
|
||||
func (sc *SubChannel) triggerDelete() {
|
||||
sc.closeCalled = true
|
||||
sc.deleteSelfIfReady()
|
||||
}
|
||||
|
||||
func (sc *SubChannel) getParentID() int64 {
|
||||
return sc.parent.ID
|
||||
}
|
||||
|
||||
// deleteSelfFromTree tries to delete the subchannel from the channelz entry relation tree, which
|
||||
// means deleting the subchannel reference from its parent's child list.
|
||||
//
|
||||
// In order for a subchannel to be deleted from the tree, it must meet the criteria that, removal of
|
||||
// the corresponding grpc object has been invoked, and the subchannel does not have any children left.
|
||||
//
|
||||
// The returned boolean value indicates whether the channel has been successfully deleted from tree.
|
||||
func (sc *SubChannel) deleteSelfFromTree() (deleted bool) {
|
||||
if !sc.closeCalled || len(sc.sockets) != 0 {
|
||||
return false
|
||||
}
|
||||
sc.parent.deleteChild(sc.ID)
|
||||
return true
|
||||
}
|
||||
|
||||
// deleteSelfFromMap checks whether it is valid to delete the subchannel from the map, which means
|
||||
// deleting the subchannel from channelz's tracking entirely. Users can no longer use id to query
|
||||
// the subchannel, and its memory will be garbage collected.
|
||||
//
|
||||
// The trace reference count of the subchannel must be 0 in order to be deleted from the map. This is
|
||||
// specified in the channel tracing gRFC that as long as some other trace has reference to an entity,
|
||||
// the trace of the referenced entity must not be deleted. In order to release the resource allocated
|
||||
// by grpc, the reference to the grpc object is reset to a dummy object.
|
||||
//
|
||||
// deleteSelfFromMap must be called after deleteSelfFromTree returns true.
|
||||
//
|
||||
// It returns a bool to indicate whether the channel can be safely deleted from map.
|
||||
func (sc *SubChannel) deleteSelfFromMap() (delete bool) {
|
||||
return sc.getTraceRefCount() == 0
|
||||
}
|
||||
|
||||
// deleteSelfIfReady tries to delete the subchannel itself from the channelz database.
|
||||
// The delete process includes two steps:
|
||||
// 1. delete the subchannel from the entry relation tree, i.e. delete the subchannel reference from
|
||||
// its parent's child list.
|
||||
// 2. delete the subchannel from the map, i.e. delete the subchannel entirely from channelz. Lookup
|
||||
// by id will return entry not found error.
|
||||
func (sc *SubChannel) deleteSelfIfReady() {
|
||||
if !sc.deleteSelfFromTree() {
|
||||
return
|
||||
}
|
||||
if !sc.deleteSelfFromMap() {
|
||||
return
|
||||
}
|
||||
db.deleteEntry(sc.ID)
|
||||
sc.trace.clear()
|
||||
}
|
||||
|
||||
func (sc *SubChannel) getChannelTrace() *ChannelTrace {
|
||||
return sc.trace
|
||||
}
|
||||
|
||||
func (sc *SubChannel) incrTraceRefCount() {
|
||||
atomic.AddInt32(&sc.traceRefCount, 1)
|
||||
}
|
||||
|
||||
func (sc *SubChannel) decrTraceRefCount() {
|
||||
atomic.AddInt32(&sc.traceRefCount, -1)
|
||||
}
|
||||
|
||||
func (sc *SubChannel) getTraceRefCount() int {
|
||||
i := atomic.LoadInt32(&sc.traceRefCount)
|
||||
return int(i)
|
||||
}
|
||||
|
||||
func (sc *SubChannel) getRefName() string {
|
||||
return sc.RefName
|
||||
}
|
||||
|
|
@ -49,3 +49,17 @@ func (s *SocketOptionData) Getsockopt(fd uintptr) {
|
|||
s.TCPInfo = v
|
||||
}
|
||||
}
|
||||
|
||||
// GetSocketOption gets the socket option info of the conn.
|
||||
func GetSocketOption(socket any) *SocketOptionData {
|
||||
c, ok := socket.(syscall.Conn)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
data := &SocketOptionData{}
|
||||
if rawConn, err := c.SyscallConn(); err == nil {
|
||||
rawConn.Control(data.Getsockopt)
|
||||
return data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
/*
|
||||
*
|
||||
|
|
@ -41,3 +40,8 @@ func (s *SocketOptionData) Getsockopt(fd uintptr) {
|
|||
logger.Warning("Channelz: socket options are not supported on non-linux environments")
|
||||
})
|
||||
}
|
||||
|
||||
// GetSocketOption gets the socket option info of the conn.
|
||||
func GetSocketOption(c any) *SocketOptionData {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
*
|
||||
* Copyright 2018 gRPC authors.
|
||||
*
|
||||
* 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 channelz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/grpclog"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMaxTraceEntry int32 = 30
|
||||
)
|
||||
|
||||
var maxTraceEntry = defaultMaxTraceEntry
|
||||
|
||||
// SetMaxTraceEntry sets maximum number of trace entries per entity (i.e.
|
||||
// channel/subchannel). Setting it to 0 will disable channel tracing.
|
||||
func SetMaxTraceEntry(i int32) {
|
||||
atomic.StoreInt32(&maxTraceEntry, i)
|
||||
}
|
||||
|
||||
// ResetMaxTraceEntryToDefault resets the maximum number of trace entries per
|
||||
// entity to default.
|
||||
func ResetMaxTraceEntryToDefault() {
|
||||
atomic.StoreInt32(&maxTraceEntry, defaultMaxTraceEntry)
|
||||
}
|
||||
|
||||
func getMaxTraceEntry() int {
|
||||
i := atomic.LoadInt32(&maxTraceEntry)
|
||||
return int(i)
|
||||
}
|
||||
|
||||
// traceEvent is an internal representation of a single trace event
|
||||
type traceEvent struct {
|
||||
// Desc is a simple description of the trace event.
|
||||
Desc string
|
||||
// Severity states the severity of this trace event.
|
||||
Severity Severity
|
||||
// Timestamp is the event time.
|
||||
Timestamp time.Time
|
||||
// RefID is the id of the entity that gets referenced in the event. RefID is 0 if no other entity is
|
||||
// involved in this event.
|
||||
// e.g. SubChannel (id: 4[]) Created. --> RefID = 4, RefName = "" (inside [])
|
||||
RefID int64
|
||||
// RefName is the reference name for the entity that gets referenced in the event.
|
||||
RefName string
|
||||
// RefType indicates the referenced entity type, i.e Channel or SubChannel.
|
||||
RefType RefChannelType
|
||||
}
|
||||
|
||||
// TraceEvent is what the caller of AddTraceEvent should provide to describe the
|
||||
// event to be added to the channel trace.
|
||||
//
|
||||
// The Parent field is optional. It is used for an event that will be recorded
|
||||
// in the entity's parent trace.
|
||||
type TraceEvent struct {
|
||||
Desc string
|
||||
Severity Severity
|
||||
Parent *TraceEvent
|
||||
}
|
||||
|
||||
type ChannelTrace struct {
|
||||
cm *channelMap
|
||||
clearCalled bool
|
||||
CreationTime time.Time
|
||||
EventNum int64
|
||||
mu sync.Mutex
|
||||
Events []*traceEvent
|
||||
}
|
||||
|
||||
func (c *ChannelTrace) copy() *ChannelTrace {
|
||||
return &ChannelTrace{
|
||||
CreationTime: c.CreationTime,
|
||||
EventNum: c.EventNum,
|
||||
Events: append(([]*traceEvent)(nil), c.Events...),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ChannelTrace) append(e *traceEvent) {
|
||||
c.mu.Lock()
|
||||
if len(c.Events) == getMaxTraceEntry() {
|
||||
del := c.Events[0]
|
||||
c.Events = c.Events[1:]
|
||||
if del.RefID != 0 {
|
||||
// start recursive cleanup in a goroutine to not block the call originated from grpc.
|
||||
go func() {
|
||||
// need to acquire c.cm.mu lock to call the unlocked attemptCleanup func.
|
||||
c.cm.mu.Lock()
|
||||
c.cm.decrTraceRefCount(del.RefID)
|
||||
c.cm.mu.Unlock()
|
||||
}()
|
||||
}
|
||||
}
|
||||
e.Timestamp = time.Now()
|
||||
c.Events = append(c.Events, e)
|
||||
c.EventNum++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *ChannelTrace) clear() {
|
||||
if c.clearCalled {
|
||||
return
|
||||
}
|
||||
c.clearCalled = true
|
||||
c.mu.Lock()
|
||||
for _, e := range c.Events {
|
||||
if e.RefID != 0 {
|
||||
// caller should have already held the c.cm.mu lock.
|
||||
c.cm.decrTraceRefCount(e.RefID)
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Severity is the severity level of a trace event.
|
||||
// The canonical enumeration of all valid values is here:
|
||||
// https://github.com/grpc/grpc-proto/blob/9b13d199cc0d4703c7ea26c9c330ba695866eb23/grpc/channelz/v1/channelz.proto#L126.
|
||||
type Severity int
|
||||
|
||||
const (
|
||||
// CtUnknown indicates unknown severity of a trace event.
|
||||
CtUnknown Severity = iota
|
||||
// CtInfo indicates info level severity of a trace event.
|
||||
CtInfo
|
||||
// CtWarning indicates warning level severity of a trace event.
|
||||
CtWarning
|
||||
// CtError indicates error level severity of a trace event.
|
||||
CtError
|
||||
)
|
||||
|
||||
// RefChannelType is the type of the entity being referenced in a trace event.
|
||||
type RefChannelType int
|
||||
|
||||
const (
|
||||
// RefUnknown indicates an unknown entity type, the zero value for this type.
|
||||
RefUnknown RefChannelType = iota
|
||||
// RefChannel indicates the referenced entity is a Channel.
|
||||
RefChannel
|
||||
// RefSubChannel indicates the referenced entity is a SubChannel.
|
||||
RefSubChannel
|
||||
// RefServer indicates the referenced entity is a Server.
|
||||
RefServer
|
||||
// RefListenSocket indicates the referenced entity is a ListenSocket.
|
||||
RefListenSocket
|
||||
// RefNormalSocket indicates the referenced entity is a NormalSocket.
|
||||
RefNormalSocket
|
||||
)
|
||||
|
||||
var refChannelTypeToString = map[RefChannelType]string{
|
||||
RefUnknown: "Unknown",
|
||||
RefChannel: "Channel",
|
||||
RefSubChannel: "SubChannel",
|
||||
RefServer: "Server",
|
||||
RefListenSocket: "ListenSocket",
|
||||
RefNormalSocket: "NormalSocket",
|
||||
}
|
||||
|
||||
func (r RefChannelType) String() string {
|
||||
return refChannelTypeToString[r]
|
||||
}
|
||||
|
||||
// AddTraceEvent adds trace related to the entity with specified id, using the
|
||||
// provided TraceEventDesc.
|
||||
//
|
||||
// If channelz is not turned ON, this will simply log the event descriptions.
|
||||
func AddTraceEvent(l grpclog.DepthLoggerV2, e Entity, depth int, desc *TraceEvent) {
|
||||
// Log only the trace description associated with the bottom most entity.
|
||||
d := fmt.Sprintf("[%s]%s", e, desc.Desc)
|
||||
switch desc.Severity {
|
||||
case CtUnknown, CtInfo:
|
||||
l.InfoDepth(depth+1, d)
|
||||
case CtWarning:
|
||||
l.WarningDepth(depth+1, d)
|
||||
case CtError:
|
||||
l.ErrorDepth(depth+1, d)
|
||||
}
|
||||
|
||||
if getMaxTraceEntry() == 0 {
|
||||
return
|
||||
}
|
||||
if IsOn() {
|
||||
db.traceEvent(e.id(), desc)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,727 +0,0 @@
|
|||
/*
|
||||
*
|
||||
* Copyright 2018 gRPC authors.
|
||||
*
|
||||
* 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 channelz
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
// entry represents a node in the channelz database.
|
||||
type entry interface {
|
||||
// addChild adds a child e, whose channelz id is id to child list
|
||||
addChild(id int64, e entry)
|
||||
// deleteChild deletes a child with channelz id to be id from child list
|
||||
deleteChild(id int64)
|
||||
// triggerDelete tries to delete self from channelz database. However, if child
|
||||
// list is not empty, then deletion from the database is on hold until the last
|
||||
// child is deleted from database.
|
||||
triggerDelete()
|
||||
// deleteSelfIfReady check whether triggerDelete() has been called before, and whether child
|
||||
// list is now empty. If both conditions are met, then delete self from database.
|
||||
deleteSelfIfReady()
|
||||
// getParentID returns parent ID of the entry. 0 value parent ID means no parent.
|
||||
getParentID() int64
|
||||
}
|
||||
|
||||
// dummyEntry is a fake entry to handle entry not found case.
|
||||
type dummyEntry struct {
|
||||
idNotFound int64
|
||||
}
|
||||
|
||||
func (d *dummyEntry) addChild(id int64, e entry) {
|
||||
// Note: It is possible for a normal program to reach here under race condition.
|
||||
// For example, there could be a race between ClientConn.Close() info being propagated
|
||||
// to addrConn and http2Client. ClientConn.Close() cancel the context and result
|
||||
// in http2Client to error. The error info is then caught by transport monitor
|
||||
// and before addrConn.tearDown() is called in side ClientConn.Close(). Therefore,
|
||||
// the addrConn will create a new transport. And when registering the new transport in
|
||||
// channelz, its parent addrConn could have already been torn down and deleted
|
||||
// from channelz tracking, and thus reach the code here.
|
||||
logger.Infof("attempt to add child of type %T with id %d to a parent (id=%d) that doesn't currently exist", e, id, d.idNotFound)
|
||||
}
|
||||
|
||||
func (d *dummyEntry) deleteChild(id int64) {
|
||||
// It is possible for a normal program to reach here under race condition.
|
||||
// Refer to the example described in addChild().
|
||||
logger.Infof("attempt to delete child with id %d from a parent (id=%d) that doesn't currently exist", id, d.idNotFound)
|
||||
}
|
||||
|
||||
func (d *dummyEntry) triggerDelete() {
|
||||
logger.Warningf("attempt to delete an entry (id=%d) that doesn't currently exist", d.idNotFound)
|
||||
}
|
||||
|
||||
func (*dummyEntry) deleteSelfIfReady() {
|
||||
// code should not reach here. deleteSelfIfReady is always called on an existing entry.
|
||||
}
|
||||
|
||||
func (*dummyEntry) getParentID() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// ChannelMetric defines the info channelz provides for a specific Channel, which
|
||||
// includes ChannelInternalMetric and channelz-specific data, such as channelz id,
|
||||
// child list, etc.
|
||||
type ChannelMetric struct {
|
||||
// ID is the channelz id of this channel.
|
||||
ID int64
|
||||
// RefName is the human readable reference string of this channel.
|
||||
RefName string
|
||||
// ChannelData contains channel internal metric reported by the channel through
|
||||
// ChannelzMetric().
|
||||
ChannelData *ChannelInternalMetric
|
||||
// NestedChans tracks the nested channel type children of this channel in the format of
|
||||
// a map from nested channel channelz id to corresponding reference string.
|
||||
NestedChans map[int64]string
|
||||
// SubChans tracks the subchannel type children of this channel in the format of a
|
||||
// map from subchannel channelz id to corresponding reference string.
|
||||
SubChans map[int64]string
|
||||
// Sockets tracks the socket type children of this channel in the format of a map
|
||||
// from socket channelz id to corresponding reference string.
|
||||
// Note current grpc implementation doesn't allow channel having sockets directly,
|
||||
// therefore, this is field is unused.
|
||||
Sockets map[int64]string
|
||||
// Trace contains the most recent traced events.
|
||||
Trace *ChannelTrace
|
||||
}
|
||||
|
||||
// SubChannelMetric defines the info channelz provides for a specific SubChannel,
|
||||
// which includes ChannelInternalMetric and channelz-specific data, such as
|
||||
// channelz id, child list, etc.
|
||||
type SubChannelMetric struct {
|
||||
// ID is the channelz id of this subchannel.
|
||||
ID int64
|
||||
// RefName is the human readable reference string of this subchannel.
|
||||
RefName string
|
||||
// ChannelData contains subchannel internal metric reported by the subchannel
|
||||
// through ChannelzMetric().
|
||||
ChannelData *ChannelInternalMetric
|
||||
// NestedChans tracks the nested channel type children of this subchannel in the format of
|
||||
// a map from nested channel channelz id to corresponding reference string.
|
||||
// Note current grpc implementation doesn't allow subchannel to have nested channels
|
||||
// as children, therefore, this field is unused.
|
||||
NestedChans map[int64]string
|
||||
// SubChans tracks the subchannel type children of this subchannel in the format of a
|
||||
// map from subchannel channelz id to corresponding reference string.
|
||||
// Note current grpc implementation doesn't allow subchannel to have subchannels
|
||||
// as children, therefore, this field is unused.
|
||||
SubChans map[int64]string
|
||||
// Sockets tracks the socket type children of this subchannel in the format of a map
|
||||
// from socket channelz id to corresponding reference string.
|
||||
Sockets map[int64]string
|
||||
// Trace contains the most recent traced events.
|
||||
Trace *ChannelTrace
|
||||
}
|
||||
|
||||
// ChannelInternalMetric defines the struct that the implementor of Channel interface
|
||||
// should return from ChannelzMetric().
|
||||
type ChannelInternalMetric struct {
|
||||
// current connectivity state of the channel.
|
||||
State connectivity.State
|
||||
// The target this channel originally tried to connect to. May be absent
|
||||
Target string
|
||||
// The number of calls started on the channel.
|
||||
CallsStarted int64
|
||||
// The number of calls that have completed with an OK status.
|
||||
CallsSucceeded int64
|
||||
// The number of calls that have a completed with a non-OK status.
|
||||
CallsFailed int64
|
||||
// The last time a call was started on the channel.
|
||||
LastCallStartedTimestamp time.Time
|
||||
}
|
||||
|
||||
// ChannelTrace stores traced events on a channel/subchannel and related info.
|
||||
type ChannelTrace struct {
|
||||
// EventNum is the number of events that ever got traced (i.e. including those that have been deleted)
|
||||
EventNum int64
|
||||
// CreationTime is the creation time of the trace.
|
||||
CreationTime time.Time
|
||||
// Events stores the most recent trace events (up to $maxTraceEntry, newer event will overwrite the
|
||||
// oldest one)
|
||||
Events []*TraceEvent
|
||||
}
|
||||
|
||||
// TraceEvent represent a single trace event
|
||||
type TraceEvent struct {
|
||||
// Desc is a simple description of the trace event.
|
||||
Desc string
|
||||
// Severity states the severity of this trace event.
|
||||
Severity Severity
|
||||
// Timestamp is the event time.
|
||||
Timestamp time.Time
|
||||
// RefID is the id of the entity that gets referenced in the event. RefID is 0 if no other entity is
|
||||
// involved in this event.
|
||||
// e.g. SubChannel (id: 4[]) Created. --> RefID = 4, RefName = "" (inside [])
|
||||
RefID int64
|
||||
// RefName is the reference name for the entity that gets referenced in the event.
|
||||
RefName string
|
||||
// RefType indicates the referenced entity type, i.e Channel or SubChannel.
|
||||
RefType RefChannelType
|
||||
}
|
||||
|
||||
// Channel is the interface that should be satisfied in order to be tracked by
|
||||
// channelz as Channel or SubChannel.
|
||||
type Channel interface {
|
||||
ChannelzMetric() *ChannelInternalMetric
|
||||
}
|
||||
|
||||
type dummyChannel struct{}
|
||||
|
||||
func (d *dummyChannel) ChannelzMetric() *ChannelInternalMetric {
|
||||
return &ChannelInternalMetric{}
|
||||
}
|
||||
|
||||
type channel struct {
|
||||
refName string
|
||||
c Channel
|
||||
closeCalled bool
|
||||
nestedChans map[int64]string
|
||||
subChans map[int64]string
|
||||
id int64
|
||||
pid int64
|
||||
cm *channelMap
|
||||
trace *channelTrace
|
||||
// traceRefCount is the number of trace events that reference this channel.
|
||||
// Non-zero traceRefCount means the trace of this channel cannot be deleted.
|
||||
traceRefCount int32
|
||||
}
|
||||
|
||||
func (c *channel) addChild(id int64, e entry) {
|
||||
switch v := e.(type) {
|
||||
case *subChannel:
|
||||
c.subChans[id] = v.refName
|
||||
case *channel:
|
||||
c.nestedChans[id] = v.refName
|
||||
default:
|
||||
logger.Errorf("cannot add a child (id = %d) of type %T to a channel", id, e)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *channel) deleteChild(id int64) {
|
||||
delete(c.subChans, id)
|
||||
delete(c.nestedChans, id)
|
||||
c.deleteSelfIfReady()
|
||||
}
|
||||
|
||||
func (c *channel) triggerDelete() {
|
||||
c.closeCalled = true
|
||||
c.deleteSelfIfReady()
|
||||
}
|
||||
|
||||
func (c *channel) getParentID() int64 {
|
||||
return c.pid
|
||||
}
|
||||
|
||||
// deleteSelfFromTree tries to delete the channel from the channelz entry relation tree, which means
|
||||
// deleting the channel reference from its parent's child list.
|
||||
//
|
||||
// In order for a channel to be deleted from the tree, it must meet the criteria that, removal of the
|
||||
// corresponding grpc object has been invoked, and the channel does not have any children left.
|
||||
//
|
||||
// The returned boolean value indicates whether the channel has been successfully deleted from tree.
|
||||
func (c *channel) deleteSelfFromTree() (deleted bool) {
|
||||
if !c.closeCalled || len(c.subChans)+len(c.nestedChans) != 0 {
|
||||
return false
|
||||
}
|
||||
// not top channel
|
||||
if c.pid != 0 {
|
||||
c.cm.findEntry(c.pid).deleteChild(c.id)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// deleteSelfFromMap checks whether it is valid to delete the channel from the map, which means
|
||||
// deleting the channel from channelz's tracking entirely. Users can no longer use id to query the
|
||||
// channel, and its memory will be garbage collected.
|
||||
//
|
||||
// The trace reference count of the channel must be 0 in order to be deleted from the map. This is
|
||||
// specified in the channel tracing gRFC that as long as some other trace has reference to an entity,
|
||||
// the trace of the referenced entity must not be deleted. In order to release the resource allocated
|
||||
// by grpc, the reference to the grpc object is reset to a dummy object.
|
||||
//
|
||||
// deleteSelfFromMap must be called after deleteSelfFromTree returns true.
|
||||
//
|
||||
// It returns a bool to indicate whether the channel can be safely deleted from map.
|
||||
func (c *channel) deleteSelfFromMap() (delete bool) {
|
||||
if c.getTraceRefCount() != 0 {
|
||||
c.c = &dummyChannel{}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// deleteSelfIfReady tries to delete the channel itself from the channelz database.
|
||||
// The delete process includes two steps:
|
||||
// 1. delete the channel from the entry relation tree, i.e. delete the channel reference from its
|
||||
// parent's child list.
|
||||
// 2. delete the channel from the map, i.e. delete the channel entirely from channelz. Lookup by id
|
||||
// will return entry not found error.
|
||||
func (c *channel) deleteSelfIfReady() {
|
||||
if !c.deleteSelfFromTree() {
|
||||
return
|
||||
}
|
||||
if !c.deleteSelfFromMap() {
|
||||
return
|
||||
}
|
||||
c.cm.deleteEntry(c.id)
|
||||
c.trace.clear()
|
||||
}
|
||||
|
||||
func (c *channel) getChannelTrace() *channelTrace {
|
||||
return c.trace
|
||||
}
|
||||
|
||||
func (c *channel) incrTraceRefCount() {
|
||||
atomic.AddInt32(&c.traceRefCount, 1)
|
||||
}
|
||||
|
||||
func (c *channel) decrTraceRefCount() {
|
||||
atomic.AddInt32(&c.traceRefCount, -1)
|
||||
}
|
||||
|
||||
func (c *channel) getTraceRefCount() int {
|
||||
i := atomic.LoadInt32(&c.traceRefCount)
|
||||
return int(i)
|
||||
}
|
||||
|
||||
func (c *channel) getRefName() string {
|
||||
return c.refName
|
||||
}
|
||||
|
||||
type subChannel struct {
|
||||
refName string
|
||||
c Channel
|
||||
closeCalled bool
|
||||
sockets map[int64]string
|
||||
id int64
|
||||
pid int64
|
||||
cm *channelMap
|
||||
trace *channelTrace
|
||||
traceRefCount int32
|
||||
}
|
||||
|
||||
func (sc *subChannel) addChild(id int64, e entry) {
|
||||
if v, ok := e.(*normalSocket); ok {
|
||||
sc.sockets[id] = v.refName
|
||||
} else {
|
||||
logger.Errorf("cannot add a child (id = %d) of type %T to a subChannel", id, e)
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *subChannel) deleteChild(id int64) {
|
||||
delete(sc.sockets, id)
|
||||
sc.deleteSelfIfReady()
|
||||
}
|
||||
|
||||
func (sc *subChannel) triggerDelete() {
|
||||
sc.closeCalled = true
|
||||
sc.deleteSelfIfReady()
|
||||
}
|
||||
|
||||
func (sc *subChannel) getParentID() int64 {
|
||||
return sc.pid
|
||||
}
|
||||
|
||||
// deleteSelfFromTree tries to delete the subchannel from the channelz entry relation tree, which
|
||||
// means deleting the subchannel reference from its parent's child list.
|
||||
//
|
||||
// In order for a subchannel to be deleted from the tree, it must meet the criteria that, removal of
|
||||
// the corresponding grpc object has been invoked, and the subchannel does not have any children left.
|
||||
//
|
||||
// The returned boolean value indicates whether the channel has been successfully deleted from tree.
|
||||
func (sc *subChannel) deleteSelfFromTree() (deleted bool) {
|
||||
if !sc.closeCalled || len(sc.sockets) != 0 {
|
||||
return false
|
||||
}
|
||||
sc.cm.findEntry(sc.pid).deleteChild(sc.id)
|
||||
return true
|
||||
}
|
||||
|
||||
// deleteSelfFromMap checks whether it is valid to delete the subchannel from the map, which means
|
||||
// deleting the subchannel from channelz's tracking entirely. Users can no longer use id to query
|
||||
// the subchannel, and its memory will be garbage collected.
|
||||
//
|
||||
// The trace reference count of the subchannel must be 0 in order to be deleted from the map. This is
|
||||
// specified in the channel tracing gRFC that as long as some other trace has reference to an entity,
|
||||
// the trace of the referenced entity must not be deleted. In order to release the resource allocated
|
||||
// by grpc, the reference to the grpc object is reset to a dummy object.
|
||||
//
|
||||
// deleteSelfFromMap must be called after deleteSelfFromTree returns true.
|
||||
//
|
||||
// It returns a bool to indicate whether the channel can be safely deleted from map.
|
||||
func (sc *subChannel) deleteSelfFromMap() (delete bool) {
|
||||
if sc.getTraceRefCount() != 0 {
|
||||
// free the grpc struct (i.e. addrConn)
|
||||
sc.c = &dummyChannel{}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// deleteSelfIfReady tries to delete the subchannel itself from the channelz database.
|
||||
// The delete process includes two steps:
|
||||
// 1. delete the subchannel from the entry relation tree, i.e. delete the subchannel reference from
|
||||
// its parent's child list.
|
||||
// 2. delete the subchannel from the map, i.e. delete the subchannel entirely from channelz. Lookup
|
||||
// by id will return entry not found error.
|
||||
func (sc *subChannel) deleteSelfIfReady() {
|
||||
if !sc.deleteSelfFromTree() {
|
||||
return
|
||||
}
|
||||
if !sc.deleteSelfFromMap() {
|
||||
return
|
||||
}
|
||||
sc.cm.deleteEntry(sc.id)
|
||||
sc.trace.clear()
|
||||
}
|
||||
|
||||
func (sc *subChannel) getChannelTrace() *channelTrace {
|
||||
return sc.trace
|
||||
}
|
||||
|
||||
func (sc *subChannel) incrTraceRefCount() {
|
||||
atomic.AddInt32(&sc.traceRefCount, 1)
|
||||
}
|
||||
|
||||
func (sc *subChannel) decrTraceRefCount() {
|
||||
atomic.AddInt32(&sc.traceRefCount, -1)
|
||||
}
|
||||
|
||||
func (sc *subChannel) getTraceRefCount() int {
|
||||
i := atomic.LoadInt32(&sc.traceRefCount)
|
||||
return int(i)
|
||||
}
|
||||
|
||||
func (sc *subChannel) getRefName() string {
|
||||
return sc.refName
|
||||
}
|
||||
|
||||
// SocketMetric defines the info channelz provides for a specific Socket, which
|
||||
// includes SocketInternalMetric and channelz-specific data, such as channelz id, etc.
|
||||
type SocketMetric struct {
|
||||
// ID is the channelz id of this socket.
|
||||
ID int64
|
||||
// RefName is the human readable reference string of this socket.
|
||||
RefName string
|
||||
// SocketData contains socket internal metric reported by the socket through
|
||||
// ChannelzMetric().
|
||||
SocketData *SocketInternalMetric
|
||||
}
|
||||
|
||||
// SocketInternalMetric defines the struct that the implementor of Socket interface
|
||||
// should return from ChannelzMetric().
|
||||
type SocketInternalMetric struct {
|
||||
// The number of streams that have been started.
|
||||
StreamsStarted int64
|
||||
// The number of streams that have ended successfully:
|
||||
// On client side, receiving frame with eos bit set.
|
||||
// On server side, sending frame with eos bit set.
|
||||
StreamsSucceeded int64
|
||||
// The number of streams that have ended unsuccessfully:
|
||||
// On client side, termination without receiving frame with eos bit set.
|
||||
// On server side, termination without sending frame with eos bit set.
|
||||
StreamsFailed int64
|
||||
// The number of messages successfully sent on this socket.
|
||||
MessagesSent int64
|
||||
MessagesReceived int64
|
||||
// The number of keep alives sent. This is typically implemented with HTTP/2
|
||||
// ping messages.
|
||||
KeepAlivesSent int64
|
||||
// The last time a stream was created by this endpoint. Usually unset for
|
||||
// servers.
|
||||
LastLocalStreamCreatedTimestamp time.Time
|
||||
// The last time a stream was created by the remote endpoint. Usually unset
|
||||
// for clients.
|
||||
LastRemoteStreamCreatedTimestamp time.Time
|
||||
// The last time a message was sent by this endpoint.
|
||||
LastMessageSentTimestamp time.Time
|
||||
// The last time a message was received by this endpoint.
|
||||
LastMessageReceivedTimestamp time.Time
|
||||
// The amount of window, granted to the local endpoint by the remote endpoint.
|
||||
// This may be slightly out of date due to network latency. This does NOT
|
||||
// include stream level or TCP level flow control info.
|
||||
LocalFlowControlWindow int64
|
||||
// The amount of window, granted to the remote endpoint by the local endpoint.
|
||||
// This may be slightly out of date due to network latency. This does NOT
|
||||
// include stream level or TCP level flow control info.
|
||||
RemoteFlowControlWindow int64
|
||||
// The locally bound address.
|
||||
LocalAddr net.Addr
|
||||
// The remote bound address. May be absent.
|
||||
RemoteAddr net.Addr
|
||||
// Optional, represents the name of the remote endpoint, if different than
|
||||
// the original target name.
|
||||
RemoteName string
|
||||
SocketOptions *SocketOptionData
|
||||
Security credentials.ChannelzSecurityValue
|
||||
}
|
||||
|
||||
// Socket is the interface that should be satisfied in order to be tracked by
|
||||
// channelz as Socket.
|
||||
type Socket interface {
|
||||
ChannelzMetric() *SocketInternalMetric
|
||||
}
|
||||
|
||||
type listenSocket struct {
|
||||
refName string
|
||||
s Socket
|
||||
id int64
|
||||
pid int64
|
||||
cm *channelMap
|
||||
}
|
||||
|
||||
func (ls *listenSocket) addChild(id int64, e entry) {
|
||||
logger.Errorf("cannot add a child (id = %d) of type %T to a listen socket", id, e)
|
||||
}
|
||||
|
||||
func (ls *listenSocket) deleteChild(id int64) {
|
||||
logger.Errorf("cannot delete a child (id = %d) from a listen socket", id)
|
||||
}
|
||||
|
||||
func (ls *listenSocket) triggerDelete() {
|
||||
ls.cm.deleteEntry(ls.id)
|
||||
ls.cm.findEntry(ls.pid).deleteChild(ls.id)
|
||||
}
|
||||
|
||||
func (ls *listenSocket) deleteSelfIfReady() {
|
||||
logger.Errorf("cannot call deleteSelfIfReady on a listen socket")
|
||||
}
|
||||
|
||||
func (ls *listenSocket) getParentID() int64 {
|
||||
return ls.pid
|
||||
}
|
||||
|
||||
type normalSocket struct {
|
||||
refName string
|
||||
s Socket
|
||||
id int64
|
||||
pid int64
|
||||
cm *channelMap
|
||||
}
|
||||
|
||||
func (ns *normalSocket) addChild(id int64, e entry) {
|
||||
logger.Errorf("cannot add a child (id = %d) of type %T to a normal socket", id, e)
|
||||
}
|
||||
|
||||
func (ns *normalSocket) deleteChild(id int64) {
|
||||
logger.Errorf("cannot delete a child (id = %d) from a normal socket", id)
|
||||
}
|
||||
|
||||
func (ns *normalSocket) triggerDelete() {
|
||||
ns.cm.deleteEntry(ns.id)
|
||||
ns.cm.findEntry(ns.pid).deleteChild(ns.id)
|
||||
}
|
||||
|
||||
func (ns *normalSocket) deleteSelfIfReady() {
|
||||
logger.Errorf("cannot call deleteSelfIfReady on a normal socket")
|
||||
}
|
||||
|
||||
func (ns *normalSocket) getParentID() int64 {
|
||||
return ns.pid
|
||||
}
|
||||
|
||||
// ServerMetric defines the info channelz provides for a specific Server, which
|
||||
// includes ServerInternalMetric and channelz-specific data, such as channelz id,
|
||||
// child list, etc.
|
||||
type ServerMetric struct {
|
||||
// ID is the channelz id of this server.
|
||||
ID int64
|
||||
// RefName is the human readable reference string of this server.
|
||||
RefName string
|
||||
// ServerData contains server internal metric reported by the server through
|
||||
// ChannelzMetric().
|
||||
ServerData *ServerInternalMetric
|
||||
// ListenSockets tracks the listener socket type children of this server in the
|
||||
// format of a map from socket channelz id to corresponding reference string.
|
||||
ListenSockets map[int64]string
|
||||
}
|
||||
|
||||
// ServerInternalMetric defines the struct that the implementor of Server interface
|
||||
// should return from ChannelzMetric().
|
||||
type ServerInternalMetric struct {
|
||||
// The number of incoming calls started on the server.
|
||||
CallsStarted int64
|
||||
// The number of incoming calls that have completed with an OK status.
|
||||
CallsSucceeded int64
|
||||
// The number of incoming calls that have a completed with a non-OK status.
|
||||
CallsFailed int64
|
||||
// The last time a call was started on the server.
|
||||
LastCallStartedTimestamp time.Time
|
||||
}
|
||||
|
||||
// Server is the interface to be satisfied in order to be tracked by channelz as
|
||||
// Server.
|
||||
type Server interface {
|
||||
ChannelzMetric() *ServerInternalMetric
|
||||
}
|
||||
|
||||
type server struct {
|
||||
refName string
|
||||
s Server
|
||||
closeCalled bool
|
||||
sockets map[int64]string
|
||||
listenSockets map[int64]string
|
||||
id int64
|
||||
cm *channelMap
|
||||
}
|
||||
|
||||
func (s *server) addChild(id int64, e entry) {
|
||||
switch v := e.(type) {
|
||||
case *normalSocket:
|
||||
s.sockets[id] = v.refName
|
||||
case *listenSocket:
|
||||
s.listenSockets[id] = v.refName
|
||||
default:
|
||||
logger.Errorf("cannot add a child (id = %d) of type %T to a server", id, e)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) deleteChild(id int64) {
|
||||
delete(s.sockets, id)
|
||||
delete(s.listenSockets, id)
|
||||
s.deleteSelfIfReady()
|
||||
}
|
||||
|
||||
func (s *server) triggerDelete() {
|
||||
s.closeCalled = true
|
||||
s.deleteSelfIfReady()
|
||||
}
|
||||
|
||||
func (s *server) deleteSelfIfReady() {
|
||||
if !s.closeCalled || len(s.sockets)+len(s.listenSockets) != 0 {
|
||||
return
|
||||
}
|
||||
s.cm.deleteEntry(s.id)
|
||||
}
|
||||
|
||||
func (s *server) getParentID() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
type tracedChannel interface {
|
||||
getChannelTrace() *channelTrace
|
||||
incrTraceRefCount()
|
||||
decrTraceRefCount()
|
||||
getRefName() string
|
||||
}
|
||||
|
||||
type channelTrace struct {
|
||||
cm *channelMap
|
||||
clearCalled bool
|
||||
createdTime time.Time
|
||||
eventCount int64
|
||||
mu sync.Mutex
|
||||
events []*TraceEvent
|
||||
}
|
||||
|
||||
func (c *channelTrace) append(e *TraceEvent) {
|
||||
c.mu.Lock()
|
||||
if len(c.events) == getMaxTraceEntry() {
|
||||
del := c.events[0]
|
||||
c.events = c.events[1:]
|
||||
if del.RefID != 0 {
|
||||
// start recursive cleanup in a goroutine to not block the call originated from grpc.
|
||||
go func() {
|
||||
// need to acquire c.cm.mu lock to call the unlocked attemptCleanup func.
|
||||
c.cm.mu.Lock()
|
||||
c.cm.decrTraceRefCount(del.RefID)
|
||||
c.cm.mu.Unlock()
|
||||
}()
|
||||
}
|
||||
}
|
||||
e.Timestamp = time.Now()
|
||||
c.events = append(c.events, e)
|
||||
c.eventCount++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *channelTrace) clear() {
|
||||
if c.clearCalled {
|
||||
return
|
||||
}
|
||||
c.clearCalled = true
|
||||
c.mu.Lock()
|
||||
for _, e := range c.events {
|
||||
if e.RefID != 0 {
|
||||
// caller should have already held the c.cm.mu lock.
|
||||
c.cm.decrTraceRefCount(e.RefID)
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Severity is the severity level of a trace event.
|
||||
// The canonical enumeration of all valid values is here:
|
||||
// https://github.com/grpc/grpc-proto/blob/9b13d199cc0d4703c7ea26c9c330ba695866eb23/grpc/channelz/v1/channelz.proto#L126.
|
||||
type Severity int
|
||||
|
||||
const (
|
||||
// CtUnknown indicates unknown severity of a trace event.
|
||||
CtUnknown Severity = iota
|
||||
// CtInfo indicates info level severity of a trace event.
|
||||
CtInfo
|
||||
// CtWarning indicates warning level severity of a trace event.
|
||||
CtWarning
|
||||
// CtError indicates error level severity of a trace event.
|
||||
CtError
|
||||
)
|
||||
|
||||
// RefChannelType is the type of the entity being referenced in a trace event.
|
||||
type RefChannelType int
|
||||
|
||||
const (
|
||||
// RefUnknown indicates an unknown entity type, the zero value for this type.
|
||||
RefUnknown RefChannelType = iota
|
||||
// RefChannel indicates the referenced entity is a Channel.
|
||||
RefChannel
|
||||
// RefSubChannel indicates the referenced entity is a SubChannel.
|
||||
RefSubChannel
|
||||
// RefServer indicates the referenced entity is a Server.
|
||||
RefServer
|
||||
// RefListenSocket indicates the referenced entity is a ListenSocket.
|
||||
RefListenSocket
|
||||
// RefNormalSocket indicates the referenced entity is a NormalSocket.
|
||||
RefNormalSocket
|
||||
)
|
||||
|
||||
var refChannelTypeToString = map[RefChannelType]string{
|
||||
RefUnknown: "Unknown",
|
||||
RefChannel: "Channel",
|
||||
RefSubChannel: "SubChannel",
|
||||
RefServer: "Server",
|
||||
RefListenSocket: "ListenSocket",
|
||||
RefNormalSocket: "NormalSocket",
|
||||
}
|
||||
|
||||
func (r RefChannelType) String() string {
|
||||
return refChannelTypeToString[r]
|
||||
}
|
||||
|
||||
func (c *channelTrace) dumpData() *ChannelTrace {
|
||||
c.mu.Lock()
|
||||
ct := &ChannelTrace{EventNum: c.eventCount, CreationTime: c.createdTime}
|
||||
ct.Events = c.events[:len(c.events)]
|
||||
c.mu.Unlock()
|
||||
return ct
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
*
|
||||
* Copyright 2018 gRPC authors.
|
||||
*
|
||||
* 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 channelz
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// GetSocketOption gets the socket option info of the conn.
|
||||
func GetSocketOption(socket any) *SocketOptionData {
|
||||
c, ok := socket.(syscall.Conn)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
data := &SocketOptionData{}
|
||||
if rawConn, err := c.SyscallConn(); err == nil {
|
||||
rawConn.Control(data.Getsockopt)
|
||||
return data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright 2018 gRPC authors.
|
||||
*
|
||||
* 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 channelz
|
||||
|
||||
// GetSocketOption gets the socket option info of the conn.
|
||||
func GetSocketOption(c any) *SocketOptionData {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -190,12 +190,16 @@ var (
|
|||
// function makes events more predictable than relying on timer events.
|
||||
TriggerXDSResourceNameNotFoundForTesting any // func(func(xdsresource.Type, string), string, string) error
|
||||
|
||||
// TriggerXDSResourceNotFoundClient invokes the testing xDS Client singleton
|
||||
// to invoke resource not found for a resource type name and resource name.
|
||||
// TriggerXDSResourceNameNotFoundClient invokes the testing xDS Client
|
||||
// singleton to invoke resource not found for a resource type name and
|
||||
// resource name.
|
||||
TriggerXDSResourceNameNotFoundClient any // func(string, string) error
|
||||
|
||||
// FromOutgoingContextRaw returns the un-merged, intermediary contents of metadata.rawMD.
|
||||
FromOutgoingContextRaw any // func(context.Context) (metadata.MD, [][]string, bool)
|
||||
|
||||
// UserSetDefaultScheme is set to true if the user has overridden the default resolver scheme.
|
||||
UserSetDefaultScheme bool = false
|
||||
)
|
||||
|
||||
// HealthChecker defines the signature of the client-side LB channel health checking function.
|
||||
|
|
|
|||
|
|
@ -24,9 +24,8 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
protov1 "github.com/golang/protobuf/proto"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
protov2 "google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/protoadapt"
|
||||
)
|
||||
|
||||
const jsonIndent = " "
|
||||
|
|
@ -35,21 +34,14 @@ const jsonIndent = " "
|
|||
//
|
||||
// If marshal fails, it falls back to fmt.Sprintf("%+v").
|
||||
func ToJSON(e any) string {
|
||||
switch ee := e.(type) {
|
||||
case protov1.Message:
|
||||
mm := protojson.MarshalOptions{Indent: jsonIndent}
|
||||
ret, err := mm.Marshal(protov1.MessageV2(ee))
|
||||
if err != nil {
|
||||
// This may fail for proto.Anys, e.g. for xDS v2, LDS, the v2
|
||||
// messages are not imported, and this will fail because the message
|
||||
// is not found.
|
||||
return fmt.Sprintf("%+v", ee)
|
||||
}
|
||||
return string(ret)
|
||||
case protov2.Message:
|
||||
if ee, ok := e.(protoadapt.MessageV1); ok {
|
||||
e = protoadapt.MessageV2Of(ee)
|
||||
}
|
||||
|
||||
if ee, ok := e.(protoadapt.MessageV2); ok {
|
||||
mm := protojson.MarshalOptions{
|
||||
Multiline: true,
|
||||
Indent: jsonIndent,
|
||||
Multiline: true,
|
||||
}
|
||||
ret, err := mm.Marshal(ee)
|
||||
if err != nil {
|
||||
|
|
@ -59,13 +51,13 @@ func ToJSON(e any) string {
|
|||
return fmt.Sprintf("%+v", ee)
|
||||
}
|
||||
return string(ret)
|
||||
default:
|
||||
ret, err := json.MarshalIndent(ee, "", jsonIndent)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%+v", ee)
|
||||
}
|
||||
return string(ret)
|
||||
}
|
||||
|
||||
ret, err := json.MarshalIndent(e, "", jsonIndent)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%+v", e)
|
||||
}
|
||||
return string(ret)
|
||||
}
|
||||
|
||||
// FormatJSON formats the input json bytes with indentation.
|
||||
|
|
|
|||
|
|
@ -45,6 +45,13 @@ import (
|
|||
// addresses from SRV records. Must not be changed after init time.
|
||||
var EnableSRVLookups = false
|
||||
|
||||
// ResolvingTimeout specifies the maximum duration for a DNS resolution request.
|
||||
// If the timeout expires before a response is received, the request will be canceled.
|
||||
//
|
||||
// It is recommended to set this value at application startup. Avoid modifying this variable
|
||||
// after initialization as it's not thread-safe for concurrent modification.
|
||||
var ResolvingTimeout = 30 * time.Second
|
||||
|
||||
var logger = grpclog.Component("dns")
|
||||
|
||||
func init() {
|
||||
|
|
@ -221,18 +228,18 @@ func (d *dnsResolver) watcher() {
|
|||
}
|
||||
}
|
||||
|
||||
func (d *dnsResolver) lookupSRV() ([]resolver.Address, error) {
|
||||
func (d *dnsResolver) lookupSRV(ctx context.Context) ([]resolver.Address, error) {
|
||||
if !EnableSRVLookups {
|
||||
return nil, nil
|
||||
}
|
||||
var newAddrs []resolver.Address
|
||||
_, srvs, err := d.resolver.LookupSRV(d.ctx, "grpclb", "tcp", d.host)
|
||||
_, srvs, err := d.resolver.LookupSRV(ctx, "grpclb", "tcp", d.host)
|
||||
if err != nil {
|
||||
err = handleDNSError(err, "SRV") // may become nil
|
||||
return nil, err
|
||||
}
|
||||
for _, s := range srvs {
|
||||
lbAddrs, err := d.resolver.LookupHost(d.ctx, s.Target)
|
||||
lbAddrs, err := d.resolver.LookupHost(ctx, s.Target)
|
||||
if err != nil {
|
||||
err = handleDNSError(err, "A") // may become nil
|
||||
if err == nil {
|
||||
|
|
@ -269,8 +276,8 @@ func handleDNSError(err error, lookupType string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (d *dnsResolver) lookupTXT() *serviceconfig.ParseResult {
|
||||
ss, err := d.resolver.LookupTXT(d.ctx, txtPrefix+d.host)
|
||||
func (d *dnsResolver) lookupTXT(ctx context.Context) *serviceconfig.ParseResult {
|
||||
ss, err := d.resolver.LookupTXT(ctx, txtPrefix+d.host)
|
||||
if err != nil {
|
||||
if envconfig.TXTErrIgnore {
|
||||
return nil
|
||||
|
|
@ -297,8 +304,8 @@ func (d *dnsResolver) lookupTXT() *serviceconfig.ParseResult {
|
|||
return d.cc.ParseServiceConfig(sc)
|
||||
}
|
||||
|
||||
func (d *dnsResolver) lookupHost() ([]resolver.Address, error) {
|
||||
addrs, err := d.resolver.LookupHost(d.ctx, d.host)
|
||||
func (d *dnsResolver) lookupHost(ctx context.Context) ([]resolver.Address, error) {
|
||||
addrs, err := d.resolver.LookupHost(ctx, d.host)
|
||||
if err != nil {
|
||||
err = handleDNSError(err, "A")
|
||||
return nil, err
|
||||
|
|
@ -316,8 +323,10 @@ func (d *dnsResolver) lookupHost() ([]resolver.Address, error) {
|
|||
}
|
||||
|
||||
func (d *dnsResolver) lookup() (*resolver.State, error) {
|
||||
srv, srvErr := d.lookupSRV()
|
||||
addrs, hostErr := d.lookupHost()
|
||||
ctx, cancel := context.WithTimeout(d.ctx, ResolvingTimeout)
|
||||
defer cancel()
|
||||
srv, srvErr := d.lookupSRV(ctx)
|
||||
addrs, hostErr := d.lookupHost(ctx)
|
||||
if hostErr != nil && (srvErr != nil || len(srv) == 0) {
|
||||
return nil, hostErr
|
||||
}
|
||||
|
|
@ -327,7 +336,7 @@ func (d *dnsResolver) lookup() (*resolver.State, error) {
|
|||
state = grpclbstate.Set(state, &grpclbstate.State{BalancerAddresses: srv})
|
||||
}
|
||||
if !d.disableServiceConfig {
|
||||
state.ServiceConfig = d.lookupTXT()
|
||||
state.ServiceConfig = d.lookupTXT(ctx)
|
||||
}
|
||||
return &state, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,14 +51,10 @@ import (
|
|||
// inside an http.Handler, or writes an HTTP error to w and returns an error.
|
||||
// It requires that the http Server supports HTTP/2.
|
||||
func NewServerHandlerTransport(w http.ResponseWriter, r *http.Request, stats []stats.Handler) (ServerTransport, error) {
|
||||
if r.ProtoMajor != 2 {
|
||||
msg := "gRPC requires HTTP/2"
|
||||
http.Error(w, msg, http.StatusBadRequest)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
msg := fmt.Sprintf("invalid gRPC request method %q", r.Method)
|
||||
http.Error(w, msg, http.StatusBadRequest)
|
||||
http.Error(w, msg, http.StatusMethodNotAllowed)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
|
|
@ -69,6 +65,11 @@ func NewServerHandlerTransport(w http.ResponseWriter, r *http.Request, stats []s
|
|||
http.Error(w, msg, http.StatusUnsupportedMediaType)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
if r.ProtoMajor != 2 {
|
||||
msg := "gRPC requires HTTP/2"
|
||||
http.Error(w, msg, http.StatusHTTPVersionNotSupported)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
if _, ok := w.(http.Flusher); !ok {
|
||||
msg := "gRPC requires a ResponseWriter supporting http.Flusher"
|
||||
http.Error(w, msg, http.StatusInternalServerError)
|
||||
|
|
|
|||
|
|
@ -140,9 +140,7 @@ type http2Client struct {
|
|||
// variable.
|
||||
kpDormant bool
|
||||
|
||||
// Fields below are for channelz metric collection.
|
||||
channelzID *channelz.Identifier
|
||||
czData *channelzData
|
||||
channelz *channelz.Socket
|
||||
|
||||
onClose func(GoAwayReason)
|
||||
|
||||
|
|
@ -319,6 +317,7 @@ func newHTTP2Client(connectCtx, ctx context.Context, addr resolver.Address, opts
|
|||
if opts.MaxHeaderListSize != nil {
|
||||
maxHeaderListSize = *opts.MaxHeaderListSize
|
||||
}
|
||||
|
||||
t := &http2Client{
|
||||
ctx: ctx,
|
||||
ctxDone: ctx.Done(), // Cache Done chan.
|
||||
|
|
@ -346,11 +345,25 @@ func newHTTP2Client(connectCtx, ctx context.Context, addr resolver.Address, opts
|
|||
maxConcurrentStreams: defaultMaxStreamsClient,
|
||||
streamQuota: defaultMaxStreamsClient,
|
||||
streamsQuotaAvailable: make(chan struct{}, 1),
|
||||
czData: new(channelzData),
|
||||
keepaliveEnabled: keepaliveEnabled,
|
||||
bufferPool: newBufferPool(),
|
||||
onClose: onClose,
|
||||
}
|
||||
var czSecurity credentials.ChannelzSecurityValue
|
||||
if au, ok := authInfo.(credentials.ChannelzSecurityInfo); ok {
|
||||
czSecurity = au.GetSecurityValue()
|
||||
}
|
||||
t.channelz = channelz.RegisterSocket(
|
||||
&channelz.Socket{
|
||||
SocketType: channelz.SocketTypeNormal,
|
||||
Parent: opts.ChannelzParent,
|
||||
SocketMetrics: channelz.SocketMetrics{},
|
||||
EphemeralMetrics: t.socketMetrics,
|
||||
LocalAddr: t.localAddr,
|
||||
RemoteAddr: t.remoteAddr,
|
||||
SocketOptions: channelz.GetSocketOption(t.conn),
|
||||
Security: czSecurity,
|
||||
})
|
||||
t.logger = prefixLoggerForClientTransport(t)
|
||||
// Add peer information to the http2client context.
|
||||
t.ctx = peer.NewContext(t.ctx, t.getPeer())
|
||||
|
|
@ -381,10 +394,6 @@ func newHTTP2Client(connectCtx, ctx context.Context, addr resolver.Address, opts
|
|||
}
|
||||
sh.HandleConn(t.ctx, connBegin)
|
||||
}
|
||||
t.channelzID, err = channelz.RegisterNormalSocket(t, opts.ChannelzParentID, fmt.Sprintf("%s -> %s", t.localAddr, t.remoteAddr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.keepaliveEnabled {
|
||||
t.kpDormancyCond = sync.NewCond(&t.mu)
|
||||
go t.keepalive()
|
||||
|
|
@ -756,8 +765,8 @@ func (t *http2Client) NewStream(ctx context.Context, callHdr *CallHdr) (*Stream,
|
|||
return ErrConnClosing
|
||||
}
|
||||
if channelz.IsOn() {
|
||||
atomic.AddInt64(&t.czData.streamsStarted, 1)
|
||||
atomic.StoreInt64(&t.czData.lastStreamCreatedTime, time.Now().UnixNano())
|
||||
t.channelz.SocketMetrics.StreamsStarted.Add(1)
|
||||
t.channelz.SocketMetrics.LastLocalStreamCreatedTimestamp.Store(time.Now().UnixNano())
|
||||
}
|
||||
// If the keepalive goroutine has gone dormant, wake it up.
|
||||
if t.kpDormant {
|
||||
|
|
@ -928,9 +937,9 @@ func (t *http2Client) closeStream(s *Stream, err error, rst bool, rstCode http2.
|
|||
t.mu.Unlock()
|
||||
if channelz.IsOn() {
|
||||
if eosReceived {
|
||||
atomic.AddInt64(&t.czData.streamsSucceeded, 1)
|
||||
t.channelz.SocketMetrics.StreamsSucceeded.Add(1)
|
||||
} else {
|
||||
atomic.AddInt64(&t.czData.streamsFailed, 1)
|
||||
t.channelz.SocketMetrics.StreamsFailed.Add(1)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -985,7 +994,7 @@ func (t *http2Client) Close(err error) {
|
|||
t.controlBuf.finish()
|
||||
t.cancel()
|
||||
t.conn.Close()
|
||||
channelz.RemoveEntry(t.channelzID)
|
||||
channelz.RemoveEntry(t.channelz.ID)
|
||||
// Append info about previous goaways if there were any, since this may be important
|
||||
// for understanding the root cause for this connection to be closed.
|
||||
_, goAwayDebugMessage := t.GetGoAwayReason()
|
||||
|
|
@ -1708,7 +1717,7 @@ func (t *http2Client) keepalive() {
|
|||
// keepalive timer expired. In both cases, we need to send a ping.
|
||||
if !outstandingPing {
|
||||
if channelz.IsOn() {
|
||||
atomic.AddInt64(&t.czData.kpCount, 1)
|
||||
t.channelz.SocketMetrics.KeepAlivesSent.Add(1)
|
||||
}
|
||||
t.controlBuf.put(p)
|
||||
timeoutLeft = t.kp.Timeout
|
||||
|
|
@ -1738,40 +1747,23 @@ func (t *http2Client) GoAway() <-chan struct{} {
|
|||
return t.goAway
|
||||
}
|
||||
|
||||
func (t *http2Client) ChannelzMetric() *channelz.SocketInternalMetric {
|
||||
s := channelz.SocketInternalMetric{
|
||||
StreamsStarted: atomic.LoadInt64(&t.czData.streamsStarted),
|
||||
StreamsSucceeded: atomic.LoadInt64(&t.czData.streamsSucceeded),
|
||||
StreamsFailed: atomic.LoadInt64(&t.czData.streamsFailed),
|
||||
MessagesSent: atomic.LoadInt64(&t.czData.msgSent),
|
||||
MessagesReceived: atomic.LoadInt64(&t.czData.msgRecv),
|
||||
KeepAlivesSent: atomic.LoadInt64(&t.czData.kpCount),
|
||||
LastLocalStreamCreatedTimestamp: time.Unix(0, atomic.LoadInt64(&t.czData.lastStreamCreatedTime)),
|
||||
LastMessageSentTimestamp: time.Unix(0, atomic.LoadInt64(&t.czData.lastMsgSentTime)),
|
||||
LastMessageReceivedTimestamp: time.Unix(0, atomic.LoadInt64(&t.czData.lastMsgRecvTime)),
|
||||
LocalFlowControlWindow: int64(t.fc.getSize()),
|
||||
SocketOptions: channelz.GetSocketOption(t.conn),
|
||||
LocalAddr: t.localAddr,
|
||||
RemoteAddr: t.remoteAddr,
|
||||
// RemoteName :
|
||||
func (t *http2Client) socketMetrics() *channelz.EphemeralSocketMetrics {
|
||||
return &channelz.EphemeralSocketMetrics{
|
||||
LocalFlowControlWindow: int64(t.fc.getSize()),
|
||||
RemoteFlowControlWindow: t.getOutFlowWindow(),
|
||||
}
|
||||
if au, ok := t.authInfo.(credentials.ChannelzSecurityInfo); ok {
|
||||
s.Security = au.GetSecurityValue()
|
||||
}
|
||||
s.RemoteFlowControlWindow = t.getOutFlowWindow()
|
||||
return &s
|
||||
}
|
||||
|
||||
func (t *http2Client) RemoteAddr() net.Addr { return t.remoteAddr }
|
||||
|
||||
func (t *http2Client) IncrMsgSent() {
|
||||
atomic.AddInt64(&t.czData.msgSent, 1)
|
||||
atomic.StoreInt64(&t.czData.lastMsgSentTime, time.Now().UnixNano())
|
||||
t.channelz.SocketMetrics.MessagesSent.Add(1)
|
||||
t.channelz.SocketMetrics.LastMessageSentTimestamp.Store(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (t *http2Client) IncrMsgRecv() {
|
||||
atomic.AddInt64(&t.czData.msgRecv, 1)
|
||||
atomic.StoreInt64(&t.czData.lastMsgRecvTime, time.Now().UnixNano())
|
||||
t.channelz.SocketMetrics.MessagesReceived.Add(1)
|
||||
t.channelz.SocketMetrics.LastMessageReceivedTimestamp.Store(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (t *http2Client) getOutFlowWindow() int64 {
|
||||
|
|
|
|||
|
|
@ -118,8 +118,7 @@ type http2Server struct {
|
|||
idle time.Time
|
||||
|
||||
// Fields below are for channelz metric collection.
|
||||
channelzID *channelz.Identifier
|
||||
czData *channelzData
|
||||
channelz *channelz.Socket
|
||||
bufferPool *bufferPool
|
||||
|
||||
connectionID uint64
|
||||
|
|
@ -262,9 +261,24 @@ func NewServerTransport(conn net.Conn, config *ServerConfig) (_ ServerTransport,
|
|||
idle: time.Now(),
|
||||
kep: kep,
|
||||
initialWindowSize: iwz,
|
||||
czData: new(channelzData),
|
||||
bufferPool: newBufferPool(),
|
||||
}
|
||||
var czSecurity credentials.ChannelzSecurityValue
|
||||
if au, ok := authInfo.(credentials.ChannelzSecurityInfo); ok {
|
||||
czSecurity = au.GetSecurityValue()
|
||||
}
|
||||
t.channelz = channelz.RegisterSocket(
|
||||
&channelz.Socket{
|
||||
SocketType: channelz.SocketTypeNormal,
|
||||
Parent: config.ChannelzParent,
|
||||
SocketMetrics: channelz.SocketMetrics{},
|
||||
EphemeralMetrics: t.socketMetrics,
|
||||
LocalAddr: t.peer.LocalAddr,
|
||||
RemoteAddr: t.peer.Addr,
|
||||
SocketOptions: channelz.GetSocketOption(t.conn),
|
||||
Security: czSecurity,
|
||||
},
|
||||
)
|
||||
t.logger = prefixLoggerForServerTransport(t)
|
||||
|
||||
t.controlBuf = newControlBuffer(t.done)
|
||||
|
|
@ -274,10 +288,6 @@ func NewServerTransport(conn net.Conn, config *ServerConfig) (_ ServerTransport,
|
|||
updateFlowControl: t.updateFlowControl,
|
||||
}
|
||||
}
|
||||
t.channelzID, err = channelz.RegisterNormalSocket(t, config.ChannelzParentID, fmt.Sprintf("%s -> %s", t.peer.Addr, t.peer.LocalAddr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.connectionID = atomic.AddUint64(&serverConnectionCounter, 1)
|
||||
t.framer.writer.Flush()
|
||||
|
|
@ -334,9 +344,11 @@ func NewServerTransport(conn net.Conn, config *ServerConfig) (_ ServerTransport,
|
|||
// closed, would lead to a TCP RST instead of FIN, and the client
|
||||
// encountering errors. For more info:
|
||||
// https://github.com/grpc/grpc-go/issues/5358
|
||||
timer := time.NewTimer(time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-t.readerDone:
|
||||
case <-time.After(time.Second):
|
||||
case <-timer.C:
|
||||
}
|
||||
t.conn.Close()
|
||||
}
|
||||
|
|
@ -592,8 +604,8 @@ func (t *http2Server) operateHeaders(ctx context.Context, frame *http2.MetaHeade
|
|||
}
|
||||
t.mu.Unlock()
|
||||
if channelz.IsOn() {
|
||||
atomic.AddInt64(&t.czData.streamsStarted, 1)
|
||||
atomic.StoreInt64(&t.czData.lastStreamCreatedTime, time.Now().UnixNano())
|
||||
t.channelz.SocketMetrics.StreamsStarted.Add(1)
|
||||
t.channelz.SocketMetrics.LastRemoteStreamCreatedTimestamp.Store(time.Now().UnixNano())
|
||||
}
|
||||
s.requestRead = func(n int) {
|
||||
t.adjustWindow(s, uint32(n))
|
||||
|
|
@ -658,8 +670,14 @@ func (t *http2Server) HandleStreams(ctx context.Context, handle func(*Stream)) {
|
|||
switch frame := frame.(type) {
|
||||
case *http2.MetaHeadersFrame:
|
||||
if err := t.operateHeaders(ctx, frame, handle); err != nil {
|
||||
t.Close(err)
|
||||
break
|
||||
// Any error processing client headers, e.g. invalid stream ID,
|
||||
// is considered a protocol violation.
|
||||
t.controlBuf.put(&goAway{
|
||||
code: http2.ErrCodeProtocol,
|
||||
debugData: []byte(err.Error()),
|
||||
closeConn: err,
|
||||
})
|
||||
continue
|
||||
}
|
||||
case *http2.DataFrame:
|
||||
t.handleData(frame)
|
||||
|
|
@ -1195,7 +1213,7 @@ func (t *http2Server) keepalive() {
|
|||
}
|
||||
if !outstandingPing {
|
||||
if channelz.IsOn() {
|
||||
atomic.AddInt64(&t.czData.kpCount, 1)
|
||||
t.channelz.SocketMetrics.KeepAlivesSent.Add(1)
|
||||
}
|
||||
t.controlBuf.put(p)
|
||||
kpTimeoutLeft = t.kp.Timeout
|
||||
|
|
@ -1235,7 +1253,7 @@ func (t *http2Server) Close(err error) {
|
|||
if err := t.conn.Close(); err != nil && t.logger.V(logLevel) {
|
||||
t.logger.Infof("Error closing underlying net.Conn during Close: %v", err)
|
||||
}
|
||||
channelz.RemoveEntry(t.channelzID)
|
||||
channelz.RemoveEntry(t.channelz.ID)
|
||||
// Cancel all active streams.
|
||||
for _, s := range streams {
|
||||
s.cancel()
|
||||
|
|
@ -1256,9 +1274,9 @@ func (t *http2Server) deleteStream(s *Stream, eosReceived bool) {
|
|||
|
||||
if channelz.IsOn() {
|
||||
if eosReceived {
|
||||
atomic.AddInt64(&t.czData.streamsSucceeded, 1)
|
||||
t.channelz.SocketMetrics.StreamsSucceeded.Add(1)
|
||||
} else {
|
||||
atomic.AddInt64(&t.czData.streamsFailed, 1)
|
||||
t.channelz.SocketMetrics.StreamsFailed.Add(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1375,38 +1393,21 @@ func (t *http2Server) outgoingGoAwayHandler(g *goAway) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (t *http2Server) ChannelzMetric() *channelz.SocketInternalMetric {
|
||||
s := channelz.SocketInternalMetric{
|
||||
StreamsStarted: atomic.LoadInt64(&t.czData.streamsStarted),
|
||||
StreamsSucceeded: atomic.LoadInt64(&t.czData.streamsSucceeded),
|
||||
StreamsFailed: atomic.LoadInt64(&t.czData.streamsFailed),
|
||||
MessagesSent: atomic.LoadInt64(&t.czData.msgSent),
|
||||
MessagesReceived: atomic.LoadInt64(&t.czData.msgRecv),
|
||||
KeepAlivesSent: atomic.LoadInt64(&t.czData.kpCount),
|
||||
LastRemoteStreamCreatedTimestamp: time.Unix(0, atomic.LoadInt64(&t.czData.lastStreamCreatedTime)),
|
||||
LastMessageSentTimestamp: time.Unix(0, atomic.LoadInt64(&t.czData.lastMsgSentTime)),
|
||||
LastMessageReceivedTimestamp: time.Unix(0, atomic.LoadInt64(&t.czData.lastMsgRecvTime)),
|
||||
LocalFlowControlWindow: int64(t.fc.getSize()),
|
||||
SocketOptions: channelz.GetSocketOption(t.conn),
|
||||
LocalAddr: t.peer.LocalAddr,
|
||||
RemoteAddr: t.peer.Addr,
|
||||
// RemoteName :
|
||||
func (t *http2Server) socketMetrics() *channelz.EphemeralSocketMetrics {
|
||||
return &channelz.EphemeralSocketMetrics{
|
||||
LocalFlowControlWindow: int64(t.fc.getSize()),
|
||||
RemoteFlowControlWindow: t.getOutFlowWindow(),
|
||||
}
|
||||
if au, ok := t.peer.AuthInfo.(credentials.ChannelzSecurityInfo); ok {
|
||||
s.Security = au.GetSecurityValue()
|
||||
}
|
||||
s.RemoteFlowControlWindow = t.getOutFlowWindow()
|
||||
return &s
|
||||
}
|
||||
|
||||
func (t *http2Server) IncrMsgSent() {
|
||||
atomic.AddInt64(&t.czData.msgSent, 1)
|
||||
atomic.StoreInt64(&t.czData.lastMsgSentTime, time.Now().UnixNano())
|
||||
t.channelz.SocketMetrics.MessagesSent.Add(1)
|
||||
t.channelz.SocketMetrics.LastMessageSentTimestamp.Add(1)
|
||||
}
|
||||
|
||||
func (t *http2Server) IncrMsgRecv() {
|
||||
atomic.AddInt64(&t.czData.msgRecv, 1)
|
||||
atomic.StoreInt64(&t.czData.lastMsgRecvTime, time.Now().UnixNano())
|
||||
t.channelz.SocketMetrics.MessagesReceived.Add(1)
|
||||
t.channelz.SocketMetrics.LastMessageReceivedTimestamp.Add(1)
|
||||
}
|
||||
|
||||
func (t *http2Server) getOutFlowWindow() int64 {
|
||||
|
|
|
|||
|
|
@ -418,10 +418,9 @@ func newFramer(conn net.Conn, writeBufferSize, readBufferSize int, sharedWriteBu
|
|||
return f
|
||||
}
|
||||
|
||||
func getWriteBufferPool(writeBufferSize int) *sync.Pool {
|
||||
func getWriteBufferPool(size int) *sync.Pool {
|
||||
writeBufferMutex.Lock()
|
||||
defer writeBufferMutex.Unlock()
|
||||
size := writeBufferSize * 2
|
||||
pool, ok := writeBufferPoolMap[size]
|
||||
if ok {
|
||||
return pool
|
||||
|
|
|
|||
|
|
@ -571,7 +571,7 @@ type ServerConfig struct {
|
|||
WriteBufferSize int
|
||||
ReadBufferSize int
|
||||
SharedWriteBuffer bool
|
||||
ChannelzParentID *channelz.Identifier
|
||||
ChannelzParent *channelz.Server
|
||||
MaxHeaderListSize *uint32
|
||||
HeaderTableSize *uint32
|
||||
}
|
||||
|
|
@ -606,8 +606,8 @@ type ConnectOptions struct {
|
|||
ReadBufferSize int
|
||||
// SharedWriteBuffer indicates whether connections should reuse write buffer
|
||||
SharedWriteBuffer bool
|
||||
// ChannelzParentID sets the addrConn id which initiate the creation of this client transport.
|
||||
ChannelzParentID *channelz.Identifier
|
||||
// ChannelzParent sets the addrConn id which initiated the creation of this client transport.
|
||||
ChannelzParent *channelz.SubChannel
|
||||
// MaxHeaderListSize sets the max (uncompressed) size of header list that is prepared to be received.
|
||||
MaxHeaderListSize *uint32
|
||||
// UseProxy specifies if a proxy should be used.
|
||||
|
|
@ -820,30 +820,6 @@ const (
|
|||
GoAwayTooManyPings GoAwayReason = 2
|
||||
)
|
||||
|
||||
// channelzData is used to store channelz related data for http2Client and http2Server.
|
||||
// These fields cannot be embedded in the original structs (e.g. http2Client), since to do atomic
|
||||
// operation on int64 variable on 32-bit machine, user is responsible to enforce memory alignment.
|
||||
// Here, by grouping those int64 fields inside a struct, we are enforcing the alignment.
|
||||
type channelzData struct {
|
||||
kpCount int64
|
||||
// The number of streams that have started, including already finished ones.
|
||||
streamsStarted int64
|
||||
// Client side: The number of streams that have ended successfully by receiving
|
||||
// EoS bit set frame from server.
|
||||
// Server side: The number of streams that have ended successfully by sending
|
||||
// frame with EoS bit set.
|
||||
streamsSucceeded int64
|
||||
streamsFailed int64
|
||||
// lastStreamCreatedTime stores the timestamp that the last stream gets created. It is of int64 type
|
||||
// instead of time.Time since it's more costly to atomically update time.Time variable than int64
|
||||
// variable. The same goes for lastMsgSentTime and lastMsgRecvTime.
|
||||
lastStreamCreatedTime int64
|
||||
msgSent int64
|
||||
msgRecv int64
|
||||
lastMsgSentTime int64
|
||||
lastMsgRecvTime int64
|
||||
}
|
||||
|
||||
// ContextErr converts the error from context package into a status error.
|
||||
func ContextErr(err error) error {
|
||||
switch err {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package internal
|
||||
// Package xds contains methods to Get/Set handshake cluster names. It is separated
|
||||
// out from the top level /internal package to avoid circular dependencies.
|
||||
package xds
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc/attributes"
|
||||
|
|
@ -38,19 +38,15 @@ const (
|
|||
logPrefix = "[pick-first-lb %p] "
|
||||
)
|
||||
|
||||
func newPickfirstBuilder() balancer.Builder {
|
||||
return &pickfirstBuilder{}
|
||||
}
|
||||
|
||||
type pickfirstBuilder struct{}
|
||||
|
||||
func (*pickfirstBuilder) Build(cc balancer.ClientConn, opt balancer.BuildOptions) balancer.Balancer {
|
||||
func (pickfirstBuilder) Build(cc balancer.ClientConn, opt balancer.BuildOptions) balancer.Balancer {
|
||||
b := &pickfirstBalancer{cc: cc}
|
||||
b.logger = internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf(logPrefix, b))
|
||||
return b
|
||||
}
|
||||
|
||||
func (*pickfirstBuilder) Name() string {
|
||||
func (pickfirstBuilder) Name() string {
|
||||
return PickFirstBalancerName
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +59,7 @@ type pfConfig struct {
|
|||
ShuffleAddressList bool `json:"shuffleAddressList"`
|
||||
}
|
||||
|
||||
func (*pickfirstBuilder) ParseConfig(js json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||||
func (pickfirstBuilder) ParseConfig(js json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||||
var cfg pfConfig
|
||||
if err := json.Unmarshal(js, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("pickfirst: unable to unmarshal LB policy config: %s, error: %v", string(js), err)
|
||||
|
|
@ -243,7 +239,3 @@ func (i *idlePicker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
|
|||
i.subConn.Connect()
|
||||
return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
|
||||
}
|
||||
|
||||
func init() {
|
||||
balancer.Register(newPickfirstBuilder())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,28 @@
|
|||
package dns
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/internal/resolver/dns"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
// SetResolvingTimeout sets the maximum duration for DNS resolution requests.
|
||||
//
|
||||
// This function affects the global timeout used by all channels using the DNS
|
||||
// name resolver scheme.
|
||||
//
|
||||
// It must be called only at application startup, before any gRPC calls are
|
||||
// made. Modifying this value after initialization is not thread-safe.
|
||||
//
|
||||
// The default value is 30 seconds. Setting the timeout too low may result in
|
||||
// premature timeouts during resolution, while setting it too high may lead to
|
||||
// unnecessary delays in service discovery. Choose a value appropriate for your
|
||||
// specific needs and network environment.
|
||||
func SetResolvingTimeout(timeout time.Duration) {
|
||||
dns.ResolvingTimeout = timeout
|
||||
}
|
||||
|
||||
// NewBuilder creates a dnsBuilder which is used to factory DNS resolvers.
|
||||
//
|
||||
// Deprecated: import grpc and use resolver.Get("dns") instead.
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import (
|
|||
|
||||
"google.golang.org/grpc/attributes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
)
|
||||
|
||||
|
|
@ -63,16 +64,18 @@ func Get(scheme string) Builder {
|
|||
}
|
||||
|
||||
// SetDefaultScheme sets the default scheme that will be used. The default
|
||||
// default scheme is "passthrough".
|
||||
// scheme is initially set to "passthrough".
|
||||
//
|
||||
// NOTE: this function must only be called during initialization time (i.e. in
|
||||
// an init() function), and is not thread-safe. The scheme set last overrides
|
||||
// previously set values.
|
||||
func SetDefaultScheme(scheme string) {
|
||||
defaultScheme = scheme
|
||||
internal.UserSetDefaultScheme = true
|
||||
}
|
||||
|
||||
// GetDefaultScheme gets the default scheme that will be used.
|
||||
// GetDefaultScheme gets the default scheme that will be used by grpc.Dial. If
|
||||
// SetDefaultScheme is never called, the default scheme used by grpc.NewClient is "dns" instead.
|
||||
func GetDefaultScheme() string {
|
||||
return defaultScheme
|
||||
}
|
||||
|
|
@ -284,9 +287,9 @@ func (t Target) Endpoint() string {
|
|||
return strings.TrimPrefix(endpoint, "/")
|
||||
}
|
||||
|
||||
// String returns a string representation of Target.
|
||||
// String returns the canonical string representation of Target.
|
||||
func (t Target) String() string {
|
||||
return t.URL.String()
|
||||
return t.URL.Scheme + "://" + t.URL.Host + "/" + t.Endpoint()
|
||||
}
|
||||
|
||||
// Builder creates a resolver that will be used to watch name resolution updates.
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ func (ccr *ccResolverWrapper) resolveNow(o resolver.ResolveNowOptions) {
|
|||
// finished shutting down, the channel should block on ccr.serializer.Done()
|
||||
// without cc.mu held.
|
||||
func (ccr *ccResolverWrapper) close() {
|
||||
channelz.Info(logger, ccr.cc.channelzID, "Closing the name resolver")
|
||||
channelz.Info(logger, ccr.cc.channelz, "Closing the name resolver")
|
||||
ccr.mu.Lock()
|
||||
ccr.closed = true
|
||||
ccr.mu.Unlock()
|
||||
|
|
@ -147,7 +147,7 @@ func (ccr *ccResolverWrapper) ReportError(err error) {
|
|||
return
|
||||
}
|
||||
ccr.mu.Unlock()
|
||||
channelz.Warningf(logger, ccr.cc.channelzID, "ccResolverWrapper: reporting error to cc: %v", err)
|
||||
channelz.Warningf(logger, ccr.cc.channelz, "ccResolverWrapper: reporting error to cc: %v", err)
|
||||
ccr.cc.updateResolverStateAndUnlock(resolver.State{}, err)
|
||||
}
|
||||
|
||||
|
|
@ -194,5 +194,5 @@ func (ccr *ccResolverWrapper) addChannelzTraceEvent(s resolver.State) {
|
|||
} else if len(ccr.curState.Addresses) == 0 && len(s.Addresses) > 0 {
|
||||
updates = append(updates, "resolver returned new addresses")
|
||||
}
|
||||
channelz.Infof(logger, ccr.cc.channelzID, "Resolver state updated: %s (%v)", pretty.ToJSON(s), strings.Join(updates, "; "))
|
||||
channelz.Infof(logger, ccr.cc.channelz, "Resolver state updated: %s (%v)", pretty.ToJSON(s), strings.Join(updates, "; "))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -962,19 +962,6 @@ func setCallInfoCodec(c *callInfo) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// channelzData is used to store channelz related data for ClientConn, addrConn and Server.
|
||||
// These fields cannot be embedded in the original structs (e.g. ClientConn), since to do atomic
|
||||
// operation on int64 variable on 32-bit machine, user is responsible to enforce memory alignment.
|
||||
// Here, by grouping those int64 fields inside a struct, we are enforcing the alignment.
|
||||
type channelzData struct {
|
||||
callsStarted int64
|
||||
callsFailed int64
|
||||
callsSucceeded int64
|
||||
// lastCallStartedTime stores the timestamp that last call starts. It is of int64 type instead of
|
||||
// time.Time since it's more costly to atomically update time.Time variable than int64 variable.
|
||||
lastCallStartedTime int64
|
||||
}
|
||||
|
||||
// The SupportPackageIsVersion variables are referenced from generated protocol
|
||||
// buffer files to ensure compatibility with the gRPC version used. The latest
|
||||
// support package version is 7.
|
||||
|
|
|
|||
|
|
@ -137,8 +137,7 @@ type Server struct {
|
|||
serveWG sync.WaitGroup // counts active Serve goroutines for Stop/GracefulStop
|
||||
handlersWG sync.WaitGroup // counts active method handler goroutines
|
||||
|
||||
channelzID *channelz.Identifier
|
||||
czData *channelzData
|
||||
channelz *channelz.Server
|
||||
|
||||
serverWorkerChannel chan func()
|
||||
serverWorkerChannelClose func()
|
||||
|
|
@ -249,11 +248,9 @@ func SharedWriteBuffer(val bool) ServerOption {
|
|||
}
|
||||
|
||||
// WriteBufferSize determines how much data can be batched before doing a write
|
||||
// on the wire. The corresponding memory allocation for this buffer will be
|
||||
// twice the size to keep syscalls low. The default value for this buffer is
|
||||
// 32KB. Zero or negative values will disable the write buffer such that each
|
||||
// write will be on underlying connection.
|
||||
// Note: A Send call may not directly translate to a write.
|
||||
// on the wire. The default value for this buffer is 32KB. Zero or negative
|
||||
// values will disable the write buffer such that each write will be on underlying
|
||||
// connection. Note: A Send call may not directly translate to a write.
|
||||
func WriteBufferSize(s int) ServerOption {
|
||||
return newFuncServerOption(func(o *serverOptions) {
|
||||
o.writeBufferSize = s
|
||||
|
|
@ -661,7 +658,7 @@ func NewServer(opt ...ServerOption) *Server {
|
|||
services: make(map[string]*serviceInfo),
|
||||
quit: grpcsync.NewEvent(),
|
||||
done: grpcsync.NewEvent(),
|
||||
czData: new(channelzData),
|
||||
channelz: channelz.RegisterServer(""),
|
||||
}
|
||||
chainUnaryServerInterceptors(s)
|
||||
chainStreamServerInterceptors(s)
|
||||
|
|
@ -675,8 +672,7 @@ func NewServer(opt ...ServerOption) *Server {
|
|||
s.initServerWorkers()
|
||||
}
|
||||
|
||||
s.channelzID = channelz.RegisterServer(&channelzServer{s}, "")
|
||||
channelz.Info(logger, s.channelzID, "Server created")
|
||||
channelz.Info(logger, s.channelz, "Server created")
|
||||
return s
|
||||
}
|
||||
|
||||
|
|
@ -802,20 +798,13 @@ var ErrServerStopped = errors.New("grpc: the server has been stopped")
|
|||
|
||||
type listenSocket struct {
|
||||
net.Listener
|
||||
channelzID *channelz.Identifier
|
||||
}
|
||||
|
||||
func (l *listenSocket) ChannelzMetric() *channelz.SocketInternalMetric {
|
||||
return &channelz.SocketInternalMetric{
|
||||
SocketOptions: channelz.GetSocketOption(l.Listener),
|
||||
LocalAddr: l.Listener.Addr(),
|
||||
}
|
||||
channelz *channelz.Socket
|
||||
}
|
||||
|
||||
func (l *listenSocket) Close() error {
|
||||
err := l.Listener.Close()
|
||||
channelz.RemoveEntry(l.channelzID)
|
||||
channelz.Info(logger, l.channelzID, "ListenSocket deleted")
|
||||
channelz.RemoveEntry(l.channelz.ID)
|
||||
channelz.Info(logger, l.channelz, "ListenSocket deleted")
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -857,7 +846,16 @@ func (s *Server) Serve(lis net.Listener) error {
|
|||
}
|
||||
}()
|
||||
|
||||
ls := &listenSocket{Listener: lis}
|
||||
ls := &listenSocket{
|
||||
Listener: lis,
|
||||
channelz: channelz.RegisterSocket(&channelz.Socket{
|
||||
SocketType: channelz.SocketTypeListen,
|
||||
Parent: s.channelz,
|
||||
RefName: lis.Addr().String(),
|
||||
LocalAddr: lis.Addr(),
|
||||
SocketOptions: channelz.GetSocketOption(lis)},
|
||||
),
|
||||
}
|
||||
s.lis[ls] = true
|
||||
|
||||
defer func() {
|
||||
|
|
@ -869,14 +867,8 @@ func (s *Server) Serve(lis net.Listener) error {
|
|||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
var err error
|
||||
ls.channelzID, err = channelz.RegisterListenSocket(ls, s.channelzID, lis.Addr().String())
|
||||
if err != nil {
|
||||
s.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
s.mu.Unlock()
|
||||
channelz.Info(logger, ls.channelzID, "ListenSocket created")
|
||||
channelz.Info(logger, ls.channelz, "ListenSocket created")
|
||||
|
||||
var tempDelay time.Duration // how long to sleep on accept failure
|
||||
for {
|
||||
|
|
@ -975,7 +967,7 @@ func (s *Server) newHTTP2Transport(c net.Conn) transport.ServerTransport {
|
|||
WriteBufferSize: s.opts.writeBufferSize,
|
||||
ReadBufferSize: s.opts.readBufferSize,
|
||||
SharedWriteBuffer: s.opts.sharedWriteBuffer,
|
||||
ChannelzParentID: s.channelzID,
|
||||
ChannelzParent: s.channelz,
|
||||
MaxHeaderListSize: s.opts.maxHeaderListSize,
|
||||
HeaderTableSize: s.opts.headerTableSize,
|
||||
}
|
||||
|
|
@ -989,7 +981,7 @@ func (s *Server) newHTTP2Transport(c net.Conn) transport.ServerTransport {
|
|||
if err != credentials.ErrConnDispatched {
|
||||
// Don't log on ErrConnDispatched and io.EOF to prevent log spam.
|
||||
if err != io.EOF {
|
||||
channelz.Info(logger, s.channelzID, "grpc: Server.Serve failed to create ServerTransport: ", err)
|
||||
channelz.Info(logger, s.channelz, "grpc: Server.Serve failed to create ServerTransport: ", err)
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
|
|
@ -1121,37 +1113,28 @@ func (s *Server) removeConn(addr string, st transport.ServerTransport) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Server) channelzMetric() *channelz.ServerInternalMetric {
|
||||
return &channelz.ServerInternalMetric{
|
||||
CallsStarted: atomic.LoadInt64(&s.czData.callsStarted),
|
||||
CallsSucceeded: atomic.LoadInt64(&s.czData.callsSucceeded),
|
||||
CallsFailed: atomic.LoadInt64(&s.czData.callsFailed),
|
||||
LastCallStartedTimestamp: time.Unix(0, atomic.LoadInt64(&s.czData.lastCallStartedTime)),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) incrCallsStarted() {
|
||||
atomic.AddInt64(&s.czData.callsStarted, 1)
|
||||
atomic.StoreInt64(&s.czData.lastCallStartedTime, time.Now().UnixNano())
|
||||
s.channelz.ServerMetrics.CallsStarted.Add(1)
|
||||
s.channelz.ServerMetrics.LastCallStartedTimestamp.Store(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (s *Server) incrCallsSucceeded() {
|
||||
atomic.AddInt64(&s.czData.callsSucceeded, 1)
|
||||
s.channelz.ServerMetrics.CallsSucceeded.Add(1)
|
||||
}
|
||||
|
||||
func (s *Server) incrCallsFailed() {
|
||||
atomic.AddInt64(&s.czData.callsFailed, 1)
|
||||
s.channelz.ServerMetrics.CallsFailed.Add(1)
|
||||
}
|
||||
|
||||
func (s *Server) sendResponse(ctx context.Context, t transport.ServerTransport, stream *transport.Stream, msg any, cp Compressor, opts *transport.Options, comp encoding.Compressor) error {
|
||||
data, err := encode(s.getCodec(stream.ContentSubtype()), msg)
|
||||
if err != nil {
|
||||
channelz.Error(logger, s.channelzID, "grpc: server failed to encode response: ", err)
|
||||
channelz.Error(logger, s.channelz, "grpc: server failed to encode response: ", err)
|
||||
return err
|
||||
}
|
||||
compData, err := compress(data, cp, comp)
|
||||
if err != nil {
|
||||
channelz.Error(logger, s.channelzID, "grpc: server failed to compress response: ", err)
|
||||
channelz.Error(logger, s.channelz, "grpc: server failed to compress response: ", err)
|
||||
return err
|
||||
}
|
||||
hdr, payload := msgHeader(data, compData)
|
||||
|
|
@ -1346,7 +1329,7 @@ func (s *Server) processUnaryRPC(ctx context.Context, t transport.ServerTranspor
|
|||
d, cancel, err := recvAndDecompress(&parser{r: stream, recvBufferPool: s.opts.recvBufferPool}, stream, dc, s.opts.maxReceiveMessageSize, payInfo, decomp)
|
||||
if err != nil {
|
||||
if e := t.WriteStatus(stream, status.Convert(err)); e != nil {
|
||||
channelz.Warningf(logger, s.channelzID, "grpc: Server.processUnaryRPC failed to write status: %v", e)
|
||||
channelz.Warningf(logger, s.channelz, "grpc: Server.processUnaryRPC failed to write status: %v", e)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -1397,7 +1380,7 @@ func (s *Server) processUnaryRPC(ctx context.Context, t transport.ServerTranspor
|
|||
trInfo.tr.SetError()
|
||||
}
|
||||
if e := t.WriteStatus(stream, appStatus); e != nil {
|
||||
channelz.Warningf(logger, s.channelzID, "grpc: Server.processUnaryRPC failed to write status: %v", e)
|
||||
channelz.Warningf(logger, s.channelz, "grpc: Server.processUnaryRPC failed to write status: %v", e)
|
||||
}
|
||||
if len(binlogs) != 0 {
|
||||
if h, _ := stream.Header(); h.Len() > 0 {
|
||||
|
|
@ -1437,7 +1420,7 @@ func (s *Server) processUnaryRPC(ctx context.Context, t transport.ServerTranspor
|
|||
}
|
||||
if sts, ok := status.FromError(err); ok {
|
||||
if e := t.WriteStatus(stream, sts); e != nil {
|
||||
channelz.Warningf(logger, s.channelzID, "grpc: Server.processUnaryRPC failed to write status: %v", e)
|
||||
channelz.Warningf(logger, s.channelz, "grpc: Server.processUnaryRPC failed to write status: %v", e)
|
||||
}
|
||||
} else {
|
||||
switch st := err.(type) {
|
||||
|
|
@ -1765,7 +1748,7 @@ func (s *Server) handleStream(t transport.ServerTransport, stream *transport.Str
|
|||
ti.tr.LazyLog(&fmtStringer{"%v", []any{err}}, true)
|
||||
ti.tr.SetError()
|
||||
}
|
||||
channelz.Warningf(logger, s.channelzID, "grpc: Server.handleStream failed to write status: %v", err)
|
||||
channelz.Warningf(logger, s.channelz, "grpc: Server.handleStream failed to write status: %v", err)
|
||||
}
|
||||
if ti != nil {
|
||||
ti.tr.Finish()
|
||||
|
|
@ -1822,7 +1805,7 @@ func (s *Server) handleStream(t transport.ServerTransport, stream *transport.Str
|
|||
ti.tr.LazyLog(&fmtStringer{"%v", []any{err}}, true)
|
||||
ti.tr.SetError()
|
||||
}
|
||||
channelz.Warningf(logger, s.channelzID, "grpc: Server.handleStream failed to write status: %v", err)
|
||||
channelz.Warningf(logger, s.channelz, "grpc: Server.handleStream failed to write status: %v", err)
|
||||
}
|
||||
if ti != nil {
|
||||
ti.tr.Finish()
|
||||
|
|
@ -1894,8 +1877,7 @@ func (s *Server) stop(graceful bool) {
|
|||
s.quit.Fire()
|
||||
defer s.done.Fire()
|
||||
|
||||
s.channelzRemoveOnce.Do(func() { channelz.RemoveEntry(s.channelzID) })
|
||||
|
||||
s.channelzRemoveOnce.Do(func() { channelz.RemoveEntry(s.channelz.ID) })
|
||||
s.mu.Lock()
|
||||
s.closeListenersLocked()
|
||||
// Wait for serving threads to be ready to exit. Only then can we be sure no
|
||||
|
|
@ -2150,14 +2132,6 @@ func Method(ctx context.Context) (string, bool) {
|
|||
return s.Method(), true
|
||||
}
|
||||
|
||||
type channelzServer struct {
|
||||
s *Server
|
||||
}
|
||||
|
||||
func (c *channelzServer) ChannelzMetric() *channelz.ServerInternalMetric {
|
||||
return c.s.channelzMetric()
|
||||
}
|
||||
|
||||
// validateSendCompressor returns an error when given compressor name cannot be
|
||||
// handled by the server or the client based on the advertised compressors.
|
||||
func validateSendCompressor(name string, clientCompressors []string) error {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,10 @@ import (
|
|||
"reflect"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/internal/balancer/gracefulswitch"
|
||||
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
)
|
||||
|
|
@ -41,11 +43,6 @@ const maxInt = int(^uint(0) >> 1)
|
|||
// https://github.com/grpc/grpc/blob/master/doc/service_config.md
|
||||
type MethodConfig = internalserviceconfig.MethodConfig
|
||||
|
||||
type lbConfig struct {
|
||||
name string
|
||||
cfg serviceconfig.LoadBalancingConfig
|
||||
}
|
||||
|
||||
// ServiceConfig is provided by the service provider and contains parameters for how
|
||||
// clients that connect to the service should behave.
|
||||
//
|
||||
|
|
@ -55,14 +52,9 @@ type lbConfig struct {
|
|||
type ServiceConfig struct {
|
||||
serviceconfig.Config
|
||||
|
||||
// LB is the load balancer the service providers recommends. This is
|
||||
// deprecated; lbConfigs is preferred. If lbConfig and LB are both present,
|
||||
// lbConfig will be used.
|
||||
LB *string
|
||||
|
||||
// lbConfig is the service config's load balancing configuration. If
|
||||
// lbConfig and LB are both present, lbConfig will be used.
|
||||
lbConfig *lbConfig
|
||||
lbConfig serviceconfig.LoadBalancingConfig
|
||||
|
||||
// Methods contains a map for the methods in this service. If there is an
|
||||
// exact match for a method (i.e. /service/method) in the map, use the
|
||||
|
|
@ -164,7 +156,7 @@ type jsonMC struct {
|
|||
// TODO(lyuxuan): delete this struct after cleaning up old service config implementation.
|
||||
type jsonSC struct {
|
||||
LoadBalancingPolicy *string
|
||||
LoadBalancingConfig *internalserviceconfig.BalancerConfig
|
||||
LoadBalancingConfig *json.RawMessage
|
||||
MethodConfig *[]jsonMC
|
||||
RetryThrottling *retryThrottlingPolicy
|
||||
HealthCheckConfig *healthCheckConfig
|
||||
|
|
@ -184,18 +176,33 @@ func parseServiceConfig(js string) *serviceconfig.ParseResult {
|
|||
return &serviceconfig.ParseResult{Err: err}
|
||||
}
|
||||
sc := ServiceConfig{
|
||||
LB: rsc.LoadBalancingPolicy,
|
||||
Methods: make(map[string]MethodConfig),
|
||||
retryThrottling: rsc.RetryThrottling,
|
||||
healthCheckConfig: rsc.HealthCheckConfig,
|
||||
rawJSONString: js,
|
||||
}
|
||||
if c := rsc.LoadBalancingConfig; c != nil {
|
||||
sc.lbConfig = &lbConfig{
|
||||
name: c.Name,
|
||||
cfg: c.Config,
|
||||
c := rsc.LoadBalancingConfig
|
||||
if c == nil {
|
||||
name := PickFirstBalancerName
|
||||
if rsc.LoadBalancingPolicy != nil {
|
||||
name = *rsc.LoadBalancingPolicy
|
||||
}
|
||||
if balancer.Get(name) == nil {
|
||||
name = PickFirstBalancerName
|
||||
}
|
||||
cfg := []map[string]any{{name: struct{}{}}}
|
||||
strCfg, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return &serviceconfig.ParseResult{Err: fmt.Errorf("unexpected error marshaling simple LB config: %w", err)}
|
||||
}
|
||||
r := json.RawMessage(strCfg)
|
||||
c = &r
|
||||
}
|
||||
cfg, err := gracefulswitch.ParseConfig(*c)
|
||||
if err != nil {
|
||||
return &serviceconfig.ParseResult{Err: err}
|
||||
}
|
||||
sc.lbConfig = cfg
|
||||
|
||||
if rsc.MethodConfig == nil {
|
||||
return &serviceconfig.ParseResult{Config: &sc}
|
||||
|
|
|
|||
|
|
@ -655,13 +655,13 @@ func (a *csAttempt) shouldRetry(err error) (bool, error) {
|
|||
if len(sps) == 1 {
|
||||
var e error
|
||||
if pushback, e = strconv.Atoi(sps[0]); e != nil || pushback < 0 {
|
||||
channelz.Infof(logger, cs.cc.channelzID, "Server retry pushback specified to abort (%q).", sps[0])
|
||||
channelz.Infof(logger, cs.cc.channelz, "Server retry pushback specified to abort (%q).", sps[0])
|
||||
cs.retryThrottler.throttle() // This counts as a failure for throttling.
|
||||
return false, err
|
||||
}
|
||||
hasPushback = true
|
||||
} else if len(sps) > 1 {
|
||||
channelz.Warningf(logger, cs.cc.channelzID, "Server retry pushback specified multiple values (%q); not retrying.", sps)
|
||||
channelz.Warningf(logger, cs.cc.channelz, "Server retry pushback specified multiple values (%q); not retrying.", sps)
|
||||
cs.retryThrottler.throttle() // This counts as a failure for throttling.
|
||||
return false, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,4 +19,4 @@
|
|||
package grpc
|
||||
|
||||
// Version is the current grpc version.
|
||||
const Version = "1.62.1"
|
||||
const Version = "1.63.2"
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ git grep 'func [A-Z]' -- "*_test.go" | not grep -v 'func Test\|Benchmark\|Exampl
|
|||
# - Do not import x/net/context.
|
||||
not git grep -l 'x/net/context' -- "*.go"
|
||||
|
||||
# - Do not use time.After except in tests. It has the potential to leak the
|
||||
# timer since there is no way to stop it early.
|
||||
git grep -l 'time.After(' -- "*.go" | not grep -v '_test.go\|test_utils\|testutils'
|
||||
|
||||
# - Do not import math/rand for real library code. Use internal/grpcrand for
|
||||
# thread safety.
|
||||
git grep -l '"math/rand"' -- "*.go" 2>&1 | not grep -v '^examples\|^interop/stress\|grpcrand\|^benchmark\|wrr_test'
|
||||
|
|
@ -172,6 +176,7 @@ UpdateAddresses is deprecated:
|
|||
UpdateSubConnState is deprecated:
|
||||
balancer.ErrTransientFailure is deprecated:
|
||||
grpc/reflection/v1alpha/reflection.proto
|
||||
SwitchTo is deprecated:
|
||||
XXXXX xDS deprecated fields we support
|
||||
.ExactMatch
|
||||
.PrefixMatch
|
||||
|
|
|
|||
|
|
@ -4,10 +4,26 @@ cloud.google.com/go/internal
|
|||
cloud.google.com/go/internal/optional
|
||||
cloud.google.com/go/internal/trace
|
||||
cloud.google.com/go/internal/version
|
||||
# cloud.google.com/go/compute v1.25.1
|
||||
# cloud.google.com/go/auth v0.2.2
|
||||
## explicit; go 1.19
|
||||
cloud.google.com/go/compute/internal
|
||||
# cloud.google.com/go/compute/metadata v0.2.3
|
||||
cloud.google.com/go/auth
|
||||
cloud.google.com/go/auth/credentials
|
||||
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
|
||||
cloud.google.com/go/auth/credentials/internal/stsexchange
|
||||
cloud.google.com/go/auth/grpctransport
|
||||
cloud.google.com/go/auth/httptransport
|
||||
cloud.google.com/go/auth/internal
|
||||
cloud.google.com/go/auth/internal/credsfile
|
||||
cloud.google.com/go/auth/internal/jwt
|
||||
cloud.google.com/go/auth/internal/transport
|
||||
cloud.google.com/go/auth/internal/transport/cert
|
||||
# cloud.google.com/go/auth/oauth2adapt v0.2.1
|
||||
## explicit; go 1.19
|
||||
cloud.google.com/go/auth/oauth2adapt
|
||||
# cloud.google.com/go/compute/metadata v0.3.0
|
||||
## explicit; go 1.19
|
||||
cloud.google.com/go/compute/metadata
|
||||
# cloud.google.com/go/iam v1.1.7
|
||||
|
|
@ -1188,7 +1204,7 @@ golang.org/x/tools/internal/tokeninternal
|
|||
golang.org/x/tools/internal/typeparams
|
||||
golang.org/x/tools/internal/typesinternal
|
||||
golang.org/x/tools/internal/versions
|
||||
# google.golang.org/api v0.172.0
|
||||
# google.golang.org/api v0.175.0
|
||||
## explicit; go 1.19
|
||||
google.golang.org/api/googleapi
|
||||
google.golang.org/api/googleapi/transport
|
||||
|
|
@ -1206,7 +1222,7 @@ google.golang.org/api/transport
|
|||
google.golang.org/api/transport/grpc
|
||||
google.golang.org/api/transport/http
|
||||
google.golang.org/api/transport/http/internal/propagation
|
||||
# google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9
|
||||
# google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de
|
||||
## explicit; go 1.19
|
||||
google.golang.org/genproto/googleapis/type/date
|
||||
google.golang.org/genproto/googleapis/type/expr
|
||||
|
|
@ -1215,12 +1231,12 @@ google.golang.org/genproto/protobuf/field_mask
|
|||
## explicit; go 1.19
|
||||
google.golang.org/genproto/googleapis/api
|
||||
google.golang.org/genproto/googleapis/api/annotations
|
||||
# google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237
|
||||
# google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be
|
||||
## explicit; go 1.19
|
||||
google.golang.org/genproto/googleapis/rpc/code
|
||||
google.golang.org/genproto/googleapis/rpc/errdetails
|
||||
google.golang.org/genproto/googleapis/rpc/status
|
||||
# google.golang.org/grpc v1.62.1
|
||||
# google.golang.org/grpc v1.63.2
|
||||
## explicit; go 1.19
|
||||
google.golang.org/grpc
|
||||
google.golang.org/grpc/attributes
|
||||
|
|
@ -1277,6 +1293,7 @@ google.golang.org/grpc/internal/status
|
|||
google.golang.org/grpc/internal/syscall
|
||||
google.golang.org/grpc/internal/transport
|
||||
google.golang.org/grpc/internal/transport/networktype
|
||||
google.golang.org/grpc/internal/xds
|
||||
google.golang.org/grpc/keepalive
|
||||
google.golang.org/grpc/metadata
|
||||
google.golang.org/grpc/peer
|
||||
|
|
|
|||
Loading…
Reference in New Issue