diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..638105e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,32 @@ +# Copyright 2022 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 + +version: 2 +updates: +- package-ecosystem: gomod + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + groups: + golang-org-x: + patterns: + - "golang.org/x/*" + k8s-io: + patterns: + - "k8s.io/*" + - "sigs.k8s.io/*" + otel: + patterns: + - "go.opentelemetry.io/*" + prom: + patterns: + - "github.com/prometheus/*" + grpc: + patterns: + - "github.com/grpc-ecosystem/*" +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/.github/workflows/actionlint.yaml b/.github/workflows/actionlint.yaml new file mode 100644 index 0000000..cfdbdda --- /dev/null +++ b/.github/workflows/actionlint.yaml @@ -0,0 +1,31 @@ +# Copyright 2022 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 + +name: Action Lint + +on: + pull_request: + branches: [ 'main', 'release-*' ] + +jobs: + + action-lint: + name: Action lint + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Find yamls + id: get_yamls + run: | + set -x + set -e + mapfile -t yamls < <(find .github/workflows -name "*.y*ml" | grep -v dependabot.) + echo "files=${yamls[*]}" >> "${GITHUB_OUTPUT}" + + - name: Action lint + uses: reviewdog/action-actionlint@7eeec1dd160c2301eb28e1568721837d084558ad # v1.57.0 + with: + actionlint_flags: ${{ steps.get_yamls.outputs.files }} diff --git a/.github/workflows/boilerplate.yaml b/.github/workflows/boilerplate.yaml new file mode 100644 index 0000000..0b03d22 --- /dev/null +++ b/.github/workflows/boilerplate.yaml @@ -0,0 +1,39 @@ +# Copyright 2022 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 + +name: Boilerplate + +on: + pull_request: + branches: [ 'main', 'release-*' ] + +jobs: + + check: + name: Boilerplate Check + runs-on: ubuntu-latest + strategy: + fail-fast: false # Keep running if one leg fails. + matrix: + extension: + - go + - sh + - yaml + + # Map between extension and human-readable name. + include: + - extension: go + language: Go + - extension: sh + language: Bash + - extension: yaml + language: YAML + + steps: + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: khulnasoft/actions/boilerplate@main + with: + extension: ${{ matrix.extension }} + language: ${{ matrix.language }} diff --git a/.github/workflows/donotsubmit.yaml b/.github/workflows/donotsubmit.yaml new file mode 100644 index 0000000..72bbdf9 --- /dev/null +++ b/.github/workflows/donotsubmit.yaml @@ -0,0 +1,21 @@ +# Copyright 2022 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 + +name: Do Not Submit + +on: + pull_request: + branches: [ 'main', 'release-*' ] + +jobs: + + donotsubmit: + name: Do Not Submit + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Do Not Submit + uses: khulnasoft/actions/donotsubmit@main diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml new file mode 100644 index 0000000..0966d19 --- /dev/null +++ b/.github/workflows/go-test.yaml @@ -0,0 +1,40 @@ +# Copyright 2022 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 + +name: Test + +on: + pull_request: + branches: [ 'main', 'release-*' ] + push: + branches: [ 'main', 'release-*' ] + +jobs: + + test: + runs-on: ubuntu-latest + steps: + - name: Check out code onto GOPATH + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # https://github.com/mvdan/github-actions-golang#how-do-i-set-up-caching-between-builds + - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + with: + # In order: + # * Module download cache + # * Build cache (Linux) + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Set up Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: './go.mod' + check-latest: true + + - run: | + go test -race ./... diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml new file mode 100644 index 0000000..f034207 --- /dev/null +++ b/.github/workflows/style.yaml @@ -0,0 +1,110 @@ +# Copyright 2022 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 + +name: Code Style + +on: + pull_request: + branches: [ 'main', 'release-*' ] + push: + branches: [ 'main', 'release-*' ] + +jobs: + + gofmt: + name: check gofmt + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: './go.mod' + check-latest: true + + - uses: khulnasoft/actions/gofmt@main + with: + args: -s + + goimports: + name: check goimports + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: './go.mod' + check-latest: true + + - uses: khulnasoft/actions/goimports@main + + golangci-lint: + name: golangci-lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: './go.mod' + check-latest: true + cache: true + + - name: golangci-lint + uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 + with: + # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. + version: v1.60 + args: --timeout=5m + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: './go.mod' + check-latest: true + + - uses: khulnasoft/actions/trailing-space@main + if: ${{ always() }} + + - uses: khulnasoft/actions/eof-newline@main + if: ${{ always() }} + + - uses: reviewdog/action-tflint@master + if: ${{ always() }} + with: + github_token: ${{ secrets.github_token }} + fail_on_error: true + + - uses: reviewdog/action-misspell@ef8b22c1cca06c8d306fc6be302c3dab0f6ca12f # v1.23.0 + if: ${{ always() }} + with: + github_token: ${{ secrets.github_token }} + fail_on_error: true + locale: "US" + exclude: | + **/go.sum + **/third_party/** + ./*.yml + + - uses: get-woke/woke-action-reviewdog@d71fd0115146a01c3181439ce714e21a69d75e31 # v0 + if: ${{ always() }} + with: + github-token: ${{ secrets.github_token }} + reporter: github-pr-check + level: error + fail-on-error: true diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml new file mode 100644 index 0000000..a5766d5 --- /dev/null +++ b/.github/workflows/verify.yaml @@ -0,0 +1,57 @@ +# Copyright 2022 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 + +name: Verify + +on: + pull_request: + branches: [ 'main', 'release-*' ] + push: + branches: [ 'main', 'release-*' ] + +jobs: + + verify: + name: Verify Codegen + runs-on: ubuntu-latest + + env: + GOPATH: ${{ github.workspace }} + + steps: + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + path: ./src/github.com/${{ github.repository }} + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: './src/github.com/${{ github.repository }}/go.mod' + check-latest: true + + # https://github.com/mvdan/github-actions-golang#how-do-i-set-up-caching-between-builds + - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + with: + # In order: + # * Module download cache + # * Build cache (Linux) + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Update Codegen + shell: bash + working-directory: ./src/github.com/${{ github.repository }} + run: | + ./hack/update-codegen.sh + + - name: Verify + uses: khulnasoft/actions/nodiff@main + with: + path: ./src/github.com/${{ github.repository }} + fixup-command: "./hack/update-codegen.sh" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e61579a --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Editor files +.idea/ + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..b00cbb1 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,35 @@ +# Copyright 2022 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 + +linters: + enable: + - asciicheck + - unused + - errcheck + - errorlint + - gofmt + - goimports + - gosec + - gocritic + - importas + - prealloc + - revive + - misspell + - stylecheck + - tparallel + - unconvert + - unparam + - whitespace +output: + uniq-by-line: false +issues: + exclude-rules: + - path: _test\.go + linters: + - errcheck + - gosec + max-issues-per-linter: 0 + max-same-issues: 0 +run: + issues-exit-code: 1 + timeout: 10m diff --git a/.khulnasoft/source.yaml b/.khulnasoft/source.yaml new file mode 100644 index 0000000..bc8c0d6 --- /dev/null +++ b/.khulnasoft/source.yaml @@ -0,0 +1,13 @@ +# Copyright 2023 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 + +spec: + authorities: + - keyless: + # allow commits signed by users using GitHub or Google OIDC + identities: + - issuer: https://accounts.google.com + - issuer: https://github.com/login/oauth + - key: + # allow commits signed by GitHub, e.g. the UI + kms: https://github.com/web-flow.gpg diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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. diff --git a/README.md b/README.md index 8db9e71..46604bc 100644 --- a/README.md +++ b/README.md @@ -1 +1,44 @@ -# go-grpc-kit \ No newline at end of file +# gRPC Kit + +This repository contains helpers working with gRPC. + +## gRPC Duplex + +For cases where you would like to serve both the gRPC endpoint and a +[gRPC Gateway](https://grpc-ecosystem.github.io/grpc-gateway/), the +go.khulnasoft.com/grpc/pkg/duplex package is a helpful wrapper. + +First, define the REST endpoints in the `.proto` file. Then change to use the +duplex object to start and build up the gRPC server. Example: + +```go +d := duplex.New(8080) + +pb.RegisterServer(d.Server, impl.NewServer()) +if err := d.RegisterHandler(ctx, impl.RegisterServiceHandlerFromEndpoint); err != nil { + log.Panicf("Failed to register gateway endpoint: %v", err) +} + +... + +if err := d.ListenAndServe(ctx); err != nil { + log.Panicf("ListenAndServe() = %v", err) +} +``` + +Run this and you should see a message like: + +```text +Duplex gRPC/HTTP server starting at ::8080 +``` + +### Options + +You can pass `grpc.ServerOption` and `runtime.NewServeMux` into `duplex.New`. + +For example, if you wanted to make a duplex server with a +[unary interceptor](https://pkg.go.dev/google.golang.org/grpc#UnaryInterceptor): + +```go +d := duplex.New(port, grpc.UnaryInterceptor(myInterceptorFn)) +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c2192a1 --- /dev/null +++ b/go.mod @@ -0,0 +1,44 @@ +module go.khulnasoft.com/go-grpc-kit + +go 1.23.1 + +require ( + github.com/google/go-cmp v0.6.0 + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 + github.com/kelseyhightower/envconfig v1.4.0 + github.com/khulnasoft/clog v0.0.0-20241204024456-7c9c0d8a98ab + github.com/prometheus/client_golang v1.20.5 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 + go.opentelemetry.io/otel/sdk v1.32.0 + golang.org/x/net v0.31.0 + golang.org/x/oauth2 v0.24.0 + google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 + google.golang.org/grpc v1.68.0 + google.golang.org/protobuf v1.35.2 +) + +require ( + cloud.google.com/go/compute/metadata v0.5.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.60.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f220c48 --- /dev/null +++ b/go.sum @@ -0,0 +1,202 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 h1:JYghRBlGCZyCF2wNUJ8W0cwaQdtpcssJ4CgC406g+WU= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/khulnasoft/clog v0.0.0-20241204024456-7c9c0d8a98ab h1:vYuIUNqCcimq/ebAL8ZmTRXC0Vw2nipMS3DKoWeD5Jw= +github.com/khulnasoft/clog v0.0.0-20241204024456-7c9c0d8a98ab/go.mod h1:SQgf4mm4/Zq3VMNgiBXrXpg4Z1DOvO7A2UJnIajIJl0= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= +github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 h1:qtFISDHKolvIxzSs0gIaiPUPR0Cucb0F2coHC7ZLdps= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0/go.mod h1:Y+Pop1Q6hCOnETWTW4NROK/q1hv50hM7yDaUTjG8lp8= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +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.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/hack/boilerplate/boilerplate.go.txt b/hack/boilerplate/boilerplate.go.txt new file mode 100644 index 0000000..a0ec2a5 --- /dev/null +++ b/hack/boilerplate/boilerplate.go.txt @@ -0,0 +1,4 @@ +/* +Copyright 2022 KhulnaSoft, Ltd. +SPDX-License-Identifier: Apache-2.0 +*/ diff --git a/hack/boilerplate/boilerplate.sh.txt b/hack/boilerplate/boilerplate.sh.txt new file mode 100644 index 0000000..1d3f289 --- /dev/null +++ b/hack/boilerplate/boilerplate.sh.txt @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# Copyright 2022 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 diff --git a/hack/boilerplate/boilerplate.yaml.txt b/hack/boilerplate/boilerplate.yaml.txt new file mode 100644 index 0000000..89f1f18 --- /dev/null +++ b/hack/boilerplate/boilerplate.yaml.txt @@ -0,0 +1,2 @@ +# Copyright 2022 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 diff --git a/hack/tools.go b/hack/tools.go new file mode 100644 index 0000000..1bb2cf7 --- /dev/null +++ b/hack/tools.go @@ -0,0 +1,9 @@ +//go:build tools +// +build tools + +/* +Copyright 2022 KhulnaSoft, Ltd. +SPDX-License-Identifier: Apache-2.0 +*/ + +package tools diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh new file mode 100755 index 0000000..e023da0 --- /dev/null +++ b/hack/update-codegen.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Copyright 2022 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 + +set -o errexit +set -o nounset +set -o pipefail + +REPO_ROOT_DIR=$PWD/$(dirname "$0")/.. + +echo === Tidying up for Golang +go mod tidy + +echo === Generating for Golang +go generate ./... + +# Make sure our dependencies are up-to-date +${REPO_ROOT_DIR}/hack/update-deps.sh diff --git a/hack/update-deps.sh b/hack/update-deps.sh new file mode 100755 index 0000000..3aff225 --- /dev/null +++ b/hack/update-deps.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +# Copyright 2022 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 + +set -o errexit +set -o nounset +set -o pipefail + +echo === Update Deps for Golang +go mod tidy diff --git a/hack/verify-golangci-lint.sh b/hack/verify-golangci-lint.sh new file mode 100755 index 0000000..3bcc0ef --- /dev/null +++ b/hack/verify-golangci-lint.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# Copyright 2021 KhulnaSoft, Ltd. +# SPDX-License-Identifier: Apache-2.0 + +set -o errexit +set -o nounset +set -o pipefail + +VERSION=v1.62.0 +URL_BASE=https://raw.githubusercontent.com/golangci/golangci-lint +URL=$URL_BASE/$VERSION/install.sh +# If you update the version above you might need to update the checksum +# if it does not match. We say might because in the past the install script +# has been unchanged even if there is a new verion of golangci-lint. +# To obtain the checksum, download the install script and run the following +# command: +# > sha256sum +INSTALL_CHECKSUM=0e09dedc7e35f511b7924b885e50d7fe48eef25bec78c86f22f5b5abd24976cc + +if [[ ! -f .golangci.yml ]]; then + echo 'ERROR: missing .golangci.yml in repo root' >&2 + exit 1 +fi + +if ! command -v golangci-lint; then + INSTALL_SCRIPT=$(mktemp -d)/install.sh + curl -sfL $URL >$INSTALL_SCRIPT + if echo "${INSTALL_CHECKSUM} $INSTALL_SCRIPT" | sha256sum --check; then + chmod 755 $INSTALL_SCRIPT + $INSTALL_SCRIPT -b /tmp $VERSION + export PATH=$PATH:/tmp + pwd + else + echo 'ERROR: install script sha256 checksum invalid' >&2 + exit 1 + fi +fi + +golangci-lint version + +error=0 +while read -r i; do + echo "Checking golangci-lint for $i" + pushd "$i" + golangci-lint run ./... || error=1 + popd +done <<< "$(find . -name go.mod -exec dirname {} \;)" + +exit $error diff --git a/pkg/duplex/server.go b/pkg/duplex/server.go new file mode 100644 index 0000000..bd59049 --- /dev/null +++ b/pkg/duplex/server.go @@ -0,0 +1,126 @@ +/* +Copyright 2022 KhulnaSoft, Ltd. +SPDX-License-Identifier: Apache-2.0 +*/ + +package duplex + +import ( + "context" + "fmt" + "net" + "net/http" + "strings" + "time" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "google.golang.org/grpc" + + "go.khulnasoft.com/go-grpc-kit/pkg/metrics" +) + +// grpcHandlerFunc routes inbound requests to either the passed gRPC server or +// the http handler based on the request content type. +// See also, https://grpc-ecosystem.github.io/grpc-gateway/ +// This is based on: https://github.com/philips/grpc-gateway-example/issues/22#issuecomment-490733965 +func grpcHandlerFunc(grpcServer *grpc.Server, httpHandler http.Handler) http.Handler { + return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") { + grpcServer.ServeHTTP(w, r) + } else { + httpHandler.ServeHTTP(w, r) + } + }), &http2.Server{}) +} + +// Duplex is a wrapper for the gRPC server, gRPC HTTP Gateway MUX and options. +type Duplex struct { + Server *grpc.Server + MUX *runtime.ServeMux + Loopback string + Host string + Port int + DialOptions []grpc.DialOption +} + +type RegisterHandlerFromEndpointFn func(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error + +// New creates a Duplex gRPC server / gRPC HTTP Gateway. New takes in options +// for `grpc.NewServer`, typed `grpc.ServerOption`, and `runtime.NewServeMux`, +// typed `runtime.ServeMuxOption`. Unknown opts will cause a panic. +func New(port int, opts ...interface{}) *Duplex { + // Split out the options into their types. + var ( + gOpts []grpc.ServerOption + dOpts []grpc.DialOption + mOpts []runtime.ServeMuxOption + ) + for _, o := range opts { + switch opt := o.(type) { + case grpc.ServerOption: + gOpts = append(gOpts, opt) + case runtime.ServeMuxOption: + mOpts = append(mOpts, opt) + case grpc.DialOption: + dOpts = append(dOpts, opt) + default: + panic(fmt.Errorf("unknown type: %T", o)) + } + } + + // Create the Duplex Server. + d := &Duplex{ + Server: grpc.NewServer(gOpts...), + MUX: runtime.NewServeMux(mOpts...), + // The REST gateway translates the json to grpc and then dispatches to + // the appropriate method on this address, so we loopback to ourselves. + Loopback: fmt.Sprintf("localhost:%d", port), + Port: port, + DialOptions: dOpts, + } + return d +} + +// RegisterHandler is a helper registration handler to call the passed in +// `RegisterHandlerFromEndpointFn` with the correct options after `d.Server` +// has been registered with the implementation. Use like: +// ```go +// +// pb.RegisterServer(d.Server, impl.New()) +// if err := d.RegisterHandler(ctx, pb.RegisterHandlerFromEndpoint); err != nil { +// log.Panicf("Failed to register gateway endpoint: %v", err) +// } +// +// ``` +func (d *Duplex) RegisterHandler(ctx context.Context, fn RegisterHandlerFromEndpointFn) error { + return fn(ctx, d.MUX, d.Loopback, d.DialOptions) +} + +// ListenAndServe starts both the gRPC server and HTTP Gateway MUX. +// Note: This call is blocking. +func (d *Duplex) ListenAndServe(_ context.Context) error { + server := &http.Server{ + Addr: fmt.Sprintf("%s:%d", d.Host, d.Port), + Handler: grpcHandlerFunc(d.Server, d.MUX), + ReadHeaderTimeout: 600 * time.Second, + } + + return server.ListenAndServe() +} + +// ListenAndServe starts both the gRPC server and HTTP Gateway MUX on the given listener. +// Note: This call is blocking. +// #nosec G114 -- used only for testing tls. +// nolint:gosec +func (d *Duplex) Serve(_ context.Context, listener net.Listener) error { + return http.Serve(listener, grpcHandlerFunc(d.Server, d.MUX)) +} + +// RegisterListenAndServe initializes Prometheus metrics and starts a HTTP +// /metrics endpoint for exporting Prometheus metrics in the background. +// Call this *after* all services have been registered. +func (d *Duplex) RegisterListenAndServeMetrics(port int, enablePprof bool) { + metrics.RegisterListenAndServe(d.Server, fmt.Sprintf("%s:%d", d.Host, port), enablePprof) +} diff --git a/pkg/metrics/server.go b/pkg/metrics/server.go new file mode 100644 index 0000000..acc2e09 --- /dev/null +++ b/pkg/metrics/server.go @@ -0,0 +1,102 @@ +/* +Copyright 2022 KhulnaSoft, Ltd. +SPDX-License-Identifier: Apache-2.0 +*/ + +package metrics + +import ( + "context" + "log" + "net/http" + "net/http/pprof" + "time" + + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + "github.com/khulnasoft/clog" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + "google.golang.org/grpc" +) + +// Fractions >= 1 will always sample. Fractions < 0 are treated as zero. To +// respect the parent trace's `SampledFlag`, the `TraceIDRatioBased` sampler +// should be used as a delegate of a `Parent` sampler. +// +// Expected usage: +// +// defer metrics.SetupTracer(ctx)() +func SetupTracer(ctx context.Context) func() { + logger := clog.FromContext(ctx) + + traceExporter, err := otlptracegrpc.New(ctx) + if err != nil { + logger.Errorf("SetupTracer() = %v", err) + panic(err) + } + bsp := trace.NewBatchSpanProcessor(traceExporter) + res := resource.Default() + + tp := trace.NewTracerProvider( + trace.WithResource(res), + trace.WithSpanProcessor(bsp), + ) + otel.SetTracerProvider(tp) + + prp := propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) + otel.SetTextMapPropagator(prp) + + return func() { + if err := tp.Shutdown(context.Background()); err != nil { + logger.Infof("Error shutting down tracer provider: %v", err) + } + } +} + +func RegisterListenAndServe(server *grpc.Server, listenAddr string, enablePprof bool) { + grpc_prometheus.Register(server) + grpc_prometheus.EnableHandlingTimeHistogram( + grpc_prometheus.WithHistogramBuckets( + []float64{0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 600}, + ), + ) + + go func(addr string) { + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + + if enablePprof { + // pprof handles + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs")) + mux.Handle("/debug/pprof/block", pprof.Handler("block")) + mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine")) + mux.Handle("/debug/pprof/heap", pprof.Handler("heap")) + mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex")) + mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate")) + + log.Println("registering handle for /debug/pprof") + } + + server := &http.Server{ + Addr: addr, + Handler: mux, + ReadHeaderTimeout: 600 * time.Second, + } + + if err := server.ListenAndServe(); err != nil { + log.Fatalf("listen and server for http /metrics = %v", err) + } + }(listenAddr) +} diff --git a/pkg/options/options.go b/pkg/options/options.go new file mode 100644 index 0000000..0f5b135 --- /dev/null +++ b/pkg/options/options.go @@ -0,0 +1,182 @@ +/* +Copyright 2022 KhulnaSoft, Ltd. +SPDX-License-Identifier: Apache-2.0 +*/ + +package options + +import ( + "context" + "crypto/rand" + "crypto/tls" + "fmt" + "math" + "math/big" + "net" + "net/url" + "sync" + "time" + + grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + "github.com/kelseyhightower/envconfig" + "github.com/khulnasoft/clog" + "go.khulnasoft.com/go-grpc-kit/pkg/trace" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +type envStruct struct { + EnableClientHandlingTimeHistogram bool `envconfig:"ENABLE_CLIENT_HANDLING_TIME_HISTOGRAM" default:"true"` + EnableClientStreamReceiveTimeHistogram bool `envconfig:"ENABLE_CLIENT_STREAM_RECEIVE_TIME_HISTOGRAM" default:"true"` + EnableClientStreamSendTimeHistogram bool `envconfig:"ENABLE_CLIENT_STREAM_SEND_TIME_HISTOGRAM" default:"true"` + GrpcClientMaxRetry uint `envconfig:"GRPC_CLIENT_MAX_RETRY" default:"0"` +} + +var ( + envOnce sync.Once + + env envStruct +) + +// Parse these lazily, to allow clients to set their own in their main() or init(). +func getEnv() *envStruct { + envOnce.Do(func() { + logger := clog.FromContext(context.Background()) + if err := envconfig.Process("", &env); err != nil { + logger.Warn("Failed to process environment variables", "error", err) + } + }) + return &env +} + +// ListenerForTest is to support bufnet in our testing. +var ListenerForTest DialableListener + +type DialableListener interface { + net.Listener + + Dial() (net.Conn, error) +} + +var listenersForTest sync.Map + +// Register a test listener and get a provided scheme. +func RegisterListenerForTest(listener DialableListener) string { + for { + val, err := rand.Int(rand.Reader, big.NewInt(int64(math.MaxInt64))) + if err != nil { + panic(err) + } + scheme := fmt.Sprintf("test%d", val.Int64()) + if _, conflicted := listenersForTest.LoadOrStore(scheme, listener); !conflicted { + return scheme + } + } +} + +// Unregister a test listener. +func UnregisterTestListener(scheme string) { + listenersForTest.Delete(scheme) +} + +func getTestListener(scheme string) (DialableListener, bool) { + v, ok := listenersForTest.Load(scheme) + if !ok { + return nil, ok + } + return v.(DialableListener), true +} + +// These are defined as global variables, so that folks can expose them as flags +// in their entrypoints, if they choose. +var ( + RecvMsgSize = 100 * 1024 * 1024 // 100MB + SendMsgSize = 100 * 1024 * 1024 // 100MB +) + +func enableClientTimeHistogram() { + hopt := grpc_prometheus.WithHistogramBuckets( + []float64{0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 600}, + ) + + if getEnv().EnableClientHandlingTimeHistogram { + grpc_prometheus.EnableClientHandlingTimeHistogram(hopt) + } + if getEnv().EnableClientStreamReceiveTimeHistogram { + grpc_prometheus.EnableClientStreamReceiveTimeHistogram(hopt) + } + if getEnv().EnableClientStreamSendTimeHistogram { + grpc_prometheus.EnableClientStreamSendTimeHistogram(hopt) + } +} + +func GRPCOptions(delegate url.URL) (string, []grpc.DialOption) { + retryOpts := []grpc_retry.CallOption{ + grpc_retry.WithBackoff(grpc_retry.BackoffExponential(100 * time.Millisecond)), + grpc_retry.WithMax(getEnv().GrpcClientMaxRetry), + } + + switch delegate.Scheme { + case "http": + port := "80" + // Explicit port from the user signifies we should override the scheme-based defaults. + if delegate.Port() != "" { + port = delegate.Port() + } + enableClientTimeHistogram() + return net.JoinHostPort(delegate.Hostname(), port), []grpc.DialOption{ + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), + grpc.WithStatsHandler(trace.PreserveTraceParentHandler), + grpc.WithChainUnaryInterceptor(grpc_prometheus.UnaryClientInterceptor, grpc_retry.UnaryClientInterceptor(retryOpts...)), + grpc.WithChainStreamInterceptor(grpc_prometheus.StreamClientInterceptor, grpc_retry.StreamClientInterceptor(retryOpts...)), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(RecvMsgSize), + grpc.MaxCallSendMsgSize(SendMsgSize), + ), + } + case "https": + port := "443" + // Explicit port from the user signifies we should override the scheme-based defaults. + if delegate.Port() != "" { + port = delegate.Port() + } + enableClientTimeHistogram() + return net.JoinHostPort(delegate.Hostname(), port), []grpc.DialOption{ + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), + grpc.WithStatsHandler(trace.PreserveTraceParentHandler), + grpc.WithChainUnaryInterceptor(grpc_prometheus.UnaryClientInterceptor, grpc_retry.UnaryClientInterceptor(retryOpts...)), + grpc.WithChainStreamInterceptor(grpc_prometheus.StreamClientInterceptor, grpc_retry.StreamClientInterceptor(retryOpts...)), + grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + MinVersion: tls.VersionTLS12, + })), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(RecvMsgSize), + grpc.MaxCallSendMsgSize(SendMsgSize), + ), + } + + case "bufnet": // This is to support testing, it will not pass webhook validation. + return "bufnet", []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return ListenerForTest.Dial() + }), + } + + default: + listener, ok := getTestListener(delegate.Scheme) + if !ok { + panic("unreachable for valid delegates.") + } + return delegate.Scheme, []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return listener.Dial() + }), + } + } +} diff --git a/pkg/options/options_test.go b/pkg/options/options_test.go new file mode 100644 index 0000000..bd9fb3a --- /dev/null +++ b/pkg/options/options_test.go @@ -0,0 +1,38 @@ +/* +Copyright 2022 KhulnaSoft, Ltd. +SPDX-License-Identifier: Apache-2.0 +*/ + +package options + +import ( + "os" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestGetEnv(t *testing.T) { + old := os.Getenv("GRPC_CLIENT_MAX_RETRY") + defer os.Setenv("GRPC_CLIENT_MAX_RETRY", old) + + want := &envStruct{ + EnableClientHandlingTimeHistogram: true, + EnableClientStreamReceiveTimeHistogram: true, + EnableClientStreamSendTimeHistogram: true, + GrpcClientMaxRetry: 42, + } + t.Run("can change env right before usage", func(t *testing.T) { + os.Setenv("GRPC_CLIENT_MAX_RETRY", strconv.Itoa(int(want.GrpcClientMaxRetry))) + if diff := cmp.Diff(want, getEnv()); diff != "" { + t.Errorf("getEnv() -want,+got: %s", diff) + } + }) + t.Run("but cannot change after usage", func(t *testing.T) { + os.Setenv("GRPC_CLIENT_MAX_RETRY", strconv.Itoa(int(want.GrpcClientMaxRetry+10))) + if diff := cmp.Diff(want, getEnv()); diff != "" { + t.Errorf("getEnv() -want,+got: %s", diff) + } + }) +} diff --git a/pkg/tls/internal/proto/helloworld/hello_world.pb.go b/pkg/tls/internal/proto/helloworld/hello_world.pb.go new file mode 100755 index 0000000..b5018b2 --- /dev/null +++ b/pkg/tls/internal/proto/helloworld/hello_world.pb.go @@ -0,0 +1,222 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.18.0 +// source: helloworld/hello_world.proto + +package hello_world + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// The request message containing the user's name +type HelloRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *HelloRequest) Reset() { + *x = HelloRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_helloworld_hello_world_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloRequest) ProtoMessage() {} + +func (x *HelloRequest) ProtoReflect() protoreflect.Message { + mi := &file_helloworld_hello_world_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. +func (*HelloRequest) Descriptor() ([]byte, []int) { + return file_helloworld_hello_world_proto_rawDescGZIP(), []int{0} +} + +func (x *HelloRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// The response message containing the greetings +type HelloReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *HelloReply) Reset() { + *x = HelloReply{} + if protoimpl.UnsafeEnabled { + mi := &file_helloworld_hello_world_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HelloReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloReply) ProtoMessage() {} + +func (x *HelloReply) ProtoReflect() protoreflect.Message { + mi := &file_helloworld_hello_world_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloReply.ProtoReflect.Descriptor instead. +func (*HelloReply) Descriptor() ([]byte, []int) { + return file_helloworld_hello_world_proto_rawDescGZIP(), []int{1} +} + +func (x *HelloReply) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_helloworld_hello_world_proto protoreflect.FileDescriptor + +var file_helloworld_hello_world_proto_rawDesc = []byte{ + 0x0a, 0x1c, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2f, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x5f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, + 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x32, 0x64, 0x0a, 0x07, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, + 0x59, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, + 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, + 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x1b, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x22, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2f, 0x65, 0x63, 0x68, 0x6f, 0x3a, 0x01, 0x2a, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x61, 0x6d, 0x72, 0x61, 0x6a, 0x69, + 0x76, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2d, 0x67, 0x72, 0x70, + 0x63, 0x2d, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, + 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x5f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_helloworld_hello_world_proto_rawDescOnce sync.Once + file_helloworld_hello_world_proto_rawDescData = file_helloworld_hello_world_proto_rawDesc +) + +func file_helloworld_hello_world_proto_rawDescGZIP() []byte { + file_helloworld_hello_world_proto_rawDescOnce.Do(func() { + file_helloworld_hello_world_proto_rawDescData = protoimpl.X.CompressGZIP(file_helloworld_hello_world_proto_rawDescData) + }) + return file_helloworld_hello_world_proto_rawDescData +} + +var file_helloworld_hello_world_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_helloworld_hello_world_proto_goTypes = []interface{}{ + (*HelloRequest)(nil), // 0: helloworld.HelloRequest + (*HelloReply)(nil), // 1: helloworld.HelloReply +} +var file_helloworld_hello_world_proto_depIdxs = []int32{ + 0, // 0: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest + 1, // 1: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_helloworld_hello_world_proto_init() } +func file_helloworld_hello_world_proto_init() { + if File_helloworld_hello_world_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_helloworld_hello_world_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HelloRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_helloworld_hello_world_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HelloReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_helloworld_hello_world_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_helloworld_hello_world_proto_goTypes, + DependencyIndexes: file_helloworld_hello_world_proto_depIdxs, + MessageInfos: file_helloworld_hello_world_proto_msgTypes, + }.Build() + File_helloworld_hello_world_proto = out.File + file_helloworld_hello_world_proto_rawDesc = nil + file_helloworld_hello_world_proto_goTypes = nil + file_helloworld_hello_world_proto_depIdxs = nil +} diff --git a/pkg/tls/internal/proto/helloworld/hello_world.pb.gw.go b/pkg/tls/internal/proto/helloworld/hello_world.pb.gw.go new file mode 100755 index 0000000..f3bf079 --- /dev/null +++ b/pkg/tls/internal/proto/helloworld/hello_world.pb.gw.go @@ -0,0 +1,167 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: helloworld/hello_world.proto + +/* +Package hello_world is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package hello_world + +import ( + "context" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray +var _ = metadata.Join + +func request_Greeter_SayHello_0(ctx context.Context, marshaler runtime.Marshaler, client GreeterClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq HelloRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.SayHello(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Greeter_SayHello_0(ctx context.Context, marshaler runtime.Marshaler, server GreeterServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq HelloRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.SayHello(ctx, &protoReq) + return msg, metadata, err + +} + +// RegisterGreeterHandlerServer registers the http handlers for service Greeter to "mux". +// UnaryRPC :call GreeterServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterGreeterHandlerFromEndpoint instead. +func RegisterGreeterHandlerServer(ctx context.Context, mux *runtime.ServeMux, server GreeterServer) error { + + mux.Handle("POST", pattern_Greeter_SayHello_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/helloworld.Greeter/SayHello", runtime.WithHTTPPathPattern("/v1/example/echo")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Greeter_SayHello_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Greeter_SayHello_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +// RegisterGreeterHandlerFromEndpoint is same as RegisterGreeterHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterGreeterHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.Dial(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterGreeterHandler(ctx, mux, conn) +} + +// RegisterGreeterHandler registers the http handlers for service Greeter to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterGreeterHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterGreeterHandlerClient(ctx, mux, NewGreeterClient(conn)) +} + +// RegisterGreeterHandlerClient registers the http handlers for service Greeter +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "GreeterClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "GreeterClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "GreeterClient" to call the correct interceptors. +func RegisterGreeterHandlerClient(ctx context.Context, mux *runtime.ServeMux, client GreeterClient) error { + + mux.Handle("POST", pattern_Greeter_SayHello_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/helloworld.Greeter/SayHello", runtime.WithHTTPPathPattern("/v1/example/echo")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Greeter_SayHello_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Greeter_SayHello_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_Greeter_SayHello_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "example", "echo"}, "")) +) + +var ( + forward_Greeter_SayHello_0 = runtime.ForwardResponseMessage +) diff --git a/pkg/tls/internal/proto/helloworld/hello_world.proto b/pkg/tls/internal/proto/helloworld/hello_world.proto new file mode 100755 index 0000000..acdc241 --- /dev/null +++ b/pkg/tls/internal/proto/helloworld/hello_world.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package helloworld; + +import "google/api/annotations.proto"; + +// Defines the import path that should be used to import the generated package +// and the package name +option go_package = "github.com/iamrajiv/helloworld-grpc-gateway/proto;hello_world"; + +// Here is the overall greeting service definition where we define all our endpoints +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + post: "/v1/example/echo" + body: "*" + }; + } +} + +// The request message containing the user's name +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/pkg/tls/internal/proto/helloworld/hello_world_grpc.pb.go b/pkg/tls/internal/proto/helloworld/hello_world_grpc.pb.go new file mode 100755 index 0000000..f718b14 --- /dev/null +++ b/pkg/tls/internal/proto/helloworld/hello_world_grpc.pb.go @@ -0,0 +1,105 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.18.0 +// source: helloworld/hello_world.proto + +package hello_world + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// GreeterClient is the client API for Greeter service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GreeterClient interface { + // Sends a greeting + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) +} + +type greeterClient struct { + cc grpc.ClientConnInterface +} + +func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient { + return &greeterClient{cc} +} + +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { + out := new(HelloReply) + err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GreeterServer is the server API for Greeter service. +// All implementations should embed UnimplementedGreeterServer +// for forward compatibility +type GreeterServer interface { + // Sends a greeting + SayHello(context.Context, *HelloRequest) (*HelloReply, error) +} + +// UnimplementedGreeterServer should be embedded to have forward compatible implementations. +type UnimplementedGreeterServer struct { +} + +func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") +} + +// UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GreeterServer will +// result in compilation errors. +type UnsafeGreeterServer interface { + mustEmbedUnimplementedGreeterServer() +} + +func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) { + s.RegisterService(&Greeter_ServiceDesc, srv) +} + +func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GreeterServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/helloworld.Greeter/SayHello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Greeter_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "helloworld.Greeter", + HandlerType: (*GreeterServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _Greeter_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "helloworld/hello_world.proto", +} diff --git a/pkg/tls/tls_test.go b/pkg/tls/tls_test.go new file mode 100644 index 0000000..3dddaad --- /dev/null +++ b/pkg/tls/tls_test.go @@ -0,0 +1,171 @@ +/* +Copyright 2022 KhulnaSoft, Ltd. +SPDX-License-Identifier: Apache-2.0 +*/ + +package tls + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "log" + "math/big" + "net" + "net/http" + "testing" + "time" + + "go.khulnasoft.com/go-grpc-kit/pkg/duplex" + pb "go.khulnasoft.com/go-grpc-kit/pkg/tls/internal/proto/helloworld" + "golang.org/x/net/http2" + "golang.org/x/oauth2" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/oauth" + "google.golang.org/grpc/metadata" +) + +func TestTLS(t *testing.T) { + ctx := context.Background() + + // We need to init the listener first so that we can generate a TLS cert + // that's bound to the listening IP. + lis, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + t.Logf("server listening at %v", lis.Addr()) + + // Get IP address from listener + ip, err := net.ResolveTCPAddr(lis.Addr().Network(), lis.Addr().String()) + if err != nil { + t.Fatalf("error resolving IP address: %v", err) + } + + tlsConfig, err := generateTLS(&x509.Certificate{ + SerialNumber: big.NewInt(1), + NotAfter: time.Now().Add(10 * time.Hour), + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{ip.IP}, + }) + if err != nil { + t.Fatalf("error generating certificate") + } + + // Start server + d := duplex.New(ip.Port, + grpc.Creds(credentials.NewTLS(tlsConfig)), + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), + ) + pb.RegisterGreeterServer(d.Server, &server{}) + if err := d.RegisterHandler(ctx, pb.RegisterGreeterHandlerFromEndpoint); err != nil { + t.Fatalf("error registering handler: %v", err) + } + go func() { + if err := d.Serve(ctx, tls.NewListener(lis, tlsConfig)); err != nil { + panic(fmt.Sprintf("failed to serve: %v", err)) + } + }() + + // grpc client + conn, err := grpc.NewClient(lis.Addr().String(), + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), + // Send password to verify gRPC doesn't reject it. + grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "hunter2"})}), + ) + if err != nil { + log.Fatalf("failed to dial: %v", err) + } + client := pb.NewGreeterClient(conn) + req := &pb.HelloRequest{ + Name: "world", + } + resp, err := client.SayHello(ctx, req) + if err != nil { + t.Fatalf("grpc request failed: %v", err) + } + t.Log("grpc response:", resp) + + // http client + httpClient := &http.Client{ + Transport: &http2.Transport{ + TLSClientConfig: tlsConfig, + }, + } + body, err := json.Marshal(req) + if err != nil { + t.Fatalf("failed to marshal json: %v", err) + } + url := "https://" + lis.Addr().String() + "/v1/example/echo" + t.Log(url) + httpResp, err := httpClient.Post(url, "application/json", bytes.NewBuffer(body)) + if err != nil { + t.Fatalf("http request failed: %v", err) + } + if httpResp.StatusCode != 200 { + t.Fatalf("non-zero HTTP response code: %s", httpResp.Status) + } + b, _ := io.ReadAll(httpResp.Body) + t.Log("http response:", string(b)) +} + +// Generate TLS creates a TLS config with a self-signed cert based on tmpl. +func generateTLS(tmpl *x509.Certificate) (*tls.Config, error) { + priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("error generating private key: %w", err) + } + pub := &priv.PublicKey + raw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, priv) + if err != nil { + return nil, fmt.Errorf("error generating certificate: %w", err) + } + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: raw, + }) + keyBytes, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + return nil, fmt.Errorf("error marshaling key bytes: %w", err) + } + keyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + }) + tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + return nil, fmt.Errorf("error loading tls certificate: %w", err) + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(certPEM) { + return nil, fmt.Errorf("error adding cert to pool") + } + + // configuration of the certificate what we want to + return &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + NextProtos: []string{"h2"}, // Specify ALPN protocol + RootCAs: pool, + }, nil +} + +// server is used to implement helloworld.GreeterServer. +type server struct { + pb.UnimplementedGreeterServer +} + +// SayHello implements helloworld.GreeterServer +func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { + md, _ := metadata.FromIncomingContext(ctx) + log.Printf("Received: %v (%v)", in.GetName(), md) + return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil +} diff --git a/pkg/trace/preserve.go b/pkg/trace/preserve.go new file mode 100644 index 0000000..6589e1c --- /dev/null +++ b/pkg/trace/preserve.go @@ -0,0 +1,57 @@ +/* +Copyright 2024 KhulnaSoft, Ltd. +SPDX-License-Identifier: Apache-2.0 +*/ + +package trace + +import ( + "context" + + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/stats" +) + +const ( + OriginalTraceParentHeader string = "original-traceparent" + TraceParentHeader string = "traceparent" +) + +var ( + // PreserveTraceParentHandler is a client stats.Handler that preserves the original + // traceparent header in the outgoing context, with a different header name. + // + // This is useful when the next hop in the request chain (like Cloud Run) may lose span + // information, and become an unreliable span. In those cases, we just use the original + // traceparent header to associate child spans directly with the outgoing span here. + PreserveTraceParentHandler stats.Handler = &preserveTraceParentHandler{} +) + +type preserveTraceParentHandler struct{} + +// TagRPC implements stats.Handler interface. +func (*preserveTraceParentHandler) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context { + md, ok := metadata.FromOutgoingContext(ctx) + if !ok { + md = metadata.MD{} + } + if tp := md.Get(TraceParentHeader); len(tp) > 0 { + md.Set(OriginalTraceParentHeader, tp...) + } + return metadata.NewOutgoingContext(ctx, md) +} + +// HandleRPC implments stats.Handler interface. +func (*preserveTraceParentHandler) HandleRPC(context.Context, stats.RPCStats) { + // Do nothing +} + +// TagConn implements stats.Handler interface. +func (*preserveTraceParentHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { + return ctx +} + +// HandleConn processes the Conn stats. +func (*preserveTraceParentHandler) HandleConn(context.Context, stats.ConnStats) { + // Do nothing +} diff --git a/pkg/trace/restore.go b/pkg/trace/restore.go new file mode 100644 index 0000000..e2db264 --- /dev/null +++ b/pkg/trace/restore.go @@ -0,0 +1,49 @@ +/* +Copyright 2024 KhulnaSoft, Ltd. +SPDX-License-Identifier: Apache-2.0 +*/ + +package trace + +import ( + "context" + + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/stats" +) + +var ( + // RestoreTraceParentHandler is a server stats.Handler that restores the traceparent + // stored by the PreserveTraceParentHandler. + RestoreTraceParentHandler stats.Handler = &restoreTraceParentHandler{} +) + +type restoreTraceParentHandler struct{} + +// TagRPC implements stats.Handler. +func (r *restoreTraceParentHandler) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + md = metadata.MD{} + } + if tp := md.Get(OriginalTraceParentHeader); len(tp) > 0 { + md.Set(TraceParentHeader, tp...) + } + return metadata.NewIncomingContext(ctx, md) +} + +// HandleRPC implements stats.Handler. +func (r *restoreTraceParentHandler) HandleRPC(_ context.Context, _ stats.RPCStats) { + // Do nothing. +} + +// TagConn implements stats.Handler. +func (r *restoreTraceParentHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { + // Do nothing. + return ctx +} + +// HandleConn implements stats.Handler. +func (r *restoreTraceParentHandler) HandleConn(_ context.Context, _ stats.ConnStats) { + // Do nothing. +}