├── .codecov.yml ├── .conform.yaml ├── .dockerignore ├── .github ├── renovate.json └── workflows │ ├── ci.yaml │ └── slack-notify.yaml ├── .gitignore ├── .golangci.yml ├── .kres.yaml ├── .markdownlint.json ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── hack ├── git-chglog │ ├── CHANGELOG.tpl.md │ └── config.yaml ├── release.sh └── release.toml ├── proxy ├── codec.go ├── codec_test.go ├── director.go ├── doc.go ├── examples_test.go ├── handler.go ├── handler_one2many.go ├── handler_one2many_test.go ├── handler_one2one.go ├── handler_one2one_test.go ├── proxy.go └── serverstream.go └── testservice ├── api └── test.proto ├── test.pb.go ├── test.proto └── test_grpc.pb.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2022-06-06T17:35:55Z by kres 65530e7. 4 | 5 | codecov: 6 | require_ci_to_pass: false 7 | 8 | coverage: 9 | status: 10 | project: 11 | default: 12 | target: 50% 13 | threshold: 0.5% 14 | base: auto 15 | if_ci_failed: success 16 | patch: off 17 | 18 | comment: false 19 | -------------------------------------------------------------------------------- /.conform.yaml: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2024-05-27T14:20:53Z by kres b5844f8. 4 | 5 | policies: 6 | - type: commit 7 | spec: 8 | dco: true 9 | gpg: 10 | required: true 11 | identity: 12 | gitHubOrganization: siderolabs 13 | spellcheck: 14 | locale: US 15 | maximumOfOneCommit: true 16 | header: 17 | length: 89 18 | imperative: true 19 | case: lower 20 | invalidLastCharacters: . 21 | body: 22 | required: true 23 | conventional: 24 | types: 25 | - chore 26 | - docs 27 | - perf 28 | - refactor 29 | - style 30 | - test 31 | - release 32 | scopes: 33 | - .* 34 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2024-05-08T10:44:06Z by kres d15226e-dirty. 4 | 5 | * 6 | !proxy 7 | !testservice 8 | !go.mod 9 | !go.sum 10 | !.golangci.yml 11 | !README.md 12 | !.markdownlint.json 13 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "description": "THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.", 4 | "prHeader": "Update Request | Renovate Bot", 5 | "extends": [ 6 | ":dependencyDashboard", 7 | ":gitSignOff", 8 | ":semanticCommitScopeDisabled", 9 | "schedule:earlyMondays" 10 | ], 11 | "packageRules": [ 12 | { 13 | "groupName": "dependencies", 14 | "matchUpdateTypes": [ 15 | "major", 16 | "minor", 17 | "patch", 18 | "pin", 19 | "digest" 20 | ] 21 | }, 22 | { 23 | "enabled": false, 24 | "matchFileNames": [ 25 | "Dockerfile" 26 | ] 27 | }, 28 | { 29 | "enabled": false, 30 | "matchFileNames": [ 31 | ".github/workflows/*.yaml" 32 | ] 33 | } 34 | ], 35 | "separateMajorMinor": false 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2025-01-22T20:13:50Z by kres 3075de9. 4 | 5 | name: default 6 | concurrency: 7 | group: ${{ github.head_ref || github.run_id }} 8 | cancel-in-progress: true 9 | "on": 10 | push: 11 | branches: 12 | - main 13 | - release-* 14 | tags: 15 | - v* 16 | pull_request: 17 | branches: 18 | - main 19 | - release-* 20 | jobs: 21 | default: 22 | permissions: 23 | actions: read 24 | contents: write 25 | issues: read 26 | packages: write 27 | pull-requests: read 28 | runs-on: 29 | - self-hosted 30 | - generic 31 | if: (!startsWith(github.head_ref, 'renovate/') && !startsWith(github.head_ref, 'dependabot/')) 32 | steps: 33 | - name: gather-system-info 34 | id: system-info 35 | uses: kenchan0130/actions-system-info@v1.3.0 36 | continue-on-error: true 37 | - name: print-system-info 38 | run: | 39 | MEMORY_GB=$((${{ steps.system-info.outputs.totalmem }}/1024/1024/1024)) 40 | 41 | OUTPUTS=( 42 | "CPU Core: ${{ steps.system-info.outputs.cpu-core }}" 43 | "CPU Model: ${{ steps.system-info.outputs.cpu-model }}" 44 | "Hostname: ${{ steps.system-info.outputs.hostname }}" 45 | "NodeName: ${NODE_NAME}" 46 | "Kernel release: ${{ steps.system-info.outputs.kernel-release }}" 47 | "Kernel version: ${{ steps.system-info.outputs.kernel-version }}" 48 | "Name: ${{ steps.system-info.outputs.name }}" 49 | "Platform: ${{ steps.system-info.outputs.platform }}" 50 | "Release: ${{ steps.system-info.outputs.release }}" 51 | "Total memory: ${MEMORY_GB} GB" 52 | ) 53 | 54 | for OUTPUT in "${OUTPUTS[@]}";do 55 | echo "${OUTPUT}" 56 | done 57 | continue-on-error: true 58 | - name: checkout 59 | uses: actions/checkout@v4 60 | - name: Unshallow 61 | run: | 62 | git fetch --prune --unshallow 63 | - name: Set up Docker Buildx 64 | id: setup-buildx 65 | uses: docker/setup-buildx-action@v3 66 | with: 67 | driver: remote 68 | endpoint: tcp://buildkit-amd64.ci.svc.cluster.local:1234 69 | timeout-minutes: 10 70 | - name: base 71 | run: | 72 | make base 73 | - name: unit-tests 74 | run: | 75 | make unit-tests 76 | - name: unit-tests-race 77 | run: | 78 | make unit-tests-race 79 | - name: coverage 80 | uses: codecov/codecov-action@v5 81 | with: 82 | files: _out/coverage-unit-tests.txt 83 | token: ${{ secrets.CODECOV_TOKEN }} 84 | timeout-minutes: 3 85 | - name: lint 86 | run: | 87 | make lint 88 | - name: release-notes 89 | if: startsWith(github.ref, 'refs/tags/') 90 | run: | 91 | make release-notes 92 | - name: Release 93 | if: startsWith(github.ref, 'refs/tags/') 94 | uses: crazy-max/ghaction-github-release@v2 95 | with: 96 | body_path: _out/RELEASE_NOTES.md 97 | draft: "true" 98 | -------------------------------------------------------------------------------- /.github/workflows/slack-notify.yaml: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2025-01-22T20:13:50Z by kres 3075de9. 4 | 5 | name: slack-notify 6 | "on": 7 | workflow_run: 8 | workflows: 9 | - default 10 | types: 11 | - completed 12 | jobs: 13 | slack-notify: 14 | runs-on: 15 | - self-hosted 16 | - generic 17 | if: github.event.workflow_run.conclusion != 'skipped' 18 | steps: 19 | - name: Get PR number 20 | id: get-pr-number 21 | if: github.event.workflow_run.event == 'pull_request' 22 | env: 23 | GH_TOKEN: ${{ github.token }} 24 | run: | 25 | echo pull_request_number=$(gh pr view -R ${{ github.repository }} ${{ github.event.workflow_run.head_repository.owner.login }}:${{ github.event.workflow_run.head_branch }} --json number --jq .number) >> $GITHUB_OUTPUT 26 | - name: Slack Notify 27 | uses: slackapi/slack-github-action@v2 28 | with: 29 | method: chat.postMessage 30 | payload: | 31 | { 32 | "channel": "proj-talos-maintainers", 33 | "attachments": [ 34 | { 35 | "color": "${{ github.event.workflow_run.conclusion == 'success' && '#2EB886' || github.event.workflow_run.conclusion == 'failure' && '#A30002' || '#FFCC00' }}", 36 | "fallback": "test", 37 | "blocks": [ 38 | { 39 | "type": "section", 40 | "fields": [ 41 | { 42 | "type": "mrkdwn", 43 | "text": "${{ github.event.workflow_run.event == 'pull_request' && format('*Pull Request:* {0} (`{1}`)\n<{2}/pull/{3}|{4}>', github.repository, github.ref_name, github.event.repository.html_url, steps.get-pr-number.outputs.pull_request_number, github.event.workflow_run.display_title) || format('*Build:* {0} (`{1}`)\n<{2}/commit/{3}|{4}>', github.repository, github.ref_name, github.event.repository.html_url, github.sha, github.event.workflow_run.display_title) }}" 44 | }, 45 | { 46 | "type": "mrkdwn", 47 | "text": "*Status:*\n`${{ github.event.workflow_run.conclusion }}`" 48 | } 49 | ] 50 | }, 51 | { 52 | "type": "section", 53 | "fields": [ 54 | { 55 | "type": "mrkdwn", 56 | "text": "*Author:*\n`${{ github.actor }}`" 57 | }, 58 | { 59 | "type": "mrkdwn", 60 | "text": "*Event:*\n`${{ github.event.workflow_run.event }}`" 61 | } 62 | ] 63 | }, 64 | { 65 | "type": "divider" 66 | }, 67 | { 68 | "type": "actions", 69 | "elements": [ 70 | { 71 | "type": "button", 72 | "text": { 73 | "type": "plain_text", 74 | "text": "Logs" 75 | }, 76 | "url": "${{ github.event.workflow_run.html_url }}" 77 | }, 78 | { 79 | "type": "button", 80 | "text": { 81 | "type": "plain_text", 82 | "text": "Commit" 83 | }, 84 | "url": "${{ github.event.repository.html_url }}/commit/${{ github.sha }}" 85 | } 86 | ] 87 | } 88 | ] 89 | } 90 | ] 91 | } 92 | token: ${{ secrets.SLACK_BOT_TOKEN }} 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2021-03-25T21:15:43Z by kres 424ae88-dirty. 4 | 5 | _out 6 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2025-01-22T20:13:50Z by kres 3075de9. 4 | 5 | # options for analysis running 6 | run: 7 | timeout: 10m 8 | issues-exit-code: 1 9 | tests: true 10 | build-tags: [ ] 11 | modules-download-mode: readonly 12 | 13 | # output configuration options 14 | output: 15 | formats: 16 | - format: colored-line-number 17 | path: stdout 18 | print-issued-lines: true 19 | print-linter-name: true 20 | path-prefix: "" 21 | 22 | # all available settings of specific linters 23 | linters-settings: 24 | dogsled: 25 | max-blank-identifiers: 2 26 | dupl: 27 | threshold: 150 28 | errcheck: 29 | check-type-assertions: true 30 | check-blank: true 31 | exhaustive: 32 | default-signifies-exhaustive: false 33 | gci: 34 | sections: 35 | - standard # Standard section: captures all standard packages. 36 | - default # Default section: contains all imports that could not be matched to another section type. 37 | - localmodule # Imports from the same module. 38 | gocognit: 39 | min-complexity: 30 40 | nestif: 41 | min-complexity: 5 42 | goconst: 43 | min-len: 3 44 | min-occurrences: 3 45 | gocritic: 46 | disabled-checks: [ ] 47 | gocyclo: 48 | min-complexity: 20 49 | godot: 50 | scope: declarations 51 | gofmt: 52 | simplify: true 53 | gomodguard: { } 54 | govet: 55 | enable-all: true 56 | lll: 57 | line-length: 200 58 | tab-width: 4 59 | misspell: 60 | locale: US 61 | ignore-words: [ ] 62 | nakedret: 63 | max-func-lines: 30 64 | prealloc: 65 | simple: true 66 | range-loops: true # Report preallocation suggestions on range loops, true by default 67 | for-loops: false # Report preallocation suggestions on for loops, false by default 68 | nolintlint: 69 | allow-unused: false 70 | allow-no-explanation: [ ] 71 | require-explanation: false 72 | require-specific: true 73 | rowserrcheck: { } 74 | testpackage: { } 75 | unparam: 76 | check-exported: false 77 | unused: 78 | local-variables-are-used: false 79 | whitespace: 80 | multi-if: false # Enforces newlines (or comments) after every multi-line if statement 81 | multi-func: false # Enforces newlines (or comments) after every multi-line function signature 82 | wsl: 83 | strict-append: true 84 | allow-assign-and-call: true 85 | allow-multiline-assign: true 86 | allow-cuddle-declarations: false 87 | allow-trailing-comment: false 88 | force-case-trailing-whitespace: 0 89 | force-err-cuddling: false 90 | allow-separated-leading-comment: false 91 | gofumpt: 92 | extra-rules: false 93 | cyclop: 94 | # the maximal code complexity to report 95 | max-complexity: 20 96 | depguard: 97 | rules: 98 | prevent_unmaintained_packages: 99 | list-mode: lax # allow unless explicitly denied 100 | files: 101 | - $all 102 | deny: 103 | - pkg: io/ioutil 104 | desc: "replaced by io and os packages since Go 1.16: https://tip.golang.org/doc/go1.16#ioutil" 105 | 106 | linters: 107 | enable-all: true 108 | disable-all: false 109 | fast: false 110 | disable: 111 | - exhaustruct 112 | - err113 113 | - forbidigo 114 | - funlen 115 | - gochecknoglobals 116 | - gochecknoinits 117 | - godox 118 | - gomoddirectives 119 | - gosec 120 | - inamedparam 121 | - ireturn 122 | - mnd 123 | - nestif 124 | - nonamedreturns 125 | - paralleltest 126 | - tagalign 127 | - tagliatelle 128 | - thelper 129 | - varnamelen 130 | - wrapcheck 131 | - testifylint # complains about our assert recorder and has a number of false positives for assert.Greater(t, thing, 1) 132 | - protogetter # complains about us using Value field on typed spec, instead of GetValue which has a different signature 133 | - perfsprint # complains about us using fmt.Sprintf in non-performance critical code, updating just kres took too long 134 | - goimports # same as gci 135 | - musttag # seems to be broken - goes into imported libraries and reports issues there 136 | - exportloopref # WARN The linter 'exportloopref' is deprecated (since v1.60.2) due to: Since Go1.22 (loopvar) this linter is no longer relevant. Replaced by copyloopvar. 137 | 138 | issues: 139 | exclude: [ ] 140 | exclude-rules: [ ] 141 | exclude-use-default: false 142 | exclude-case-sensitive: false 143 | max-issues-per-linter: 10 144 | max-same-issues: 3 145 | new: false 146 | uniq-by-line: true 147 | 148 | severity: 149 | default-severity: error 150 | case-sensitive: false 151 | -------------------------------------------------------------------------------- /.kres.yaml: -------------------------------------------------------------------------------- 1 | kind: common.Repository 2 | spec: 3 | conformLicenseCheck: false 4 | licenses: 5 | - enabled: false 6 | licenseChecks: [] 7 | --- 8 | kind: golang.Generate 9 | spec: 10 | baseSpecPath: /testservice 11 | specs: 12 | - source: testservice/api/test.proto 13 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2021-03-25T21:15:43Z by kres 424ae88-dirty. 4 | 5 | { 6 | "MD013": false, 7 | "MD033": false, 8 | "default": true 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile-upstream:1.12.1-labs 2 | 3 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 4 | # 5 | # Generated on 2025-01-22T20:13:50Z by kres 3075de9. 6 | 7 | ARG TOOLCHAIN 8 | 9 | # runs markdownlint 10 | FROM docker.io/oven/bun:1.1.43-alpine AS lint-markdown 11 | WORKDIR /src 12 | RUN bun i markdownlint-cli@0.43.0 sentences-per-line@0.3.0 13 | COPY .markdownlint.json . 14 | COPY ./README.md ./README.md 15 | RUN bunx markdownlint --ignore "CHANGELOG.md" --ignore "**/node_modules/**" --ignore '**/hack/chglog/**' --rules sentences-per-line . 16 | 17 | # collects proto specs 18 | FROM scratch AS proto-specs 19 | ADD testservice/api/test.proto /testservice/ 20 | 21 | # base toolchain image 22 | FROM --platform=${BUILDPLATFORM} ${TOOLCHAIN} AS toolchain 23 | RUN apk --update --no-cache add bash curl build-base protoc protobuf-dev 24 | 25 | # build tools 26 | FROM --platform=${BUILDPLATFORM} toolchain AS tools 27 | ENV GO111MODULE=on 28 | ARG CGO_ENABLED 29 | ENV CGO_ENABLED=${CGO_ENABLED} 30 | ARG GOTOOLCHAIN 31 | ENV GOTOOLCHAIN=${GOTOOLCHAIN} 32 | ARG GOEXPERIMENT 33 | ENV GOEXPERIMENT=${GOEXPERIMENT} 34 | ENV GOPATH=/go 35 | ARG GOIMPORTS_VERSION 36 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install golang.org/x/tools/cmd/goimports@v${GOIMPORTS_VERSION} 37 | RUN mv /go/bin/goimports /bin 38 | ARG PROTOBUF_GO_VERSION 39 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install google.golang.org/protobuf/cmd/protoc-gen-go@v${PROTOBUF_GO_VERSION} 40 | RUN mv /go/bin/protoc-gen-go /bin 41 | ARG GRPC_GO_VERSION 42 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v${GRPC_GO_VERSION} 43 | RUN mv /go/bin/protoc-gen-go-grpc /bin 44 | ARG GRPC_GATEWAY_VERSION 45 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v${GRPC_GATEWAY_VERSION} 46 | RUN mv /go/bin/protoc-gen-grpc-gateway /bin 47 | ARG DEEPCOPY_VERSION 48 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/siderolabs/deep-copy@${DEEPCOPY_VERSION} \ 49 | && mv /go/bin/deep-copy /bin/deep-copy 50 | ARG GOLANGCILINT_VERSION 51 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCILINT_VERSION} \ 52 | && mv /go/bin/golangci-lint /bin/golangci-lint 53 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install golang.org/x/vuln/cmd/govulncheck@latest \ 54 | && mv /go/bin/govulncheck /bin/govulncheck 55 | ARG GOFUMPT_VERSION 56 | RUN go install mvdan.cc/gofumpt@${GOFUMPT_VERSION} \ 57 | && mv /go/bin/gofumpt /bin/gofumpt 58 | 59 | # tools and sources 60 | FROM tools AS base 61 | WORKDIR /src 62 | COPY go.mod go.mod 63 | COPY go.sum go.sum 64 | RUN cd . 65 | RUN --mount=type=cache,target=/go/pkg go mod download 66 | RUN --mount=type=cache,target=/go/pkg go mod verify 67 | COPY ./proxy ./proxy 68 | COPY ./testservice ./testservice 69 | RUN --mount=type=cache,target=/go/pkg go list -mod=readonly all >/dev/null 70 | 71 | # runs protobuf compiler 72 | FROM tools AS proto-compile 73 | COPY --from=proto-specs / / 74 | RUN protoc -I/testservice --go_out=paths=source_relative:/testservice --go-grpc_out=paths=source_relative:/testservice /testservice/test.proto 75 | RUN rm /testservice/test.proto 76 | RUN goimports -w -local github.com/siderolabs/grpc-proxy /testservice 77 | RUN gofumpt -w /testservice 78 | 79 | # runs gofumpt 80 | FROM base AS lint-gofumpt 81 | RUN FILES="$(gofumpt -l .)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'gofumpt -w .':\n${FILES}"; exit 1) 82 | 83 | # runs golangci-lint 84 | FROM base AS lint-golangci-lint 85 | WORKDIR /src 86 | COPY .golangci.yml . 87 | ENV GOGC=50 88 | RUN golangci-lint config verify --config .golangci.yml 89 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/root/.cache/golangci-lint --mount=type=cache,target=/go/pkg golangci-lint run --config .golangci.yml 90 | 91 | # runs govulncheck 92 | FROM base AS lint-govulncheck 93 | WORKDIR /src 94 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg govulncheck ./... 95 | 96 | # runs unit-tests with race detector 97 | FROM base AS unit-tests-race 98 | WORKDIR /src 99 | ARG TESTPKGS 100 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg --mount=type=cache,target=/tmp CGO_ENABLED=1 go test -v -race -count 1 ${TESTPKGS} 101 | 102 | # runs unit-tests 103 | FROM base AS unit-tests-run 104 | WORKDIR /src 105 | ARG TESTPKGS 106 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg --mount=type=cache,target=/tmp go test -v -covermode=atomic -coverprofile=coverage.txt -coverpkg=${TESTPKGS} -count 1 ${TESTPKGS} 107 | 108 | # cleaned up specs and compiled versions 109 | FROM scratch AS generate 110 | COPY --from=proto-compile /testservice/ /testservice/ 111 | 112 | FROM scratch AS unit-tests 113 | COPY --from=unit-tests-run /src/coverage.txt /coverage-unit-tests.txt 114 | 115 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2025-01-22T20:13:50Z by kres 3075de9. 4 | 5 | # common variables 6 | 7 | SHA := $(shell git describe --match=none --always --abbrev=8 --dirty) 8 | TAG := $(shell git describe --tag --always --dirty --match v[0-9]\*) 9 | ABBREV_TAG := $(shell git describe --tags >/dev/null 2>/dev/null && git describe --tag --always --match v[0-9]\* --abbrev=0 || echo 'undefined') 10 | BRANCH := $(shell git rev-parse --abbrev-ref HEAD) 11 | ARTIFACTS := _out 12 | IMAGE_TAG ?= $(TAG) 13 | OPERATING_SYSTEM := $(shell uname -s | tr '[:upper:]' '[:lower:]') 14 | GOARCH := $(shell uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/') 15 | WITH_DEBUG ?= false 16 | WITH_RACE ?= false 17 | REGISTRY ?= ghcr.io 18 | USERNAME ?= siderolabs 19 | REGISTRY_AND_USERNAME ?= $(REGISTRY)/$(USERNAME) 20 | PROTOBUF_GO_VERSION ?= 1.36.2 21 | GRPC_GO_VERSION ?= 1.5.1 22 | GRPC_GATEWAY_VERSION ?= 2.25.1 23 | VTPROTOBUF_VERSION ?= 0.6.0 24 | GOIMPORTS_VERSION ?= 0.29.0 25 | DEEPCOPY_VERSION ?= v0.5.6 26 | GOLANGCILINT_VERSION ?= v1.63.4 27 | GOFUMPT_VERSION ?= v0.7.0 28 | GO_VERSION ?= 1.23.4 29 | GO_BUILDFLAGS ?= 30 | GO_LDFLAGS ?= 31 | CGO_ENABLED ?= 0 32 | GOTOOLCHAIN ?= local 33 | TESTPKGS ?= ./... 34 | KRES_IMAGE ?= ghcr.io/siderolabs/kres:latest 35 | CONFORMANCE_IMAGE ?= ghcr.io/siderolabs/conform:latest 36 | 37 | # docker build settings 38 | 39 | BUILD := docker buildx build 40 | PLATFORM ?= linux/amd64 41 | PROGRESS ?= auto 42 | PUSH ?= false 43 | CI_ARGS ?= 44 | BUILDKIT_MULTI_PLATFORM ?= 45 | COMMON_ARGS = --file=Dockerfile 46 | COMMON_ARGS += --provenance=false 47 | COMMON_ARGS += --progress=$(PROGRESS) 48 | COMMON_ARGS += --platform=$(PLATFORM) 49 | COMMON_ARGS += --build-arg=BUILDKIT_MULTI_PLATFORM=$(BUILDKIT_MULTI_PLATFORM) 50 | COMMON_ARGS += --push=$(PUSH) 51 | COMMON_ARGS += --build-arg=ARTIFACTS="$(ARTIFACTS)" 52 | COMMON_ARGS += --build-arg=SHA="$(SHA)" 53 | COMMON_ARGS += --build-arg=TAG="$(TAG)" 54 | COMMON_ARGS += --build-arg=ABBREV_TAG="$(ABBREV_TAG)" 55 | COMMON_ARGS += --build-arg=USERNAME="$(USERNAME)" 56 | COMMON_ARGS += --build-arg=REGISTRY="$(REGISTRY)" 57 | COMMON_ARGS += --build-arg=TOOLCHAIN="$(TOOLCHAIN)" 58 | COMMON_ARGS += --build-arg=CGO_ENABLED="$(CGO_ENABLED)" 59 | COMMON_ARGS += --build-arg=GO_BUILDFLAGS="$(GO_BUILDFLAGS)" 60 | COMMON_ARGS += --build-arg=GO_LDFLAGS="$(GO_LDFLAGS)" 61 | COMMON_ARGS += --build-arg=GOTOOLCHAIN="$(GOTOOLCHAIN)" 62 | COMMON_ARGS += --build-arg=GOEXPERIMENT="$(GOEXPERIMENT)" 63 | COMMON_ARGS += --build-arg=PROTOBUF_GO_VERSION="$(PROTOBUF_GO_VERSION)" 64 | COMMON_ARGS += --build-arg=GRPC_GO_VERSION="$(GRPC_GO_VERSION)" 65 | COMMON_ARGS += --build-arg=GRPC_GATEWAY_VERSION="$(GRPC_GATEWAY_VERSION)" 66 | COMMON_ARGS += --build-arg=VTPROTOBUF_VERSION="$(VTPROTOBUF_VERSION)" 67 | COMMON_ARGS += --build-arg=GOIMPORTS_VERSION="$(GOIMPORTS_VERSION)" 68 | COMMON_ARGS += --build-arg=DEEPCOPY_VERSION="$(DEEPCOPY_VERSION)" 69 | COMMON_ARGS += --build-arg=GOLANGCILINT_VERSION="$(GOLANGCILINT_VERSION)" 70 | COMMON_ARGS += --build-arg=GOFUMPT_VERSION="$(GOFUMPT_VERSION)" 71 | COMMON_ARGS += --build-arg=TESTPKGS="$(TESTPKGS)" 72 | TOOLCHAIN ?= docker.io/golang:1.23-alpine 73 | 74 | # help menu 75 | 76 | export define HELP_MENU_HEADER 77 | # Getting Started 78 | 79 | To build this project, you must have the following installed: 80 | 81 | - git 82 | - make 83 | - docker (19.03 or higher) 84 | 85 | ## Creating a Builder Instance 86 | 87 | The build process makes use of experimental Docker features (buildx). 88 | To enable experimental features, add 'experimental: "true"' to '/etc/docker/daemon.json' on 89 | Linux or enable experimental features in Docker GUI for Windows or Mac. 90 | 91 | To create a builder instance, run: 92 | 93 | docker buildx create --name local --use 94 | 95 | If running builds that needs to be cached aggresively create a builder instance with the following: 96 | 97 | docker buildx create --name local --use --config=config.toml 98 | 99 | config.toml contents: 100 | 101 | [worker.oci] 102 | gc = true 103 | gckeepstorage = 50000 104 | 105 | [[worker.oci.gcpolicy]] 106 | keepBytes = 10737418240 107 | keepDuration = 604800 108 | filters = [ "type==source.local", "type==exec.cachemount", "type==source.git.checkout"] 109 | [[worker.oci.gcpolicy]] 110 | all = true 111 | keepBytes = 53687091200 112 | 113 | If you already have a compatible builder instance, you may use that instead. 114 | 115 | ## Artifacts 116 | 117 | All artifacts will be output to ./$(ARTIFACTS). Images will be tagged with the 118 | registry "$(REGISTRY)", username "$(USERNAME)", and a dynamic tag (e.g. $(IMAGE):$(IMAGE_TAG)). 119 | The registry and username can be overridden by exporting REGISTRY, and USERNAME 120 | respectively. 121 | 122 | endef 123 | 124 | ifneq (, $(filter $(WITH_RACE), t true TRUE y yes 1)) 125 | GO_BUILDFLAGS += -race 126 | CGO_ENABLED := 1 127 | GO_LDFLAGS += -linkmode=external -extldflags '-static' 128 | endif 129 | 130 | ifneq (, $(filter $(WITH_DEBUG), t true TRUE y yes 1)) 131 | GO_BUILDFLAGS += -tags sidero.debug 132 | else 133 | GO_LDFLAGS += -s 134 | endif 135 | 136 | all: unit-tests lint 137 | 138 | $(ARTIFACTS): ## Creates artifacts directory. 139 | @mkdir -p $(ARTIFACTS) 140 | 141 | .PHONY: clean 142 | clean: ## Cleans up all artifacts. 143 | @rm -rf $(ARTIFACTS) 144 | 145 | target-%: ## Builds the specified target defined in the Dockerfile. The build result will only remain in the build cache. 146 | @$(BUILD) --target=$* $(COMMON_ARGS) $(TARGET_ARGS) $(CI_ARGS) . 147 | 148 | registry-%: ## Builds the specified target defined in the Dockerfile and the output is an image. The image is pushed to the registry if PUSH=true. 149 | @$(MAKE) target-$* TARGET_ARGS="--tag=$(REGISTRY)/$(USERNAME)/$(IMAGE_NAME):$(IMAGE_TAG)" BUILDKIT_MULTI_PLATFORM=1 150 | 151 | local-%: ## Builds the specified target defined in the Dockerfile using the local output type. The build result will be output to the specified local destination. 152 | @$(MAKE) target-$* TARGET_ARGS="--output=type=local,dest=$(DEST) $(TARGET_ARGS)" 153 | @PLATFORM=$(PLATFORM) DEST=$(DEST) bash -c '\ 154 | for platform in $$(tr "," "\n" <<< "$$PLATFORM"); do \ 155 | directory="$${platform//\//_}"; \ 156 | if [[ -d "$$DEST/$$directory" ]]; then \ 157 | echo $$platform; \ 158 | mv -f "$$DEST/$$directory/"* $$DEST; \ 159 | rmdir "$$DEST/$$directory/"; \ 160 | fi; \ 161 | done' 162 | 163 | generate: ## Generate .proto definitions. 164 | @$(MAKE) local-$@ DEST=./ 165 | 166 | lint-golangci-lint: ## Runs golangci-lint linter. 167 | @$(MAKE) target-$@ 168 | 169 | lint-gofumpt: ## Runs gofumpt linter. 170 | @$(MAKE) target-$@ 171 | 172 | .PHONY: fmt 173 | fmt: ## Formats the source code 174 | @docker run --rm -it -v $(PWD):/src -w /src golang:$(GO_VERSION) \ 175 | bash -c "export GOTOOLCHAIN=local; \ 176 | export GO111MODULE=on; export GOPROXY=https://proxy.golang.org; \ 177 | go install mvdan.cc/gofumpt@$(GOFUMPT_VERSION) && \ 178 | gofumpt -w ." 179 | 180 | lint-govulncheck: ## Runs govulncheck linter. 181 | @$(MAKE) target-$@ 182 | 183 | .PHONY: base 184 | base: ## Prepare base toolchain 185 | @$(MAKE) target-$@ 186 | 187 | .PHONY: unit-tests 188 | unit-tests: ## Performs unit tests 189 | @$(MAKE) local-$@ DEST=$(ARTIFACTS) 190 | 191 | .PHONY: unit-tests-race 192 | unit-tests-race: ## Performs unit tests with race detection enabled. 193 | @$(MAKE) target-$@ 194 | 195 | .PHONY: lint-markdown 196 | lint-markdown: ## Runs markdownlint. 197 | @$(MAKE) target-$@ 198 | 199 | .PHONY: lint 200 | lint: lint-golangci-lint lint-gofumpt lint-govulncheck lint-markdown ## Run all linters for the project. 201 | 202 | .PHONY: rekres 203 | rekres: 204 | @docker pull $(KRES_IMAGE) 205 | @docker run --rm --net=host --user $(shell id -u):$(shell id -g) -v $(PWD):/src -w /src -e GITHUB_TOKEN $(KRES_IMAGE) 206 | 207 | .PHONY: help 208 | help: ## This help menu. 209 | @echo "$$HELP_MENU_HEADER" 210 | @grep -E '^[a-zA-Z%_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 211 | 212 | .PHONY: release-notes 213 | release-notes: $(ARTIFACTS) 214 | @ARTIFACTS=$(ARTIFACTS) ./hack/release.sh $@ $(ARTIFACTS)/RELEASE_NOTES.md $(TAG) 215 | 216 | .PHONY: conformance 217 | conformance: 218 | @docker pull $(CONFORMANCE_IMAGE) 219 | @docker run --rm -it -v $(PWD):/src -w /src $(CONFORMANCE_IMAGE) enforce 220 | 221 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gRPC Proxy 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/talos-systems/grpc-proxy)](https://goreportcard.com/report/github.com/talos-systems/grpc-proxy) 4 | [![GoDoc](http://img.shields.io/badge/GoDoc-Reference-blue.svg)](https://godoc.org/github.com/talos-systems/grpc-proxy) 5 | [![Apache 2.0 License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) 6 | 7 | [gRPC Go](https://github.com/grpc/grpc-go) Proxy server 8 | 9 | ## Project Goal 10 | 11 | Build a transparent reverse proxy for gRPC targets that will make it easy to expose gRPC services 12 | over the Internet. 13 | 14 | This includes: 15 | 16 | * no needed knowledge of the semantics of requests exchanged in the call (independent rollouts) 17 | * easy declarative definition of backends and their mappings to frontends 18 | * simple round-robin load balancing of inbound requests from a single connection to multiple backends 19 | 20 | ## Proxying Modes 21 | 22 | There are two proxying modes supported: 23 | 24 | * one to one: in this mode data passed back and forth is transmitted as is without any modifications; 25 | * one to many: one client connection gets mapped into multiple upstream connections, results might be aggregated 26 | (for unary calls), errors translated into response messages; this mode requires a special layout of protobuf messages. 27 | 28 | ## Proxy Handler 29 | 30 | The package [`proxy`](proxy/) contains a generic gRPC reverse proxy handler that allows a gRPC server not to 31 | know about method names and their request/response data types. 32 | Please consult the package documentation. 33 | Here you can find an example usage. 34 | 35 | First, define `Backend` implementation to identify specific upstream. 36 | For one to one proxying, `SingleBackend` might be used: 37 | 38 | ```go 39 | conn, err := grpc.NewClient( 40 | "api-service.staging.svc.local", 41 | grpc.WithDefaultCallOptions(grpc.ForceCodec(proxy.Codec())), 42 | ) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | backend := &proxy.SingleBackend{ 48 | GetConn: func(ctx context.Context) (context.Context, *grpc.ClientConn, error) { 49 | md, _ := metadata.FromIncomingContext(ctx) 50 | 51 | // Copy the inbound metadata explicitly. 52 | outCtx := metadata.NewOutgoingContext(ctx, md.Copy()) 53 | 54 | return outCtx, conn, nil 55 | }, 56 | } 57 | ``` 58 | 59 | Defining a `StreamDirector` that decides where (if at all) to send the request 60 | 61 | ```go 62 | director = func(ctx context.Context, fullMethodName string) (proxy.Mode, []proxy.Backend, error) { 63 | // Make sure we never forward internal services. 64 | if strings.HasPrefix(fullMethodName, "/com.example.internal.") { 65 | return proxy.One2One, nil, status.Errorf(codes.Unimplemented, "Unknown method") 66 | } 67 | 68 | md, ok := metadata.FromIncomingContext(ctx) 69 | 70 | if ok { 71 | // Decide on which backend to dial 72 | if val, exists := md[":authority"]; exists && val[0] == "staging.api.example.com" { 73 | return proxy.One2One, []proxy.Backend{stagingBackend}, nil 74 | } else if val, exists := md[":authority"]; exists && val[0] == "api.example.com" { 75 | return proxy.One2One, []proxy.Backend{prodBackend}, nil 76 | } 77 | } 78 | 79 | return proxy.One2One, nil, status.Errorf(codes.Unimplemented, "Unknown method") 80 | } 81 | ``` 82 | 83 | Then you need to register it with a `grpc.Server`. 84 | The server may have other handlers that will be served locally: 85 | 86 | ```go 87 | server := grpc.NewServer( 88 | grpc.ForceServerCodec(proxy.Codec()), 89 | grpc.UnknownServiceHandler( 90 | proxy.TransparentHandler(director), 91 | proxy.WithMode(proxy.One2One), 92 | )) 93 | pb_test.RegisterTestServiceServer(server, &testImpl{}) 94 | ``` 95 | 96 | ## One to Many Proxying 97 | 98 | In one to many proxying mode, it's critical to identify source of each message proxied back from the upstreams. 99 | Also upstream error shouldn't fail whole request and instead return errors as messages back. 100 | In order to achieve this goal, protobuf response message should follow the same structure: 101 | 102 | 1. Every response should be `repeated` list of response messages so that responses from multiple upstreams might be 103 | concatenated to build a combined response from all the upstreams. 104 | 105 | 2. Response should contain common metadata fields which allow grpc-proxy to inject source information and error information 106 | into response. 107 | 108 | ## Talks 109 | 110 | * "Transparent gRPC proxy in Go" at [GopherCon Russia 2021](https://www.gophercon-russia.ru/) [slides](https://speakerdeck.com/smira/transparent-grpc-gateway-in-go) 111 | 112 | ## History 113 | 114 | This is a fork of awesome [mwitkow/grpc-proxy](https://github.com/mwitkow/grpc-proxy) package with added support 115 | for one to many proxying. 116 | 117 | ## License 118 | 119 | `grpc-proxy` is released under the Apache 2.0 license. 120 | See [LICENSE.txt](LICENSE.txt). 121 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/siderolabs/grpc-proxy 2 | 3 | go 1.22.3 4 | 5 | require ( 6 | github.com/golang/protobuf v1.5.4 7 | github.com/hashicorp/go-multierror v1.1.1 8 | github.com/jhump/grpctunnel v0.3.0 9 | github.com/stretchr/testify v1.10.0 10 | google.golang.org/grpc v1.69.4 11 | google.golang.org/protobuf v1.36.3 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/fullstorydev/grpchan v1.1.1 // indirect 17 | github.com/hashicorp/errwrap v1.1.0 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | golang.org/x/net v0.34.0 // indirect 20 | golang.org/x/sys v0.29.0 // indirect 21 | golang.org/x/text v0.21.0 // indirect 22 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250122153221-138b5a5a4fd4 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | 26 | retract v0.5.0 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 4 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 5 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 10 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 11 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 12 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 13 | github.com/fullstorydev/grpchan v1.1.1 h1:heQqIJlAv5Cnks9a70GRL2EJke6QQoUB25VGR6TZQas= 14 | github.com/fullstorydev/grpchan v1.1.1/go.mod h1:f4HpiV8V6htfY/K44GWV1ESQzHBTq7DinhzqQ95lpgc= 15 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 16 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 17 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 18 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 19 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 20 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 21 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 22 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 23 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 24 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 25 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 26 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 27 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 28 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 29 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 30 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 31 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 32 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 33 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 34 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 35 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 36 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 37 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 38 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 39 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 40 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 41 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 42 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 43 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 44 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 45 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 46 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 47 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 48 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 49 | github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= 50 | github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= 51 | github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= 52 | github.com/jhump/grpctunnel v0.3.0 h1:itddWDKl7J4CeW4nzY3S/a1s7mPZUb8UtUzEhc/R8mg= 53 | github.com/jhump/grpctunnel v0.3.0/go.mod h1:dn5zls1F+1ftPMkbh4kVTVgGuY5t/v3ZgdjtnSMC3f4= 54 | github.com/jhump/protoreflect v1.11.0 h1:bvACHUD1Ua/3VxY4aAMpItKMhhwbimlKFJKsLsVgDjU= 55 | github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= 56 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 57 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 58 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 59 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 60 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 61 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 62 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 63 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 64 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 65 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 66 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 67 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 68 | go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= 69 | go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= 70 | go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= 71 | go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= 72 | go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= 73 | go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= 74 | go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= 75 | go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= 76 | go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= 77 | go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= 78 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 79 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 80 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 81 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 82 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 83 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 84 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 85 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 86 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 87 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 88 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 89 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 90 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 91 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 92 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 93 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 96 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 98 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 99 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 100 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 101 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 103 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 104 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 105 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 106 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 107 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 108 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 109 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 110 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 111 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 112 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 113 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 114 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 115 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 116 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 117 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 118 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 119 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 120 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250122153221-138b5a5a4fd4 h1:yrTuav+chrF0zF/joFGICKTzYv7mh/gr9AgEXrVU8ao= 121 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= 122 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 123 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 124 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 125 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 126 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 127 | google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= 128 | google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= 129 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 130 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 131 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 132 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 133 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 134 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 135 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 136 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 137 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 138 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 139 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 140 | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= 141 | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 142 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 143 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 144 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 145 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 146 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 147 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 148 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 149 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 150 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 151 | -------------------------------------------------------------------------------- /hack/git-chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ range .Versions }} 6 | 7 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }}) 8 | 9 | {{ range .CommitGroups -}} 10 | ### {{ .Title }} 11 | 12 | {{ range .Commits -}} 13 | * {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 14 | {{ end }} 15 | {{ end -}} 16 | 17 | {{- if .NoteGroups -}} 18 | {{ range .NoteGroups -}} 19 | ### {{ .Title }} 20 | 21 | {{ range .Notes }} 22 | {{ .Body }} 23 | {{ end }} 24 | {{ end -}} 25 | {{ end -}} 26 | {{ end -}} 27 | -------------------------------------------------------------------------------- /hack/git-chglog/config.yaml: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2021-03-25T21:15:43Z by kres 424ae88-dirty. 4 | 5 | style: github 6 | template: CHANGELOG.tpl.md 7 | info: 8 | title: CHANGELOG 9 | repository_url: https://github.com/talos-systems/grpc-proxy 10 | options: 11 | commits: 12 | # filters: 13 | # Type: 14 | # - feat 15 | # - fix 16 | # - perf 17 | # - refactor 18 | commit_groups: 19 | # title_maps: 20 | # feat: Features 21 | # fix: Bug Fixes 22 | # perf: Performance Improvements 23 | # refactor: Code Refactoring 24 | header: 25 | pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$" 26 | pattern_maps: 27 | - Type 28 | - Scope 29 | - Subject 30 | notes: 31 | keywords: 32 | - BREAKING CHANGE 33 | -------------------------------------------------------------------------------- /hack/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 4 | # 5 | # Generated on 2024-10-10T19:23:45Z by kres 34e72ac. 6 | 7 | set -e 8 | 9 | RELEASE_TOOL_IMAGE="ghcr.io/siderolabs/release-tool:latest" 10 | 11 | function release-tool { 12 | docker pull "${RELEASE_TOOL_IMAGE}" >/dev/null 13 | docker run --rm -w /src -v "${PWD}":/src:ro "${RELEASE_TOOL_IMAGE}" -l -d -n -t "${1}" ./hack/release.toml 14 | } 15 | 16 | function changelog { 17 | if [ "$#" -eq 1 ]; then 18 | (release-tool ${1}; echo; cat CHANGELOG.md) > CHANGELOG.md- && mv CHANGELOG.md- CHANGELOG.md 19 | else 20 | echo 1>&2 "Usage: $0 changelog [tag]" 21 | exit 1 22 | fi 23 | } 24 | 25 | function release-notes { 26 | release-tool "${2}" > "${1}" 27 | } 28 | 29 | function cherry-pick { 30 | if [ $# -ne 2 ]; then 31 | echo 1>&2 "Usage: $0 cherry-pick " 32 | exit 1 33 | fi 34 | 35 | git checkout $2 36 | git fetch 37 | git rebase upstream/$2 38 | git cherry-pick -x $1 39 | } 40 | 41 | function commit { 42 | if [ $# -ne 1 ]; then 43 | echo 1>&2 "Usage: $0 commit " 44 | exit 1 45 | fi 46 | 47 | if is_on_main_branch; then 48 | update_license_files 49 | fi 50 | 51 | git commit -s -m "release($1): prepare release" -m "This is the official $1 release." 52 | } 53 | 54 | function is_on_main_branch { 55 | main_remotes=("upstream" "origin") 56 | branch_names=("main" "master") 57 | current_branch=$(git rev-parse --abbrev-ref HEAD) 58 | 59 | echo "Check current branch: $current_branch" 60 | 61 | for remote in "${main_remotes[@]}"; do 62 | echo "Fetch remote $remote..." 63 | 64 | if ! git fetch --quiet "$remote" &>/dev/null; then 65 | echo "Failed to fetch $remote, skip..." 66 | 67 | continue 68 | fi 69 | 70 | for branch_name in "${branch_names[@]}"; do 71 | if ! git rev-parse --verify "$branch_name" &>/dev/null; then 72 | echo "Branch $branch_name does not exist, skip..." 73 | 74 | continue 75 | fi 76 | 77 | echo "Branch $remote/$branch_name exists, comparing..." 78 | 79 | merge_base=$(git merge-base "$current_branch" "$remote/$branch_name") 80 | latest_main=$(git rev-parse "$remote/$branch_name") 81 | 82 | if [ "$merge_base" = "$latest_main" ]; then 83 | echo "Current branch is up-to-date with $remote/$branch_name" 84 | 85 | return 0 86 | else 87 | echo "Current branch is not on $remote/$branch_name" 88 | 89 | return 1 90 | fi 91 | done 92 | done 93 | 94 | echo "No main or master branch found on any remote" 95 | 96 | return 1 97 | } 98 | 99 | function update_license_files { 100 | script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 101 | parent_dir="$(dirname "$script_dir")" 102 | current_year=$(date +"%Y") 103 | change_date=$(date -v+4y +"%Y-%m-%d" 2>/dev/null || date -d "+4 years" +"%Y-%m-%d" 2>/dev/null || date --date="+4 years" +"%Y-%m-%d") 104 | 105 | # Find LICENSE and .kres.yaml files recursively in the parent directory (project root) 106 | find "$parent_dir" \( -name "LICENSE" -o -name ".kres.yaml" \) -type f | while read -r file; do 107 | temp_file="${file}.tmp" 108 | 109 | if [[ $file == *"LICENSE" ]]; then 110 | if grep -q "^Business Source License" "$file"; then 111 | sed -e "s/The Licensed Work is (c) [0-9]\{4\}/The Licensed Work is (c) $current_year/" \ 112 | -e "s/Change Date: [0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}/Change Date: $change_date/" \ 113 | "$file" >"$temp_file" 114 | else 115 | continue # Not a Business Source License file 116 | fi 117 | elif [[ $file == *".kres.yaml" ]]; then 118 | sed -E 's/^([[:space:]]*)ChangeDate:.*$/\1ChangeDate: "'"$change_date"'"/' "$file" >"$temp_file" 119 | fi 120 | 121 | # Check if the file has changed 122 | if ! cmp -s "$file" "$temp_file"; then 123 | mv "$temp_file" "$file" 124 | echo "Updated: $file" 125 | git add "$file" 126 | else 127 | echo "No changes: $file" 128 | rm "$temp_file" 129 | fi 130 | done 131 | } 132 | 133 | if declare -f "$1" > /dev/null 134 | then 135 | cmd="$1" 136 | shift 137 | $cmd "$@" 138 | else 139 | cat <%s", c.parentCodec.Name()) 81 | } 82 | -------------------------------------------------------------------------------- /proxy/codec_test.go: -------------------------------------------------------------------------------- 1 | package proxy_test 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "google.golang.org/grpc/mem" 10 | 11 | "github.com/siderolabs/grpc-proxy/proxy" 12 | talos_testproto "github.com/siderolabs/grpc-proxy/testservice" 13 | ) 14 | 15 | func TestCodec_ReadYourWrites(t *testing.T) { 16 | d := []byte{0xDE, 0xAD, 0xBE, 0xEF} 17 | 18 | for key, val := range map[string][]byte{ 19 | "short message": d, 20 | "long message": bytes.Repeat(d, 3072), 21 | } { 22 | t.Run(key, func(t *testing.T) { 23 | framePtr := proxy.NewFrame(nil) 24 | codec := proxy.Codec() 25 | 26 | buffer := mem.Copy(val, mem.DefaultBufferPool()) 27 | defer func() { buffer.Free() }() 28 | 29 | require.NoError(t, codec.Unmarshal(mem.BufferSlice{buffer}, framePtr), "unmarshalling must go ok") 30 | out, err := codec.Marshal(framePtr) 31 | require.NoError(t, err, "no marshal error") 32 | require.Equal(t, val, out.Materialize(), "output and data must be the same") 33 | 34 | out.Free() 35 | buffer.Free() 36 | buffer = mem.Copy([]byte{0x55}, mem.DefaultBufferPool()) 37 | 38 | // reuse 39 | require.NoError(t, codec.Unmarshal(mem.BufferSlice{buffer}, framePtr), "unmarshalling must go ok") 40 | out, err = codec.Marshal(framePtr) 41 | require.NoError(t, err, "no marshal error") 42 | require.Equal(t, []byte{0x55}, out.Materialize(), "output and data must be the same") 43 | 44 | out.Free() 45 | }) 46 | } 47 | } 48 | 49 | func TestCodecUsualMessage(t *testing.T) { 50 | const msg = "short message" 51 | 52 | for key, val := range map[string]string{ 53 | "short message": "edbca", 54 | "long message": strings.Repeat(msg, 3072), 55 | } { 56 | t.Run(key, func(t *testing.T) { 57 | msg := &talos_testproto.PingRequest{Value: val} 58 | 59 | codec := proxy.Codec() 60 | 61 | out, err := codec.Marshal(msg) 62 | require.NoError(t, err, "no marshal error") 63 | 64 | defer out.Free() 65 | 66 | var dst talos_testproto.PingRequest 67 | 68 | require.NoError(t, codec.Unmarshal(out, &dst), "unmarshalling must go ok") 69 | require.NotZero(t, dst.Value, "output must not be zero") 70 | require.Equal(t, msg.Value, dst.Value, "output and data must be the same") 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /proxy/director.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Michal Witkowski. All Rights Reserved. 2 | // Copyright 2019 Andrey Smirnov. All Rights Reserved. 3 | // See LICENSE for licensing terms. 4 | 5 | package proxy 6 | 7 | import ( 8 | "context" 9 | 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | // Backend wraps information about upstream connection. 14 | // 15 | // For simple one-to-one proxying, not much should be done in the Backend, simply 16 | // providing a connection is enough. 17 | // 18 | // When proxying one-to-many and aggregating results, Backend might be used to 19 | // append additional fields to upstream response to support more complicated 20 | // proxying. 21 | type Backend interface { 22 | // String provides backend name for logging and errors. 23 | String() string 24 | 25 | // GetConnection returns a grpc connection to the backend. 26 | // 27 | // The context returned from this function should be the context for the *outgoing* (to backend) call. In case you want 28 | // to forward any Metadata between the inbound request and outbound requests, you should do it manually. However, you 29 | // *must* propagate the cancel function (`context.WithCancel`) of the inbound context to the one returned. 30 | GetConnection(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) 31 | 32 | // AppendInfo is called to enhance response from the backend with additional data. 33 | // 34 | // Parameter streaming indicates if response is delivered in streaming mode or not. 35 | // 36 | // Usecase might be appending backend endpoint (or name) to the protobuf serialized response, so that response is enhanced 37 | // with source information. This is particularly important for one to many calls, when it is required to identify 38 | // response from each of the backends participating in the proxying. 39 | // 40 | // If not additional proxying is required, simply returning the buffer without changes works fine. 41 | AppendInfo(streaming bool, resp []byte) ([]byte, error) 42 | 43 | // BuildError is called to convert error from upstream into response field. 44 | // 45 | // BuildError is never called for one to one proxying, in that case all the errors are returned back to the caller 46 | // as grpc errors. Parameter streaming indicates if response is delivered in streaming mode or not. 47 | // 48 | // When proxying one to many, if one the requests fails or upstream returns an error, it is undesirable to fail the whole 49 | // request and discard responses from other backends. BuildError converts (marshals) error from backend into protobuf encoded 50 | // response which is analyzed by the caller, so that caller reaching out to N upstreams receives N1 successful responses and 51 | // N2 error responses so that N1 + N2 == N. 52 | // 53 | // If BuildError returns nil, error is returned as grpc error (failing whole request). 54 | BuildError(streaming bool, err error) ([]byte, error) 55 | } 56 | 57 | // SingleBackend implements a simple wrapper around get connection function of one to one proxying. 58 | // 59 | // SingleBackend implements Backend interface and might be used as an easy wrapper for one to one proxying. 60 | type SingleBackend struct { 61 | // GetConn returns a grpc connection to the backend. 62 | // 63 | // The context returned from this function should be the context for the *outgoing* (to backend) call. In case you want 64 | // to forward any Metadata between the inbound request and outbound requests, you should do it manually. However, you 65 | // *must* propagate the cancel function (`context.WithCancel`) of the inbound context to the one returned. 66 | GetConn func(ctx context.Context) (context.Context, *grpc.ClientConn, error) 67 | } 68 | 69 | func (sb *SingleBackend) String() string { 70 | return "backend" 71 | } 72 | 73 | // GetConnection returns a grpc connection to the backend. 74 | func (sb *SingleBackend) GetConnection(ctx context.Context, _ string) (context.Context, *grpc.ClientConn, error) { 75 | return sb.GetConn(ctx) 76 | } 77 | 78 | // AppendInfo is called to enhance response from the backend with additional data. 79 | func (sb *SingleBackend) AppendInfo(_ bool, resp []byte) ([]byte, error) { 80 | return resp, nil 81 | } 82 | 83 | // BuildError is called to convert error from upstream into response field. 84 | func (sb *SingleBackend) BuildError(bool, error) ([]byte, error) { 85 | return nil, nil 86 | } 87 | 88 | // StreamDirector returns a list of Backend objects to forward the call to. 89 | // 90 | // There are two proxying modes: 91 | // 1. one to one: StreamDirector returns a single Backend object - proxying is done verbatim, Backend.AppendInfo might 92 | // be used to enhance response with source information (or it might be skipped). 93 | // 2. one to many: StreamDirector returns more than one Backend object - for unary calls responses from Backend objects 94 | // are aggregated by concatenating protobuf responses (requires top-level `repeated` protobuf definition) and errors 95 | // are wrapped as responses via BuildError. Responses are potentially enhanced via AppendInfo. 96 | // 97 | // The presence of the `Context` allows for rich filtering, e.g. based on Metadata (headers). 98 | // If no handling is meant to be done, a `codes.NotImplemented` gRPC error should be returned. 99 | // 100 | // It is worth noting that the StreamDirector will be fired *after* all server-side stream interceptors 101 | // are invoked. So decisions around authorization, monitoring etc. are better to be handled there. 102 | // 103 | // See the rather rich example. 104 | type StreamDirector func(ctx context.Context, fullMethodName string) (Mode, []Backend, error) 105 | -------------------------------------------------------------------------------- /proxy/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Michal Witkowski. All Rights Reserved. 2 | // See LICENSE for licensing terms. 3 | 4 | /* 5 | Package proxy provides a reverse proxy handler for gRPC. 6 | 7 | The implementation allows a `grpc.Server` to pass a received ServerStream to a ClientStream without understanding 8 | the semantics of the messages exchanged. It basically provides a transparent reverse-proxy. 9 | 10 | This package is intentionally generic, exposing a `StreamDirector` function that allows users of this package 11 | to implement whatever logic of backend-picking, dialing and service verification to perform. 12 | 13 | See examples on documented functions. 14 | */ 15 | package proxy 16 | -------------------------------------------------------------------------------- /proxy/examples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Michal Witkowski. All Rights Reserved. 2 | // See LICENSE for licensing terms. 3 | 4 | package proxy_test 5 | 6 | import ( 7 | "context" 8 | "log" 9 | "strings" 10 | 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/codes" 13 | "google.golang.org/grpc/credentials/insecure" 14 | "google.golang.org/grpc/metadata" 15 | "google.golang.org/grpc/status" 16 | 17 | "github.com/siderolabs/grpc-proxy/proxy" 18 | ) 19 | 20 | var director proxy.StreamDirector 21 | 22 | // ExampleRegisterService is a simple example of registering a service with the proxy. 23 | func ExampleRegisterService() { 24 | // A gRPC server with the proxying codec enabled. 25 | server := grpc.NewServer(grpc.ForceServerCodecV2(proxy.Codec())) 26 | 27 | // Register a TestService with 4 of its methods explicitly. 28 | proxy.RegisterService(server, director, 29 | "talos.testproto.TestService", 30 | proxy.WithMethodNames("PingEmpty", "Ping", "PingError", "PingList"), 31 | proxy.WithStreamedMethodNames("PingList"), 32 | ) 33 | 34 | // Output: 35 | } 36 | 37 | // ExampleTransparentHandler is an example of redirecting all requests to the proxy. 38 | func ExampleTransparentHandler() { 39 | grpc.NewServer( 40 | grpc.ForceServerCodecV2(proxy.Codec()), 41 | grpc.UnknownServiceHandler(proxy.TransparentHandler(director)), 42 | ) 43 | 44 | // Output: 45 | } 46 | 47 | // Provide sa simple example of a director that shields internal services and dials a staging or production backend. 48 | // This is a *very naive* implementation that creates a new connection on every request. Consider using pooling. 49 | func ExampleStreamDirector() { 50 | simpleBackendGen := func(hostname string) (proxy.Backend, error) { 51 | conn, err := grpc.NewClient( 52 | hostname, 53 | grpc.WithDefaultCallOptions(grpc.ForceCodecV2(proxy.Codec())), 54 | grpc.WithTransportCredentials(insecure.NewCredentials()), 55 | ) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return &proxy.SingleBackend{ 61 | GetConn: func(ctx context.Context) (context.Context, *grpc.ClientConn, error) { 62 | md, _ := metadata.FromIncomingContext(ctx) 63 | 64 | // Copy the inbound metadata explicitly. 65 | outCtx := metadata.NewOutgoingContext(ctx, md.Copy()) 66 | 67 | return outCtx, conn, nil 68 | }, 69 | }, nil 70 | } 71 | 72 | stagingBackend, err := simpleBackendGen("api-service.staging.svc.local") 73 | if err != nil { 74 | log.Fatal("failed to create staging backend:", err) 75 | } 76 | 77 | prodBackend, err := simpleBackendGen("api-service.prod.svc.local") 78 | if err != nil { 79 | log.Fatal("failed to create production backend:", err) 80 | } 81 | 82 | director = func(ctx context.Context, fullMethodName string) (proxy.Mode, []proxy.Backend, error) { 83 | // Make sure we never forward internal services. 84 | if strings.HasPrefix(fullMethodName, "/com.example.internal.") { 85 | return proxy.One2One, nil, status.Errorf(codes.Unimplemented, "Unknown method") 86 | } 87 | 88 | md, ok := metadata.FromIncomingContext(ctx) 89 | 90 | if ok { 91 | // Decide on which backend to dial 92 | if val, exists := md[":authority"]; exists && val[0] == "staging.api.example.com" { 93 | return proxy.One2One, []proxy.Backend{stagingBackend}, nil 94 | } else if val, exists := md[":authority"]; exists && val[0] == "api.example.com" { 95 | return proxy.One2One, []proxy.Backend{prodBackend}, nil 96 | } 97 | } 98 | 99 | return proxy.One2One, nil, status.Errorf(codes.Unimplemented, "Unknown method") 100 | } 101 | 102 | // Output: 103 | } 104 | -------------------------------------------------------------------------------- /proxy/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Michal Witkowski. All Rights Reserved. 2 | // Copyright 2019 Andrey Smirnov. All Rights Reserved. 3 | // See LICENSE for licensing terms. 4 | 5 | package proxy 6 | 7 | import ( 8 | "context" 9 | 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | ) 14 | 15 | var clientStreamDescForProxying = &grpc.StreamDesc{ 16 | ServerStreams: true, 17 | ClientStreams: true, 18 | } 19 | 20 | type handlerOptions struct { 21 | streamedMethods map[string]struct{} 22 | streamedDetector StreamedDetectorFunc 23 | serviceName string 24 | methodNames []string 25 | } 26 | 27 | type handler struct { 28 | director StreamDirector 29 | options handlerOptions 30 | } 31 | 32 | type backendConnection struct { 33 | backend Backend 34 | 35 | backendConn *grpc.ClientConn 36 | connError error 37 | 38 | clientStream grpc.ClientStream 39 | } 40 | 41 | // handler is where the real magic of proxying happens. 42 | // It is invoked like any gRPC server stream and uses the gRPC server framing to get and receive bytes from the wire, 43 | // forwarding it to a ClientStream established against the relevant ClientConn. 44 | func (s *handler) handler(_ any, serverStream grpc.ServerStream) error { 45 | // little bit of gRPC internals never hurt anyone 46 | fullMethodName, ok := grpc.MethodFromServerStream(serverStream) 47 | if !ok { 48 | return status.Errorf(codes.Internal, "lowLevelServerStream doesn't exist in the context") 49 | } 50 | 51 | mode, backends, err := s.director(serverStream.Context(), fullMethodName) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | backendConnections := make([]backendConnection, len(backends)) 57 | 58 | clientCtx, clientCancel := context.WithCancel(serverStream.Context()) 59 | defer clientCancel() 60 | 61 | for i := range backends { 62 | backendConnections[i].backend = backends[i] 63 | 64 | // We require that the backend's returned context inherits from the serverStream.Context(). 65 | var outgoingCtx context.Context 66 | outgoingCtx, backendConnections[i].backendConn, backendConnections[i].connError = backends[i].GetConnection(clientCtx, fullMethodName) 67 | 68 | if backendConnections[i].connError != nil { 69 | continue 70 | } 71 | 72 | backendConnections[i].clientStream, backendConnections[i].connError = grpc.NewClientStream(outgoingCtx, clientStreamDescForProxying, 73 | backendConnections[i].backendConn, fullMethodName) 74 | 75 | if backendConnections[i].connError != nil { 76 | continue 77 | } 78 | } 79 | 80 | switch mode { 81 | case One2One: 82 | if len(backendConnections) != 1 { 83 | return status.Errorf(codes.Internal, "one2one proxying should have exactly one connection (got %d)", len(backendConnections)) 84 | } 85 | 86 | return s.handlerOne2One(serverStream, backendConnections) 87 | case One2Many: 88 | if len(backendConnections) == 0 { 89 | return status.Errorf(codes.Unavailable, "no backend connections for proxying") 90 | } 91 | 92 | return s.handlerOne2Many(fullMethodName, serverStream, backendConnections) 93 | default: 94 | return status.Errorf(codes.Internal, "unsupported proxy mode") 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /proxy/handler_one2many.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Andrey Smirnov. All Rights Reserved. 2 | // See LICENSE for licensing terms. 3 | 4 | package proxy 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "io" 11 | 12 | "github.com/hashicorp/go-multierror" 13 | "google.golang.org/grpc" 14 | "google.golang.org/grpc/codes" 15 | "google.golang.org/grpc/status" 16 | ) 17 | 18 | func (s *handler) handlerOne2Many(fullMethodName string, serverStream grpc.ServerStream, backendConnections []backendConnection) error { 19 | // wrap the stream for safe concurrent access 20 | serverStream = &ServerStreamWrapper{ServerStream: serverStream} 21 | 22 | s2cErrChan := s.forwardServerToClientsMulti(serverStream, backendConnections) 23 | 24 | var c2sErrChan chan error 25 | 26 | if s.options.streamedDetector != nil && s.options.streamedDetector(fullMethodName) { 27 | c2sErrChan = s.forwardClientsToServerMultiStreaming(backendConnections, serverStream) 28 | } else { 29 | c2sErrChan = s.forwardClientsToServerMultiUnary(backendConnections, serverStream) 30 | } 31 | 32 | for range 2 { 33 | select { 34 | case s2cErr := <-s2cErrChan: 35 | if errors.Is(s2cErr, io.EOF) { 36 | // this is the happy case where the sender has encountered io.EOF, and won't be sending anymore./ 37 | // the clientStream>serverStream may continue pumping though. 38 | for i := range backendConnections { 39 | if backendConnections[i].clientStream != nil { 40 | backendConnections[i].clientStream.CloseSend() //nolint: errcheck 41 | } 42 | } 43 | } else { 44 | // however, we may have gotten a receive error (stream disconnected, a read error etc) in which case we need 45 | // to cancel the clientStream to the backend, let all of its goroutines be freed up by the CancelFunc and 46 | // exit with an error to the stack 47 | return status.Errorf(codes.Internal, "failed proxying s2c: %v", s2cErr) 48 | } 49 | case c2sErr := <-c2sErrChan: 50 | // c2sErr will contain RPC error from client code. If not io.EOF return the RPC error as server stream error. 51 | if !errors.Is(c2sErr, io.EOF) { 52 | return c2sErr 53 | } 54 | 55 | return nil 56 | } 57 | } 58 | 59 | return status.Errorf(codes.Internal, "gRPC proxying should never reach this stage.") 60 | } 61 | 62 | // formatError tries to format error from upstream as message to the client. 63 | func (s *handler) formatError(streaming bool, src *backendConnection, backendErr error) ([]byte, error) { 64 | payload, err := src.backend.BuildError(streaming, backendErr) 65 | if err != nil { 66 | return nil, fmt.Errorf("error building error for %s: %w", src.backend, err) 67 | } 68 | 69 | if payload == nil { 70 | err = backendErr 71 | } 72 | 73 | return payload, err 74 | } 75 | 76 | // sendError tries to deliver error back to the client via dst. 77 | // 78 | // If sendError fails to deliver the error, error is returned. 79 | // If sendError successfully delivers the error, nil is returned. 80 | func (s *handler) sendError(src *backendConnection, dst grpc.ServerStream, backendErr error) error { 81 | payload, err := s.formatError(true, src, backendErr) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | f := NewFrame(payload) 87 | 88 | if err = dst.SendMsg(f); err != nil { 89 | if errors.Is(err, context.Canceled) { 90 | return nil 91 | } 92 | 93 | if rpcStatus, ok := status.FromError(err); ok && rpcStatus.Message() == "transport is closing" { 94 | return nil 95 | } 96 | 97 | return fmt.Errorf("error sending error back: %w", err) 98 | } 99 | 100 | return nil 101 | } 102 | 103 | // forwardClientsToServerMultiUnary handles one:many proxying, unary call version (merging results) 104 | // 105 | //nolint:gocognit 106 | func (s *handler) forwardClientsToServerMultiUnary(sources []backendConnection, dst grpc.ServerStream) chan error { 107 | ret := make(chan error, 1) 108 | 109 | payloadCh := make(chan []byte, len(sources)) 110 | errCh := make(chan error, len(sources)) 111 | 112 | for i := range sources { 113 | go func(src *backendConnection) { 114 | errCh <- func() error { 115 | if src.connError != nil { 116 | payload, err := s.formatError(false, src, src.connError) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | payloadCh <- payload 122 | 123 | return nil 124 | } 125 | 126 | // Send the header metadata first 127 | md, err := src.clientStream.Header() 128 | if err != nil { 129 | var payload []byte 130 | 131 | payload, err = s.formatError(false, src, err) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | payloadCh <- payload 137 | 138 | return nil 139 | } 140 | 141 | if md != nil { 142 | if err = dst.SetHeader(md); err != nil { 143 | return fmt.Errorf("error setting headers from client %s: %w", src.backend, err) 144 | } 145 | } 146 | 147 | f := &frame{} 148 | 149 | for { 150 | if err := src.clientStream.RecvMsg(f); err != nil { 151 | if errors.Is(err, io.EOF) { 152 | // This happens when the clientStream has nothing else to offer (io.EOF), returned a gRPC error. In those two 153 | // cases we may have received Trailers as part of the call. In case of other errors (stream closed) the trailers 154 | // will be nil. 155 | dst.SetTrailer(src.clientStream.Trailer()) 156 | 157 | return nil 158 | } 159 | 160 | var payload []byte 161 | 162 | payload, err = s.formatError(false, src, err) 163 | if err != nil { 164 | return err 165 | } 166 | 167 | payloadCh <- payload 168 | 169 | return nil 170 | } 171 | 172 | var err error 173 | 174 | f.payload, err = src.backend.AppendInfo(false, f.payload) 175 | if err != nil { 176 | return fmt.Errorf("error appending info for %s: %w", src.backend, err) 177 | } 178 | 179 | payloadCh <- f.payload 180 | } 181 | }() 182 | }(&sources[i]) 183 | } 184 | 185 | go func() { 186 | var multiErr *multierror.Error 187 | 188 | for range sources { 189 | multiErr = multierror.Append(multiErr, <-errCh) 190 | } 191 | 192 | if multiErr.ErrorOrNil() != nil { 193 | ret <- multiErr.ErrorOrNil() 194 | 195 | return 196 | } 197 | 198 | close(payloadCh) 199 | 200 | var merged []byte 201 | for b := range payloadCh { 202 | merged = append(merged, b...) 203 | } 204 | 205 | ret <- dst.SendMsg(NewFrame(merged)) 206 | }() 207 | 208 | return ret 209 | } 210 | 211 | // one:many proxying, streaming version (no merge). 212 | // 213 | //nolint:gocognit 214 | func (s *handler) forwardClientsToServerMultiStreaming(sources []backendConnection, dst grpc.ServerStream) chan error { 215 | ret := make(chan error, 1) 216 | 217 | errCh := make(chan error, len(sources)) 218 | 219 | for i := range sources { 220 | go func(src *backendConnection) { 221 | errCh <- func() error { 222 | if src.connError != nil { 223 | return s.sendError(src, dst, src.connError) 224 | } 225 | 226 | f := &frame{} 227 | 228 | for j := 0; ; j++ { 229 | if err := src.clientStream.RecvMsg(f); err != nil { 230 | if errors.Is(err, io.EOF) { 231 | // This happens when the clientStream has nothing else to offer (io.EOF), returned a gRPC error. In those two 232 | // cases we may have received Trailers as part of the call. In case of other errors (stream closed) the trailers 233 | // will be nil. 234 | dst.SetTrailer(src.clientStream.Trailer()) 235 | 236 | return nil 237 | } 238 | 239 | return s.sendError(src, dst, err) 240 | } 241 | if j == 0 { 242 | // This is a bit of a hack, but client to server headers are only readable after first client msg is 243 | // received but must be written to server stream before the first msg is flushed. 244 | // This is the only place to do it nicely. 245 | md, err := src.clientStream.Header() 246 | if err != nil { 247 | return s.sendError(src, dst, err) 248 | } 249 | 250 | dst.SetHeader(md) //nolint:errcheck // ignore errors, as we might try to set headers multiple times 251 | } 252 | 253 | var err error 254 | f.payload, err = src.backend.AppendInfo(true, f.payload) 255 | if err != nil { 256 | return fmt.Errorf("error appending info for %s: %w", src.backend, err) 257 | } 258 | 259 | if err = dst.SendMsg(f); err != nil { 260 | return fmt.Errorf("error sending back to server from %s: %w", src.backend, err) 261 | } 262 | } 263 | }() 264 | }(&sources[i]) 265 | } 266 | 267 | go func() { 268 | var multiErr *multierror.Error 269 | 270 | for range sources { 271 | multiErr = multierror.Append(multiErr, <-errCh) 272 | } 273 | 274 | ret <- multiErr.ErrorOrNil() 275 | }() 276 | 277 | return ret 278 | } 279 | 280 | func (s *handler) forwardServerToClientsMulti(src grpc.ServerStream, destinations []backendConnection) chan error { 281 | ret := make(chan error, 1) 282 | 283 | go func() { 284 | f := NewFrame(nil) 285 | 286 | for { 287 | if err := src.RecvMsg(f); err != nil { 288 | ret <- err 289 | 290 | return 291 | } 292 | 293 | errCh := make(chan error) 294 | 295 | for i := range destinations { 296 | go func(dst *backendConnection) { 297 | errCh <- func() error { 298 | if dst.clientStream == nil || dst.connError != nil { 299 | return nil //nolint:nilerr // skip it 300 | } 301 | 302 | return dst.clientStream.SendMsg(f) 303 | }() 304 | }(&destinations[i]) 305 | } 306 | 307 | liveDestinations := 0 308 | 309 | for range destinations { 310 | if err := <-errCh; err == nil { 311 | liveDestinations++ 312 | } 313 | } 314 | 315 | if liveDestinations == 0 { 316 | ret <- io.EOF 317 | 318 | return 319 | } 320 | } 321 | }() 322 | 323 | return ret 324 | } 325 | -------------------------------------------------------------------------------- /proxy/handler_one2many_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Michal Witkowski. All Rights Reserved. 2 | // Copyright 2019 Andrey Smirnov. All Rights Reserved. 3 | // See LICENSE for licensing terms. 4 | 5 | package proxy_test 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "net" 13 | "os" 14 | "strconv" 15 | "strings" 16 | "testing" 17 | "time" 18 | 19 | "github.com/stretchr/testify/assert" 20 | "github.com/stretchr/testify/require" 21 | "github.com/stretchr/testify/suite" 22 | "google.golang.org/grpc" 23 | "google.golang.org/grpc/codes" 24 | "google.golang.org/grpc/credentials/insecure" 25 | "google.golang.org/grpc/grpclog" 26 | "google.golang.org/grpc/metadata" 27 | "google.golang.org/grpc/status" 28 | "google.golang.org/protobuf/encoding/protowire" 29 | "google.golang.org/protobuf/proto" 30 | 31 | "github.com/siderolabs/grpc-proxy/proxy" 32 | pb "github.com/siderolabs/grpc-proxy/testservice" 33 | ) 34 | 35 | const ( 36 | numUpstreams = 5 37 | ) 38 | 39 | // asserting service is implemented on the server side and serves as a handler for stuff. 40 | type assertingMultiService struct { 41 | pb.UnimplementedMultiServiceServer 42 | 43 | t *testing.T 44 | server string 45 | } 46 | 47 | func (s *assertingMultiService) PingEmpty(ctx context.Context, _ *pb.Empty) (*pb.MultiPingReply, error) { 48 | // Check that this call has client's metadata. 49 | md, ok := metadata.FromIncomingContext(ctx) 50 | assert.True(s.t, ok, "PingEmpty call must have metadata in context") 51 | _, ok = md[clientMdKey] 52 | assert.True(s.t, ok, "PingEmpty call must have clients's custom headers in metadata") 53 | 54 | return &pb.MultiPingReply{ 55 | Response: []*pb.MultiPingResponse{ 56 | { 57 | Value: pingDefaultValue, 58 | Counter: 42, 59 | Server: s.server, 60 | }, 61 | }, 62 | }, nil 63 | } 64 | 65 | func (s *assertingMultiService) Ping(ctx context.Context, ping *pb.PingRequest) (*pb.MultiPingReply, error) { 66 | // Send user trailers and headers. 67 | grpc.SendHeader(ctx, metadata.Pairs(serverHeaderMdKey, "I like turtles.")) //nolint: errcheck 68 | grpc.SetTrailer(ctx, metadata.Pairs(serverTrailerMdKey, "I like ending turtles.")) //nolint: errcheck 69 | 70 | return &pb.MultiPingReply{ 71 | Response: []*pb.MultiPingResponse{ 72 | { 73 | Value: ping.Value, 74 | Counter: 42, 75 | Server: s.server, 76 | }, 77 | }, 78 | }, nil 79 | } 80 | 81 | func (s *assertingMultiService) PingError(context.Context, *pb.PingRequest) (*pb.EmptyReply, error) { 82 | return nil, status.Errorf(codes.FailedPrecondition, "Userspace error.") 83 | } 84 | 85 | func (s *assertingMultiService) PingList(ping *pb.PingRequest, stream pb.MultiService_PingListServer) error { 86 | // Send user trailers and headers. 87 | stream.SendHeader(metadata.Pairs(serverHeaderMdKey, "I like turtles.")) //nolint: errcheck 88 | 89 | for i := range countListResponses { 90 | stream.Send(&pb.MultiPingResponse{ //nolint: errcheck 91 | Value: ping.Value, 92 | Counter: int32(i), 93 | Server: s.server, 94 | }) 95 | } 96 | 97 | stream.SetTrailer(metadata.Pairs(serverTrailerMdKey, "I like ending turtles.")) 98 | 99 | return nil 100 | } 101 | 102 | func (s *assertingMultiService) PingStream(stream pb.MultiService_PingStreamServer) error { 103 | stream.SendHeader(metadata.Pairs(serverHeaderMdKey, "I like turtles.")) //nolint: errcheck 104 | 105 | counter := int32(0) 106 | 107 | for { 108 | ping, err := stream.Recv() 109 | 110 | if errors.Is(err, io.EOF) { 111 | break 112 | } else if err != nil { 113 | require.NoError(s.t, err, "can't fail reading stream") 114 | 115 | return err 116 | } 117 | 118 | pong := &pb.MultiPingResponse{ 119 | Value: ping.Value, 120 | Counter: counter, 121 | Server: s.server, 122 | } 123 | 124 | if err := stream.Send(pong); err != nil { 125 | require.NoError(s.t, err, "can't fail sending back a pong") 126 | } 127 | 128 | counter++ 129 | } 130 | 131 | stream.SetTrailer(metadata.Pairs(serverTrailerMdKey, "I like ending turtles.")) 132 | 133 | return nil 134 | } 135 | 136 | func (s *assertingMultiService) PingStreamError(pb.MultiService_PingStreamErrorServer) error { 137 | return status.Errorf(codes.FailedPrecondition, "Userspace error.") 138 | } 139 | 140 | type assertingBackend struct { 141 | conn *grpc.ClientConn 142 | i int 143 | } 144 | 145 | func (b *assertingBackend) String() string { 146 | return fmt.Sprintf("backend%d", b.i) 147 | } 148 | 149 | func (b *assertingBackend) GetConnection(ctx context.Context, _ string) (context.Context, *grpc.ClientConn, error) { 150 | md, _ := metadata.FromIncomingContext(ctx) 151 | // Explicitly copy the metadata, otherwise the tests will fail. 152 | outCtx := metadata.NewOutgoingContext(ctx, md.Copy()) 153 | 154 | if b.conn == nil { 155 | return ctx, nil, status.Error(codes.Unavailable, "backend connection failed") 156 | } 157 | 158 | return outCtx, b.conn, nil 159 | } 160 | 161 | func (b *assertingBackend) AppendInfo(streaming bool, resp []byte) ([]byte, error) { 162 | payload, err := proto.Marshal(&pb.ResponseMetadataPrepender{ 163 | Metadata: &pb.ResponseMetadata{ 164 | Hostname: fmt.Sprintf("server%d", b.i), 165 | }, 166 | }) 167 | 168 | if streaming { 169 | return append(resp, payload...), err 170 | } 171 | 172 | // decode protobuf embedded header 173 | typ, n1 := protowire.ConsumeVarint(resp) 174 | _, n2 := protowire.ConsumeVarint(resp[n1:]) // length 175 | 176 | if typ != (1<<3)|2 { // type: 2, field_number: 1 177 | return nil, fmt.Errorf("unexpected message format: %d", typ) 178 | } 179 | 180 | // cut off embedded message header 181 | resp = resp[n1+n2:] 182 | // build new embedded message header 183 | prefix := protowire.AppendVarint(protowire.AppendVarint(nil, (1<<3)|2), uint64(len(resp)+len(payload))) 184 | resp = append(prefix, resp...) 185 | 186 | return append(resp, payload...), err 187 | } 188 | 189 | func (b *assertingBackend) BuildError(streaming bool, err error) ([]byte, error) { 190 | resp := &pb.EmptyReply{ 191 | Response: []*pb.EmptyResponse{ 192 | { 193 | Metadata: &pb.ResponseMetadata{ 194 | Hostname: fmt.Sprintf("server%d", b.i), 195 | UpstreamError: err.Error(), 196 | }, 197 | }, 198 | }, 199 | } 200 | 201 | if streaming { 202 | return proto.Marshal(resp.Response[0]) 203 | } 204 | 205 | return proto.Marshal(resp) 206 | } 207 | 208 | type ProxyOne2ManySuite struct { //nolint: govet 209 | suite.Suite 210 | 211 | serverListeners []net.Listener 212 | servers []*grpc.Server 213 | proxyListener net.Listener 214 | proxy *grpc.Server 215 | serverClientConn *grpc.ClientConn 216 | 217 | client *grpc.ClientConn 218 | testClient pb.MultiServiceClient 219 | 220 | ctx context.Context //nolint:containedctx 221 | ctxCancel context.CancelFunc 222 | } 223 | 224 | func (s *ProxyOne2ManySuite) TestPingEmptyCarriesClientMetadata() { 225 | ctx := metadata.NewOutgoingContext(s.ctx, metadata.Pairs(clientMdKey, "true")) 226 | out, err := s.testClient.PingEmpty(ctx, &pb.Empty{}) 227 | require.NoError(s.T(), err, "PingEmpty should succeed without errors") 228 | 229 | expectedUpstreams := map[string]struct{}{} 230 | for i := range numUpstreams { 231 | expectedUpstreams[fmt.Sprintf("server%d", i)] = struct{}{} 232 | } 233 | 234 | s.Require().Len(out.Response, numUpstreams) 235 | 236 | for _, resp := range out.Response { 237 | s.Require().Equal(pingDefaultValue, resp.Value) 238 | s.Require().EqualValues(42, resp.Counter) 239 | 240 | // equal metadata set by proxy and server 241 | s.Require().Equal(resp.Metadata.Hostname, resp.Server) 242 | 243 | delete(expectedUpstreams, resp.Metadata.Hostname) 244 | } 245 | 246 | s.Require().Empty(expectedUpstreams) 247 | } 248 | 249 | func (s *ProxyOne2ManySuite) TestPingEmpty_StressTest() { 250 | for range 50 { 251 | s.TestPingEmptyCarriesClientMetadata() 252 | } 253 | } 254 | 255 | func (s *ProxyOne2ManySuite) TestPingEmptyTargets() { 256 | for _, targets := range [][]string{ 257 | {"1", "2"}, 258 | {"3", "2", "1"}, 259 | {"0", "4"}, 260 | {"3"}, 261 | } { 262 | md := metadata.Pairs(clientMdKey, "true") 263 | md.Set("targets", targets...) 264 | 265 | ctx := metadata.NewOutgoingContext(s.ctx, md) 266 | out, err := s.testClient.PingEmpty(ctx, &pb.Empty{}) 267 | require.NoError(s.T(), err, "PingEmpty should succeed without errors") 268 | 269 | expectedUpstreams := map[string]struct{}{} 270 | for _, target := range targets { 271 | expectedUpstreams[fmt.Sprintf("server%s", target)] = struct{}{} 272 | } 273 | 274 | s.Require().Len(out.Response, len(expectedUpstreams)) 275 | 276 | for _, resp := range out.Response { 277 | s.Require().Equal(pingDefaultValue, resp.Value) 278 | s.Require().EqualValues(42, resp.Counter) 279 | 280 | // equal metadata set by proxy and server 281 | s.Require().Equal(resp.Metadata.Hostname, resp.Server) 282 | 283 | delete(expectedUpstreams, resp.Metadata.Hostname) 284 | } 285 | 286 | s.Require().Empty(expectedUpstreams) 287 | } 288 | } 289 | 290 | func (s *ProxyOne2ManySuite) TestPingEmptyConnError() { 291 | targets := []string{"0", "-1", "2"} 292 | md := metadata.Pairs(clientMdKey, "true") 293 | md.Set("targets", targets...) 294 | 295 | ctx := metadata.NewOutgoingContext(s.ctx, md) 296 | out, err := s.testClient.PingEmpty(ctx, &pb.Empty{}) 297 | require.NoError(s.T(), err, "PingEmpty should succeed without errors") 298 | 299 | expectedUpstreams := map[string]struct{}{} 300 | for _, target := range targets { 301 | expectedUpstreams[fmt.Sprintf("server%s", target)] = struct{}{} 302 | } 303 | 304 | s.Require().Len(out.Response, len(expectedUpstreams)) 305 | 306 | for _, resp := range out.Response { 307 | delete(expectedUpstreams, resp.Metadata.Hostname) 308 | 309 | if resp.Metadata.Hostname != "server-1" { 310 | s.Assert().Equal(pingDefaultValue, resp.Value) 311 | s.Assert().EqualValues(42, resp.Counter) 312 | 313 | // equal metadata set by proxy and server 314 | s.Assert().Equal(resp.Metadata.Hostname, resp.Server) 315 | } else { 316 | s.Assert().Equal("rpc error: code = Unavailable desc = backend connection failed", resp.Metadata.UpstreamError) 317 | } 318 | } 319 | 320 | s.Require().Empty(expectedUpstreams) 321 | } 322 | 323 | func (s *ProxyOne2ManySuite) TestPingCarriesServerHeadersAndTrailers() { 324 | headerMd := make(metadata.MD) 325 | trailerMd := make(metadata.MD) 326 | // This is an awkward calling convention... but meh. 327 | out, err := s.testClient.Ping(s.ctx, &pb.PingRequest{Value: "foo"}, grpc.Header(&headerMd), grpc.Trailer(&trailerMd)) 328 | require.NoError(s.T(), err, "Ping should succeed without errors") 329 | 330 | s.Require().Len(out.Response, numUpstreams) 331 | 332 | for _, resp := range out.Response { 333 | s.Require().Equal("foo", resp.Value) 334 | s.Require().EqualValues(42, resp.Counter) 335 | 336 | // equal metadata set by proxy and server 337 | s.Require().Equal(resp.Metadata.Hostname, resp.Server) 338 | } 339 | 340 | assert.Contains(s.T(), headerMd, serverHeaderMdKey, "server response headers must contain server data") 341 | assert.Len(s.T(), trailerMd, 1, "server response trailers must contain server data") 342 | } 343 | 344 | func (s *ProxyOne2ManySuite) TestPingErrorPropagatesAppError() { 345 | out, err := s.testClient.PingError(s.ctx, &pb.PingRequest{Value: "foo"}) 346 | s.Require().NoError(err, "error should be encapsulated in the response") 347 | 348 | s.Require().Len(out.Response, numUpstreams) 349 | 350 | for _, resp := range out.Response { 351 | s.Require().NotEmpty(resp.Metadata.UpstreamError) 352 | s.Require().NotEmpty(resp.Metadata.Hostname) 353 | s.Assert().Equal("rpc error: code = FailedPrecondition desc = Userspace error.", resp.Metadata.UpstreamError) 354 | } 355 | } 356 | 357 | func (s *ProxyOne2ManySuite) TestPingStreamErrorPropagatesAppError() { 358 | stream, err := s.testClient.PingStreamError(s.ctx) 359 | s.Require().NoError(err, "error should be encapsulated in the response") 360 | 361 | for range numUpstreams { 362 | var resp *pb.MultiPingResponse 363 | 364 | resp, err = stream.Recv() 365 | s.Require().NoError(err) 366 | 367 | s.Assert().Equal("rpc error: code = FailedPrecondition desc = Userspace error.", resp.Metadata.UpstreamError) 368 | } 369 | 370 | require.NoError(s.T(), stream.CloseSend(), "no error on close send") 371 | _, err = stream.Recv() 372 | require.Equal(s.T(), io.EOF, err, "stream should close with io.EOF, meaning OK") 373 | } 374 | 375 | func (s *ProxyOne2ManySuite) TestPingStreamConnError() { 376 | targets := []string{"0", "-1", "2"} 377 | md := metadata.Pairs(clientMdKey, "true") 378 | md.Set("targets", targets...) 379 | 380 | ctx := metadata.NewOutgoingContext(s.ctx, md) 381 | stream, err := s.testClient.PingStream(ctx) 382 | s.Require().NoError(err, "error should be encapsulated in the response") 383 | 384 | require.NoError(s.T(), stream.CloseSend(), "no error on close send") 385 | 386 | resp, err := stream.Recv() 387 | s.Require().NoError(err) 388 | 389 | s.Assert().Equal("rpc error: code = Unavailable desc = backend connection failed", resp.Metadata.UpstreamError) 390 | 391 | _, err = stream.Recv() 392 | require.Equal(s.T(), io.EOF, err, "stream should close with io.EOF, meaning OK") 393 | } 394 | 395 | func (s *ProxyOne2ManySuite) TestDirectorErrorIsPropagated() { 396 | // See SetupSuite where the StreamDirector has a special case. 397 | ctx := metadata.NewOutgoingContext(s.ctx, metadata.Pairs(rejectingMdKey, "true")) 398 | _, err := s.testClient.Ping(ctx, &pb.PingRequest{Value: "foo"}) 399 | require.Error(s.T(), err, "Director should reject this RPC") 400 | assert.Equal(s.T(), codes.PermissionDenied, status.Code(err)) 401 | assert.Equal(s.T(), "testing rejection", status.Convert(err).Message()) 402 | } 403 | 404 | func (s *ProxyOne2ManySuite) TestPingStream_FullDuplexWorks() { 405 | stream, err := s.testClient.PingStream(s.ctx) 406 | require.NoError(s.T(), err, "PingStream request should be successful.") 407 | 408 | for i := range countListResponses { 409 | ping := &pb.PingRequest{Value: fmt.Sprintf("foo:%d", i)} 410 | require.NoError(s.T(), stream.Send(ping), "sending to PingStream must not fail") 411 | 412 | expectedUpstreams := map[string]struct{}{} 413 | for j := range numUpstreams { 414 | expectedUpstreams[fmt.Sprintf("server%d", j)] = struct{}{} 415 | } 416 | 417 | // each upstream should send back response 418 | for range numUpstreams { 419 | var resp *pb.MultiPingResponse 420 | resp, err = stream.Recv() 421 | s.Require().NoError(err) 422 | 423 | s.Assert().EqualValues(i, resp.Counter, "ping roundtrip must succeed with the correct id") 424 | s.Assert().EqualValues(resp.Metadata.Hostname, resp.Server) 425 | 426 | delete(expectedUpstreams, resp.Metadata.Hostname) 427 | } 428 | 429 | s.Require().Empty(expectedUpstreams) 430 | 431 | if i == 0 { 432 | // Check that the header arrives before all entries. 433 | var headerMd metadata.MD 434 | headerMd, err = stream.Header() 435 | require.NoError(s.T(), err, "PingStream headers should not error.") 436 | assert.Contains(s.T(), headerMd, serverHeaderMdKey, "PingStream response headers user contain metadata") 437 | } 438 | } 439 | 440 | require.NoError(s.T(), stream.CloseSend(), "no error on close send") 441 | _, err = stream.Recv() 442 | require.Equal(s.T(), io.EOF, err, "stream should close with io.EOF, meaning OK") 443 | // Check that the trailer headers are here. 444 | trailerMd := stream.Trailer() 445 | assert.Len(s.T(), trailerMd, 1, "PingList trailer headers user contain metadata") 446 | } 447 | 448 | //nolint:gocognit 449 | func (s *ProxyOne2ManySuite) TestPingStream_FullDuplexConcurrent() { 450 | stream, err := s.testClient.PingStream(s.ctx) 451 | require.NoError(s.T(), err, "PingStream request should be successful.") 452 | 453 | // send countListResponses requests and concurrently read numUpstreams * countListResponses replies 454 | errCh := make(chan error, 2) 455 | 456 | expectedUpstreams := map[string]int32{} 457 | 458 | for j := range numUpstreams { 459 | expectedUpstreams[fmt.Sprintf("server%d", j)] = 0 460 | } 461 | 462 | go func() { 463 | errCh <- func() error { 464 | for i := range countListResponses { 465 | ping := &pb.PingRequest{Value: fmt.Sprintf("foo:%d", i)} 466 | if err = stream.Send(ping); err != nil { 467 | return err 468 | } 469 | } 470 | 471 | return stream.CloseSend() 472 | }() 473 | }() 474 | 475 | go func() { 476 | errCh <- func() error { 477 | for range countListResponses * numUpstreams { 478 | var resp *pb.MultiPingResponse 479 | 480 | resp, err = stream.Recv() 481 | if err != nil { 482 | return err 483 | } 484 | 485 | if resp.Metadata == nil { 486 | return fmt.Errorf("response metadata expected: %v", resp) 487 | } 488 | 489 | if resp.Metadata.Hostname != resp.Server { 490 | return fmt.Errorf("mismatch on host metadata: %v != %v", resp.Metadata.Hostname, resp.Server) 491 | } 492 | 493 | expectedCounter, ok := expectedUpstreams[resp.Server] 494 | if !ok { 495 | return fmt.Errorf("unexpected host: %v", resp.Server) 496 | } 497 | 498 | if expectedCounter != resp.Counter { 499 | return fmt.Errorf("unexpected counter value: %d != %d", expectedCounter, resp.Counter) 500 | } 501 | 502 | expectedUpstreams[resp.Server]++ 503 | } 504 | 505 | return nil 506 | }() 507 | }() 508 | 509 | s.Require().NoError(<-errCh) 510 | s.Require().NoError(<-errCh) 511 | 512 | _, err = stream.Recv() 513 | require.Equal(s.T(), io.EOF, err, "stream should close with io.EOF, meaning OK") 514 | // Check that the trailer headers are here. 515 | trailerMd := stream.Trailer() 516 | assert.Len(s.T(), trailerMd, 1, "PingList trailer headers user contain metadata") 517 | } 518 | 519 | func (s *ProxyOne2ManySuite) TestPingStream_StressTest() { 520 | for range 50 { 521 | s.TestPingStream_FullDuplexWorks() 522 | } 523 | } 524 | 525 | func (s *ProxyOne2ManySuite) SetupTest() { 526 | s.ctx, s.ctxCancel = context.WithTimeout(context.TODO(), 120*time.Second) 527 | } 528 | 529 | func (s *ProxyOne2ManySuite) TearDownTest() { 530 | s.ctxCancel() 531 | } 532 | 533 | //nolint:gocognit 534 | func (s *ProxyOne2ManySuite) SetupSuite() { 535 | var err error 536 | 537 | s.proxyListener, err = net.Listen("tcp", "127.0.0.1:0") 538 | require.NoError(s.T(), err, "must be able to allocate a port for proxyListener") 539 | 540 | s.serverListeners = make([]net.Listener, numUpstreams) 541 | 542 | for i := range s.serverListeners { 543 | s.serverListeners[i], err = net.Listen("tcp", "127.0.0.1:0") 544 | require.NoError(s.T(), err, "must be able to allocate a port for serverListener") 545 | } 546 | 547 | s.servers = make([]*grpc.Server, numUpstreams) 548 | 549 | for i := range s.servers { 550 | s.servers[i] = grpc.NewServer() 551 | pb.RegisterMultiServiceServer(s.servers[i], 552 | &assertingMultiService{ 553 | t: s.T(), 554 | server: fmt.Sprintf("server%d", i), 555 | }) 556 | } 557 | 558 | backends := make([]*assertingBackend, numUpstreams) 559 | 560 | for i := range backends { 561 | var conn *grpc.ClientConn 562 | conn, err = grpc.NewClient( 563 | s.serverListeners[i].Addr().String(), 564 | grpc.WithTransportCredentials(insecure.NewCredentials()), 565 | grpc.WithDefaultCallOptions(grpc.ForceCodecV2(proxy.Codec())), 566 | ) 567 | require.NoError(s.T(), err) 568 | 569 | backends[i] = &assertingBackend{ 570 | conn: conn, 571 | i: i, 572 | } 573 | } 574 | 575 | failingBackend := &assertingBackend{ 576 | conn: nil, 577 | i: -1, 578 | } 579 | 580 | // Setup of the proxy's Director. 581 | director := func(ctx context.Context, _ string) (proxy.Mode, []proxy.Backend, error) { 582 | var targets []int 583 | 584 | md, ok := metadata.FromIncomingContext(ctx) 585 | if ok { 586 | if _, exists := md[rejectingMdKey]; exists { 587 | return proxy.One2Many, nil, status.Errorf(codes.PermissionDenied, "testing rejection") 588 | } 589 | 590 | if mdTargets, exists := md["targets"]; exists { 591 | for _, strTarget := range mdTargets { 592 | var t int 593 | 594 | t, err = strconv.Atoi(strTarget) 595 | if err != nil { 596 | return proxy.One2Many, nil, err 597 | } 598 | 599 | targets = append(targets, t) 600 | } 601 | } 602 | } 603 | 604 | var result []proxy.Backend 605 | 606 | if targets == nil { 607 | for i := range backends { 608 | targets = append(targets, i) 609 | } 610 | } 611 | 612 | for _, t := range targets { 613 | if t == -1 { 614 | result = append(result, failingBackend) 615 | } else { 616 | result = append(result, backends[t]) 617 | } 618 | } 619 | 620 | return proxy.One2Many, result, nil 621 | } 622 | 623 | s.proxy = grpc.NewServer( 624 | grpc.ForceServerCodecV2(proxy.Codec()), 625 | grpc.UnknownServiceHandler(proxy.TransparentHandler(director)), 626 | ) 627 | // Ping handler is handled as an explicit registration and not as a TransparentHandler. 628 | proxy.RegisterService(s.proxy, director, 629 | "talos.testproto.MultiService", 630 | proxy.WithMethodNames("Ping", "PingStream", "PingStreamError"), 631 | proxy.WithStreamedMethodNames("PingStream", "PingStreamError"), 632 | ) 633 | 634 | // Start the serving loops. 635 | for i := range s.servers { 636 | s.T().Logf("starting grpc.Server at: %v", s.serverListeners[i].Addr().String()) 637 | 638 | go func(i int) { 639 | s.servers[i].Serve(s.serverListeners[i]) //nolint: errcheck 640 | }(i) 641 | } 642 | 643 | s.T().Logf("starting grpc.Proxy at: %v", s.proxyListener.Addr().String()) 644 | 645 | go func() { 646 | s.proxy.Serve(s.proxyListener) //nolint: errcheck 647 | }() 648 | 649 | clientConn, err := grpc.NewClient(strings.Replace(s.proxyListener.Addr().String(), "127.0.0.1", "localhost", 1), grpc.WithTransportCredentials(insecure.NewCredentials())) 650 | require.NoError(s.T(), err, "must not error on deferred client Dial") 651 | s.testClient = pb.NewMultiServiceClient(clientConn) 652 | } 653 | 654 | func (s *ProxyOne2ManySuite) TearDownSuite() { 655 | if s.client != nil { 656 | s.Assert().NoError(s.client.Close()) 657 | } 658 | 659 | if s.serverClientConn != nil { 660 | s.Assert().NoError(s.serverClientConn.Close()) 661 | } 662 | 663 | // Close all transports so the logs don't get spammy. 664 | time.Sleep(10 * time.Millisecond) 665 | 666 | if s.proxy != nil { 667 | s.proxy.Stop() 668 | s.proxyListener.Close() //nolint: errcheck 669 | } 670 | 671 | for _, server := range s.servers { 672 | if server != nil { 673 | server.Stop() 674 | } 675 | } 676 | 677 | for _, serverListener := range s.serverListeners { 678 | if serverListener != nil { 679 | serverListener.Close() //nolint: errcheck 680 | } 681 | } 682 | } 683 | 684 | func TestProxyOne2ManySuite(t *testing.T) { 685 | suite.Run(t, &ProxyOne2ManySuite{}) 686 | } 687 | 688 | func init() { 689 | grpclog.SetLoggerV2(grpclog.NewLoggerV2(os.Stderr, os.Stderr, os.Stderr)) 690 | } 691 | -------------------------------------------------------------------------------- /proxy/handler_one2one.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Michal Witkowski. All Rights Reserved. 2 | // Copyright 2019 Andrey Smirnov. All Rights Reserved. 3 | // See LICENSE for licensing terms. 4 | 5 | package proxy 6 | 7 | import ( 8 | "errors" 9 | "io" 10 | 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/codes" 13 | "google.golang.org/grpc/status" 14 | ) 15 | 16 | func (s *handler) handlerOne2One(serverStream grpc.ServerStream, backendConnections []backendConnection) error { 17 | // case of proxying one to one: 18 | if backendConnections[0].connError != nil { 19 | return backendConnections[0].connError 20 | } 21 | 22 | // Explicitly *do not close* s2cErrChan and c2sErrChan, otherwise the select below will not terminate. 23 | // Channels do not have to be closed, it is just a control flow mechanism, see 24 | // https://groups.google.com/forum/#!msg/golang-nuts/pZwdYRGxCIk/qpbHxRRPJdUJ 25 | s2cErrChan := s.forwardServerToClient(serverStream, &backendConnections[0]) 26 | c2sErrChan := s.forwardClientToServer(&backendConnections[0], serverStream) 27 | // We don't know which side is going to stop sending first, so we need a select between the two. 28 | for range 2 { 29 | select { 30 | case s2cErr := <-s2cErrChan: 31 | if errors.Is(s2cErr, io.EOF) { 32 | // this is the happy case where the sender has encountered io.EOF, and won't be sending anymore./ 33 | // the clientStream>serverStream may continue pumping though. 34 | //nolint: errcheck 35 | backendConnections[0].clientStream.CloseSend() 36 | } else { 37 | // however, we may have gotten a receive error (stream disconnected, a read error etc) in which case we need 38 | // to cancel the clientStream to the backend, let all of its goroutines be freed up by the CancelFunc and 39 | // exit with an error to the stack 40 | return status.Errorf(codes.Internal, "failed proxying s2c: %v", s2cErr) 41 | } 42 | case c2sErr := <-c2sErrChan: 43 | // This happens when the clientStream has nothing else to offer (io.EOF), returned a gRPC error. In those two 44 | // cases we may have received Trailers as part of the call. In case of other errors (stream closed) the trailers 45 | // will be nil. 46 | serverStream.SetTrailer(backendConnections[0].clientStream.Trailer()) 47 | // c2sErr will contain RPC error from client code. If not io.EOF return the RPC error as server stream error. 48 | if !errors.Is(c2sErr, io.EOF) { 49 | return c2sErr 50 | } 51 | 52 | return nil 53 | } 54 | } 55 | 56 | return status.Errorf(codes.Internal, "gRPC proxying should never reach this stage.") 57 | } 58 | 59 | func (s *handler) forwardClientToServer(src *backendConnection, dst grpc.ServerStream) chan error { 60 | ret := make(chan error, 1) 61 | 62 | go func() { 63 | // Send the header metadata first 64 | md, err := src.clientStream.Header() 65 | if err != nil { 66 | ret <- err 67 | 68 | return 69 | } 70 | 71 | if md != nil { 72 | if err = dst.SendHeader(md); err != nil { 73 | ret <- err 74 | 75 | return 76 | } 77 | } 78 | 79 | f := NewFrame(nil) 80 | 81 | for { 82 | if err = src.clientStream.RecvMsg(f); err != nil { 83 | ret <- err // this can be io.EOF which is happy case 84 | 85 | break 86 | } 87 | 88 | if err = dst.SendMsg(f); err != nil { 89 | ret <- err 90 | 91 | break 92 | } 93 | } 94 | }() 95 | 96 | return ret 97 | } 98 | 99 | func (s *handler) forwardServerToClient(src grpc.ServerStream, dst *backendConnection) chan error { 100 | ret := make(chan error, 1) 101 | 102 | go func() { 103 | f := NewFrame(nil) 104 | 105 | for { 106 | if err := src.RecvMsg(f); err != nil { 107 | ret <- err // this can be io.EOF which is happy case 108 | 109 | break 110 | } 111 | 112 | if err := dst.clientStream.SendMsg(f); err != nil { 113 | ret <- err 114 | 115 | break 116 | } 117 | } 118 | }() 119 | 120 | return ret 121 | } 122 | -------------------------------------------------------------------------------- /proxy/handler_one2one_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Michal Witkowski. All Rights Reserved. 2 | // See LICENSE for licensing terms. 3 | 4 | package proxy_test 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net" 12 | "strings" 13 | "testing" 14 | "time" 15 | 16 | "github.com/jhump/grpctunnel" 17 | "github.com/jhump/grpctunnel/tunnelpb" 18 | "github.com/stretchr/testify/assert" 19 | "github.com/stretchr/testify/require" 20 | "github.com/stretchr/testify/suite" 21 | "google.golang.org/grpc" 22 | "google.golang.org/grpc/codes" 23 | "google.golang.org/grpc/credentials/insecure" 24 | "google.golang.org/grpc/metadata" 25 | "google.golang.org/grpc/status" 26 | "google.golang.org/protobuf/proto" 27 | 28 | "github.com/siderolabs/grpc-proxy/proxy" 29 | pb "github.com/siderolabs/grpc-proxy/testservice" 30 | ) 31 | 32 | const ( 33 | pingDefaultValue = "I like kittens." 34 | clientMdKey = "test-client-header" 35 | serverHeaderMdKey = "test-client-header" 36 | serverTrailerMdKey = "test-client-trailer" 37 | 38 | rejectingMdKey = "test-reject-rpc-if-in-context" 39 | 40 | countListResponses = 20 41 | ) 42 | 43 | // asserting service is implemented on the server side and serves as a handler for stuff. 44 | type assertingService struct { 45 | pb.UnimplementedTestServiceServer 46 | 47 | t *testing.T 48 | } 49 | 50 | func (s *assertingService) PingEmpty(ctx context.Context, _ *pb.Empty) (*pb.PingResponse, error) { 51 | // Check that this call has client's metadata. 52 | md, ok := metadata.FromIncomingContext(ctx) 53 | assert.True(s.t, ok, "PingEmpty call must have metadata in context") 54 | _, ok = md[clientMdKey] 55 | assert.True(s.t, ok, "PingEmpty call must have clients's custom headers in metadata") 56 | 57 | return &pb.PingResponse{Value: pingDefaultValue, Counter: 42}, nil 58 | } 59 | 60 | func (s *assertingService) Ping(ctx context.Context, ping *pb.PingRequest) (*pb.PingResponse, error) { 61 | // Send user trailers and headers. 62 | grpc.SendHeader(ctx, metadata.Pairs(serverHeaderMdKey, "I like turtles.")) //nolint: errcheck 63 | grpc.SetTrailer(ctx, metadata.Pairs(serverTrailerMdKey, "I like ending turtles.")) //nolint: errcheck 64 | 65 | return &pb.PingResponse{Value: ping.Value, Counter: 42}, nil 66 | } 67 | 68 | func (s *assertingService) PingError(context.Context, *pb.PingRequest) (*pb.Empty, error) { 69 | return nil, status.Errorf(codes.FailedPrecondition, "Userspace error.") 70 | } 71 | 72 | func (s *assertingService) PingList(ping *pb.PingRequest, stream pb.TestService_PingListServer) error { 73 | // Send user trailers and headers. 74 | stream.SendHeader(metadata.Pairs(serverHeaderMdKey, "I like turtles.")) //nolint: errcheck 75 | 76 | for i := range countListResponses { 77 | stream.Send(&pb.PingResponse{Value: ping.Value, Counter: int32(i)}) //nolint: errcheck 78 | } 79 | 80 | stream.SetTrailer(metadata.Pairs(serverTrailerMdKey, "I like ending turtles.")) 81 | 82 | return nil 83 | } 84 | 85 | func (s *assertingService) PingStream(stream pb.TestService_PingStreamServer) error { 86 | stream.SendHeader(metadata.Pairs(serverHeaderMdKey, "I like turtles.")) //nolint: errcheck 87 | 88 | counter := int32(0) 89 | 90 | for { 91 | ping, err := stream.Recv() 92 | if errors.Is(err, io.EOF) { 93 | break 94 | } else if err != nil { 95 | require.NoError(s.t, err, "can't fail reading stream") 96 | 97 | return err 98 | } 99 | 100 | pong := &pb.PingResponse{Value: ping.Value, Counter: counter} 101 | 102 | if err := stream.Send(pong); err != nil { 103 | require.NoError(s.t, err, "can't fail sending back a pong") 104 | } 105 | 106 | counter++ 107 | } 108 | 109 | stream.SetTrailer(metadata.Pairs(serverTrailerMdKey, "I like ending turtles.")) 110 | 111 | return nil 112 | } 113 | 114 | // ProxyOne2OneSuite tests the "happy" path of handling: that everything works in absence of connection issues. 115 | type ProxyOne2OneSuite struct { 116 | suite.Suite 117 | 118 | serverListener net.Listener 119 | server *grpc.Server 120 | proxyListener net.Listener 121 | proxy *grpc.Server 122 | serverClientConn *grpc.ClientConn 123 | 124 | tunnelHandler *grpctunnel.TunnelServiceHandler 125 | tunnelOpenedCh chan struct{} 126 | 127 | client *grpc.ClientConn 128 | testClient pb.TestServiceClient 129 | tunnelClient tunnelpb.TunnelServiceClient 130 | 131 | ctx context.Context //nolint:containedctx 132 | ctxCancel context.CancelFunc 133 | } 134 | 135 | func (s *ProxyOne2OneSuite) SetupTest() { 136 | s.ctx, s.ctxCancel = context.WithTimeout(context.TODO(), 120*time.Second) 137 | } 138 | 139 | func (s *ProxyOne2OneSuite) TearDownTest() { 140 | s.ctxCancel() 141 | } 142 | 143 | func (s *ProxyOne2OneSuite) TestPingEmptyCarriesClientMetadata() { 144 | ctx := metadata.NewOutgoingContext(s.ctx, metadata.Pairs(clientMdKey, "true")) 145 | out, err := s.testClient.PingEmpty(ctx, &pb.Empty{}) 146 | require.NoError(s.T(), err, "PingEmpty should succeed without errors") 147 | require.True(s.T(), proto.Equal(&pb.PingResponse{Value: pingDefaultValue, Counter: 42}, out)) 148 | } 149 | 150 | func (s *ProxyOne2OneSuite) TestPingEmpty_StressTest() { 151 | for range 50 { 152 | s.TestPingEmptyCarriesClientMetadata() 153 | } 154 | } 155 | 156 | func (s *ProxyOne2OneSuite) TestPingCarriesServerHeadersAndTrailers() { 157 | s.testPingCarriesServerHeadersAndTrailers(s.testClient) 158 | } 159 | 160 | func (s *ProxyOne2OneSuite) testPingCarriesServerHeadersAndTrailers(client pb.TestServiceClient) { 161 | headerMd := make(metadata.MD) 162 | trailerMd := make(metadata.MD) 163 | // This is an awkward calling convention... but meh. 164 | out, err := client.Ping(s.ctx, &pb.PingRequest{Value: "foo"}, grpc.Header(&headerMd), grpc.Trailer(&trailerMd)) 165 | require.NoError(s.T(), err, "Ping should succeed without errors") 166 | require.True(s.T(), proto.Equal(&pb.PingResponse{Value: "foo", Counter: 42}, out)) 167 | assert.Contains(s.T(), headerMd, serverHeaderMdKey, "server response headers must contain server data") 168 | assert.Len(s.T(), trailerMd, 1, "server response trailers must contain server data") 169 | } 170 | 171 | func (s *ProxyOne2OneSuite) TestPingErrorPropagatesAppError() { 172 | s.testPingErrorPropagatesAppError(s.testClient) 173 | } 174 | 175 | func (s *ProxyOne2OneSuite) testPingErrorPropagatesAppError(client pb.TestServiceClient) { 176 | _, err := client.PingError(s.ctx, &pb.PingRequest{Value: "foo"}) 177 | require.Error(s.T(), err, "PingError should never succeed") 178 | assert.Equal(s.T(), codes.FailedPrecondition, status.Code(err)) 179 | assert.Equal(s.T(), "Userspace error.", status.Convert(err).Message()) 180 | } 181 | 182 | func (s *ProxyOne2OneSuite) TestDirectorErrorIsPropagated() { 183 | // See SetupSuite where the StreamDirector has a special case. 184 | ctx := metadata.NewOutgoingContext(s.ctx, metadata.Pairs(rejectingMdKey, "true")) 185 | _, err := s.testClient.Ping(ctx, &pb.PingRequest{Value: "foo"}) 186 | require.Error(s.T(), err, "Director should reject this RPC") 187 | assert.Equal(s.T(), codes.PermissionDenied, status.Code(err)) 188 | assert.Equal(s.T(), "testing rejection", status.Convert(err).Message()) 189 | } 190 | 191 | func (s *ProxyOne2OneSuite) TestPingStream_FullDuplexWorks() { 192 | s.testStream(s.testClient) 193 | } 194 | 195 | func (s *ProxyOne2OneSuite) TestReverseTunnel() { 196 | channelServer := grpctunnel.NewReverseTunnelServer(s.tunnelClient) 197 | 198 | pb.RegisterTestServiceServer(channelServer, &assertingService{t: s.T()}) 199 | 200 | go func() { 201 | channelServer.Serve(s.ctx) //nolint:errcheck // we are not interested in the error here 202 | }() 203 | 204 | // wait for the tunnel to open 205 | select { 206 | case <-s.ctx.Done(): 207 | s.FailNow("timeout waiting for tunnel to open") 208 | case <-s.tunnelOpenedCh: 209 | } 210 | 211 | fakeConn := s.tunnelHandler.KeyAsChannel("asdf") 212 | client := pb.NewTestServiceClient(fakeConn) 213 | 214 | s.testPingCarriesServerHeadersAndTrailers(client) 215 | s.testPingErrorPropagatesAppError(client) 216 | s.testStream(client) 217 | } 218 | 219 | func (s *ProxyOne2OneSuite) testStream(client pb.TestServiceClient) { 220 | stream, err := client.PingStream(s.ctx) 221 | require.NoError(s.T(), err, "PingStream request should be successful.") 222 | 223 | for i := range countListResponses { 224 | ping := &pb.PingRequest{Value: fmt.Sprintf("foo:%d", i)} 225 | require.NoError(s.T(), stream.Send(ping), "sending to PingStream must not fail") 226 | 227 | var resp *pb.PingResponse 228 | 229 | resp, err = stream.Recv() 230 | if errors.Is(err, io.EOF) { 231 | break 232 | } 233 | 234 | if i == 0 { 235 | // Check that the header arrives before all entries. 236 | var headerMd metadata.MD 237 | headerMd, err = stream.Header() 238 | require.NoError(s.T(), err, "PingStream headers should not error.") 239 | assert.Contains(s.T(), headerMd, serverHeaderMdKey, "PingStream response headers user contain metadata") 240 | } 241 | 242 | assert.EqualValues(s.T(), i, resp.Counter, "ping roundtrip must succeed with the correct id") 243 | } 244 | 245 | require.NoError(s.T(), stream.CloseSend(), "no error on close send") 246 | _, err = stream.Recv() 247 | require.Equal(s.T(), io.EOF, err, "stream should close with io.EOF, meaining OK") 248 | // Check that the trailer headers are here. 249 | trailerMd := stream.Trailer() 250 | assert.Len(s.T(), trailerMd, 1, "PingList trailer headers user contain metadata") 251 | } 252 | 253 | func (s *ProxyOne2OneSuite) TestPingStream_StressTest() { 254 | for range 50 { 255 | s.TestPingStream_FullDuplexWorks() 256 | } 257 | } 258 | 259 | func (s *ProxyOne2OneSuite) SetupSuite() { 260 | var err error 261 | 262 | s.proxyListener, err = net.Listen("tcp", "127.0.0.1:0") 263 | require.NoError(s.T(), err, "must be able to allocate a port for proxyListener") 264 | s.serverListener, err = net.Listen("tcp", "127.0.0.1:0") 265 | require.NoError(s.T(), err, "must be able to allocate a port for serverListener") 266 | 267 | s.server = grpc.NewServer() 268 | pb.RegisterTestServiceServer(s.server, &assertingService{t: s.T()}) 269 | 270 | s.tunnelOpenedCh = make(chan struct{}) 271 | 272 | s.tunnelHandler = grpctunnel.NewTunnelServiceHandler( 273 | grpctunnel.TunnelServiceHandlerOptions{ 274 | OnReverseTunnelOpen: func(grpctunnel.TunnelChannel) { 275 | s.T().Logf("[tunnel] open reverse tunnel") 276 | }, 277 | OnReverseTunnelClose: func(grpctunnel.TunnelChannel) { 278 | s.T().Logf("[tunnel] close reverse tunnel") 279 | }, 280 | AffinityKey: func(grpctunnel.TunnelChannel) any { 281 | s.T().Logf("[tunnel] get affinity key") 282 | 283 | select { 284 | case <-s.ctx.Done(): 285 | return "fail" 286 | case s.tunnelOpenedCh <- struct{}{}: 287 | return "asdf" 288 | } 289 | }, 290 | }, 291 | ) 292 | 293 | tunnelpb.RegisterTunnelServiceServer(s.server, s.tunnelHandler.Service()) 294 | 295 | // Setup of the proxy's Director. 296 | s.serverClientConn, err = grpc.NewClient( 297 | s.serverListener.Addr().String(), 298 | grpc.WithTransportCredentials(insecure.NewCredentials()), 299 | grpc.WithDefaultCallOptions(grpc.ForceCodecV2(proxy.Codec())), 300 | ) 301 | require.NoError(s.T(), err, "must not error on deferred client Dial") 302 | 303 | director := func(ctx context.Context, _ string) (proxy.Mode, []proxy.Backend, error) { 304 | md, ok := metadata.FromIncomingContext(ctx) 305 | if ok { 306 | if _, exists := md[rejectingMdKey]; exists { 307 | return proxy.One2One, nil, status.Errorf(codes.PermissionDenied, "testing rejection") 308 | } 309 | } 310 | 311 | return proxy.One2One, []proxy.Backend{ 312 | &proxy.SingleBackend{ 313 | GetConn: func(ctx context.Context) (context.Context, *grpc.ClientConn, error) { 314 | md, _ := metadata.FromIncomingContext(ctx) 315 | // Explicitly copy the metadata, otherwise the tests will fail. 316 | outCtx := metadata.NewOutgoingContext(ctx, md.Copy()) 317 | 318 | return outCtx, s.serverClientConn, nil 319 | }, 320 | }, 321 | }, nil 322 | } 323 | 324 | s.proxy = grpc.NewServer( 325 | grpc.ForceServerCodecV2(proxy.Codec()), 326 | grpc.UnknownServiceHandler(proxy.TransparentHandler(director)), 327 | ) 328 | 329 | // Ping handler is handled as an explicit registration and not as a TransparentHandler. 330 | proxy.RegisterService(s.proxy, director, 331 | "talos.testproto.TestService", 332 | proxy.WithMethodNames("Ping"), 333 | ) 334 | 335 | // Start the serving loops. 336 | s.T().Logf("starting grpc.Server at: %v", s.serverListener.Addr().String()) 337 | 338 | go func() { 339 | s.server.Serve(s.serverListener) //nolint: errcheck 340 | }() 341 | 342 | s.T().Logf("starting grpc.Proxy at: %v", s.proxyListener.Addr().String()) 343 | 344 | go func() { 345 | s.proxy.Serve(s.proxyListener) //nolint: errcheck 346 | }() 347 | 348 | clientConn, err := grpc.NewClient( 349 | strings.Replace(s.proxyListener.Addr().String(), "127.0.0.1", "localhost", 1), 350 | grpc.WithTransportCredentials(insecure.NewCredentials()), 351 | ) 352 | require.NoError(s.T(), err, "must not error on deferred client Dial") 353 | s.testClient = pb.NewTestServiceClient(clientConn) 354 | s.tunnelClient = tunnelpb.NewTunnelServiceClient(clientConn) 355 | } 356 | 357 | func (s *ProxyOne2OneSuite) TearDownSuite() { 358 | if s.client != nil { 359 | s.client.Close() //nolint: errcheck 360 | } 361 | 362 | if s.serverClientConn != nil { 363 | s.serverClientConn.Close() //nolint: errcheck 364 | } 365 | 366 | // Close all transports so the logs don't get spammy. 367 | time.Sleep(10 * time.Millisecond) 368 | 369 | if s.proxy != nil { 370 | s.proxy.Stop() 371 | s.proxyListener.Close() //nolint: errcheck 372 | } 373 | 374 | if s.serverListener != nil { 375 | s.server.Stop() 376 | s.serverListener.Close() //nolint: errcheck 377 | } 378 | } 379 | 380 | func TestProxyOne2OneSuite(t *testing.T) { 381 | suite.Run(t, &ProxyOne2OneSuite{}) 382 | } 383 | -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Michal Witkowski. All Rights Reserved. 2 | // Copyright 2019 Andrey Smirnov. All Rights Reserved. 3 | // See LICENSE for licensing terms. 4 | 5 | package proxy 6 | 7 | import "google.golang.org/grpc" 8 | 9 | // Mode specifies proxying mode: one2one (transparent) or one2many (aggregation, error wrapping). 10 | type Mode int 11 | 12 | // Mode constants. 13 | const ( 14 | One2One Mode = iota 15 | One2Many 16 | ) 17 | 18 | // StreamedDetectorFunc reports is gRPC is doing streaming (only for one2many proxying). 19 | type StreamedDetectorFunc func(fullMethodName string) bool 20 | 21 | // Option configures gRPC proxy. 22 | type Option func(*handlerOptions) 23 | 24 | // WithMethodNames configures list of method names to proxy for non-transparent handler. 25 | func WithMethodNames(methodNames ...string) Option { 26 | return func(o *handlerOptions) { 27 | o.methodNames = append([]string(nil), methodNames...) 28 | } 29 | } 30 | 31 | // WithStreamedMethodNames configures list of streamed method names. 32 | // 33 | // This is only important for one2many proxying. 34 | // This option can't be used with TransparentHandler. 35 | func WithStreamedMethodNames(streamedMethodNames ...string) Option { 36 | return func(o *handlerOptions) { 37 | o.streamedMethods = map[string]struct{}{} 38 | 39 | for _, methodName := range streamedMethodNames { 40 | o.streamedMethods["/"+o.serviceName+"/"+methodName] = struct{}{} 41 | } 42 | 43 | o.streamedDetector = func(fullMethodName string) bool { 44 | _, exists := o.streamedMethods[fullMethodName] 45 | 46 | return exists 47 | } 48 | } 49 | } 50 | 51 | // WithStreamedDetector configures a function to detect streamed methods. 52 | // 53 | // This is only important for one2many proxying. 54 | func WithStreamedDetector(detector StreamedDetectorFunc) Option { 55 | return func(o *handlerOptions) { 56 | o.streamedDetector = detector 57 | } 58 | } 59 | 60 | // RegisterService sets up a proxy handler for a particular gRPC service and method. 61 | // The behavior is the same as if you were registering a handler method, e.g. from a codegenerated pb.go file. 62 | // 63 | // This can *only* be used if the `server` also uses grpc.CustomCodec() ServerOption. 64 | func RegisterService(server grpc.ServiceRegistrar, director StreamDirector, serviceName string, options ...Option) { 65 | streamer := &handler{ 66 | director: director, 67 | options: handlerOptions{ 68 | serviceName: serviceName, 69 | }, 70 | } 71 | 72 | for _, o := range options { 73 | o(&streamer.options) 74 | } 75 | 76 | fakeDesc := &grpc.ServiceDesc{ 77 | ServiceName: serviceName, 78 | HandlerType: (*any)(nil), 79 | } 80 | 81 | for _, m := range streamer.options.methodNames { 82 | streamDesc := grpc.StreamDesc{ 83 | StreamName: m, 84 | Handler: streamer.handler, 85 | ServerStreams: true, 86 | ClientStreams: true, 87 | } 88 | fakeDesc.Streams = append(fakeDesc.Streams, streamDesc) 89 | } 90 | 91 | server.RegisterService(fakeDesc, streamer) 92 | } 93 | 94 | // TransparentHandler returns a handler that attempts to proxy all requests that are not registered in the server. 95 | // The indented use here is as a transparent proxy, where the server doesn't know about the services implemented by the 96 | // backends. It should be used as a `grpc.UnknownServiceHandler`. 97 | // 98 | // This can *only* be used if the `server` also uses grpc.CustomCodec() ServerOption. 99 | func TransparentHandler(director StreamDirector, options ...Option) grpc.StreamHandler { 100 | streamer := &handler{ 101 | director: director, 102 | } 103 | 104 | for _, o := range options { 105 | o(&streamer.options) 106 | } 107 | 108 | return streamer.handler 109 | } 110 | -------------------------------------------------------------------------------- /proxy/serverstream.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Andrey Smirnov. All Rights Reserved. 2 | // See LICENSE for licensing terms. 3 | 4 | package proxy 5 | 6 | import ( 7 | "sync" 8 | 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/metadata" 11 | ) 12 | 13 | // ServerStreamWrapper wraps grpc.ServerStream and adds locking to the send path. 14 | type ServerStreamWrapper struct { 15 | grpc.ServerStream 16 | 17 | sendMu sync.Mutex 18 | } 19 | 20 | // SetHeader sets the header metadata. 21 | // 22 | // It may be called multiple times. 23 | // When call multiple times, all the provided metadata will be merged. 24 | // All the metadata will be sent out when one of the following happens: 25 | // - ServerStream.SendHeader() is called; 26 | // - The first response is sent out; 27 | // - An RPC status is sent out (error or success). 28 | func (wrapper *ServerStreamWrapper) SetHeader(md metadata.MD) error { 29 | wrapper.sendMu.Lock() 30 | defer wrapper.sendMu.Unlock() 31 | 32 | err := wrapper.ServerStream.SetHeader(md) 33 | if err != nil && err.Error() == "transport: the stream is done or WriteHeader was already called" { 34 | // hack: swallow grpc.internal.transport.ErrIllegalHeaderWrite 35 | err = nil 36 | } 37 | 38 | return err 39 | } 40 | 41 | // SendHeader sends the header metadata. 42 | // The provided md and headers set by SetHeader() will be sent. 43 | // It fails if called multiple times. 44 | func (wrapper *ServerStreamWrapper) SendHeader(md metadata.MD) error { 45 | wrapper.sendMu.Lock() 46 | defer wrapper.sendMu.Unlock() 47 | 48 | err := wrapper.ServerStream.SendHeader(md) 49 | if err.Error() == "transport: the stream is done or WriteHeader was already called" { 50 | // hack: swallow grpc.internal.transport.ErrIllegalHeaderWrite 51 | err = nil 52 | } 53 | 54 | return err 55 | } 56 | 57 | // SetTrailer sets the trailer metadata which will be sent with the RPC status. 58 | // When called more than once, all the provided metadata will be merged. 59 | func (wrapper *ServerStreamWrapper) SetTrailer(md metadata.MD) { 60 | wrapper.sendMu.Lock() 61 | defer wrapper.sendMu.Unlock() 62 | 63 | wrapper.ServerStream.SetTrailer(md) 64 | } 65 | 66 | // SendMsg sends a message. On error, SendMsg aborts the stream and the 67 | // error is returned directly. 68 | // 69 | // SendMsg blocks until: 70 | // - There is sufficient flow control to schedule m with the transport, or 71 | // - The stream is done, or 72 | // - The stream breaks. 73 | // 74 | // SendMsg does not wait until the message is received by the client. An 75 | // untimely stream closure may result in lost messages. 76 | // 77 | // It is safe to have a goroutine calling SendMsg and another goroutine 78 | // calling RecvMsg on the same stream at the same time, but it is not safe 79 | // to call SendMsg on the same stream in different goroutines. 80 | func (wrapper *ServerStreamWrapper) SendMsg(m any) error { 81 | wrapper.sendMu.Lock() 82 | defer wrapper.sendMu.Unlock() 83 | 84 | return wrapper.ServerStream.SendMsg(m) 85 | } 86 | -------------------------------------------------------------------------------- /testservice/api/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package talos.testproto; 4 | 5 | message Empty {} 6 | 7 | message PingRequest { string value = 1; } 8 | 9 | message PingResponse { 10 | string Value = 1; 11 | int32 counter = 2; 12 | } 13 | 14 | service TestService { 15 | rpc PingEmpty(Empty) returns (PingResponse) {} 16 | 17 | rpc Ping(PingRequest) returns (PingResponse) {} 18 | 19 | rpc PingError(PingRequest) returns (Empty) {} 20 | 21 | rpc PingList(PingRequest) returns (stream PingResponse) {} 22 | 23 | rpc PingStream(stream PingRequest) returns (stream PingResponse) {} 24 | } 25 | 26 | message ResponseMetadata { 27 | string hostname = 99; 28 | string upstream_error = 100; 29 | } 30 | 31 | message ResponseMetadataPrepender { ResponseMetadata metadata = 99; } 32 | 33 | message MultiPingResponse { 34 | ResponseMetadata metadata = 99; 35 | string Value = 1; 36 | int32 counter = 2; 37 | string server = 3; 38 | } 39 | 40 | message MultiPingReply { repeated MultiPingResponse response = 1; } 41 | 42 | message EmptyReply { repeated EmptyResponse response = 1; } 43 | 44 | message EmptyResponse { ResponseMetadata metadata = 99; } 45 | 46 | service MultiService { 47 | rpc PingEmpty(Empty) returns (MultiPingReply) {} 48 | 49 | rpc Ping(PingRequest) returns (MultiPingReply) {} 50 | 51 | rpc PingError(PingRequest) returns (EmptyReply) {} 52 | 53 | rpc PingList(PingRequest) returns (stream MultiPingResponse) {} 54 | 55 | rpc PingStream(stream PingRequest) returns (stream MultiPingResponse) {} 56 | 57 | rpc PingStreamError(stream PingRequest) returns (stream MultiPingResponse) {} 58 | } 59 | -------------------------------------------------------------------------------- /testservice/test.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.25.0 4 | // protoc v3.13.0 5 | // source: test.proto 6 | 7 | package talos_testproto 8 | 9 | import ( 10 | reflect "reflect" 11 | sync "sync" 12 | 13 | proto "github.com/golang/protobuf/proto" 14 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 15 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 16 | ) 17 | 18 | const ( 19 | // Verify that this generated code is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 21 | // Verify that runtime/protoimpl is sufficiently up-to-date. 22 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 23 | ) 24 | 25 | // This is a compile-time assertion that a sufficiently up-to-date version 26 | // of the legacy proto package is being used. 27 | const _ = proto.ProtoPackageIsVersion4 28 | 29 | type Empty struct { 30 | state protoimpl.MessageState 31 | sizeCache protoimpl.SizeCache 32 | unknownFields protoimpl.UnknownFields 33 | } 34 | 35 | func (x *Empty) Reset() { 36 | *x = Empty{} 37 | if protoimpl.UnsafeEnabled { 38 | mi := &file_test_proto_msgTypes[0] 39 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 40 | ms.StoreMessageInfo(mi) 41 | } 42 | } 43 | 44 | func (x *Empty) String() string { 45 | return protoimpl.X.MessageStringOf(x) 46 | } 47 | 48 | func (*Empty) ProtoMessage() {} 49 | 50 | func (x *Empty) ProtoReflect() protoreflect.Message { 51 | mi := &file_test_proto_msgTypes[0] 52 | if protoimpl.UnsafeEnabled && x != nil { 53 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 54 | if ms.LoadMessageInfo() == nil { 55 | ms.StoreMessageInfo(mi) 56 | } 57 | return ms 58 | } 59 | return mi.MessageOf(x) 60 | } 61 | 62 | // Deprecated: Use Empty.ProtoReflect.Descriptor instead. 63 | func (*Empty) Descriptor() ([]byte, []int) { 64 | return file_test_proto_rawDescGZIP(), []int{0} 65 | } 66 | 67 | type PingRequest struct { 68 | state protoimpl.MessageState 69 | sizeCache protoimpl.SizeCache 70 | unknownFields protoimpl.UnknownFields 71 | 72 | Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` 73 | } 74 | 75 | func (x *PingRequest) Reset() { 76 | *x = PingRequest{} 77 | if protoimpl.UnsafeEnabled { 78 | mi := &file_test_proto_msgTypes[1] 79 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 80 | ms.StoreMessageInfo(mi) 81 | } 82 | } 83 | 84 | func (x *PingRequest) String() string { 85 | return protoimpl.X.MessageStringOf(x) 86 | } 87 | 88 | func (*PingRequest) ProtoMessage() {} 89 | 90 | func (x *PingRequest) ProtoReflect() protoreflect.Message { 91 | mi := &file_test_proto_msgTypes[1] 92 | if protoimpl.UnsafeEnabled && x != nil { 93 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 94 | if ms.LoadMessageInfo() == nil { 95 | ms.StoreMessageInfo(mi) 96 | } 97 | return ms 98 | } 99 | return mi.MessageOf(x) 100 | } 101 | 102 | // Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. 103 | func (*PingRequest) Descriptor() ([]byte, []int) { 104 | return file_test_proto_rawDescGZIP(), []int{1} 105 | } 106 | 107 | func (x *PingRequest) GetValue() string { 108 | if x != nil { 109 | return x.Value 110 | } 111 | return "" 112 | } 113 | 114 | type PingResponse struct { 115 | state protoimpl.MessageState 116 | sizeCache protoimpl.SizeCache 117 | unknownFields protoimpl.UnknownFields 118 | 119 | Value string `protobuf:"bytes,1,opt,name=Value,proto3" json:"Value,omitempty"` 120 | Counter int32 `protobuf:"varint,2,opt,name=counter,proto3" json:"counter,omitempty"` 121 | } 122 | 123 | func (x *PingResponse) Reset() { 124 | *x = PingResponse{} 125 | if protoimpl.UnsafeEnabled { 126 | mi := &file_test_proto_msgTypes[2] 127 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 128 | ms.StoreMessageInfo(mi) 129 | } 130 | } 131 | 132 | func (x *PingResponse) String() string { 133 | return protoimpl.X.MessageStringOf(x) 134 | } 135 | 136 | func (*PingResponse) ProtoMessage() {} 137 | 138 | func (x *PingResponse) ProtoReflect() protoreflect.Message { 139 | mi := &file_test_proto_msgTypes[2] 140 | if protoimpl.UnsafeEnabled && x != nil { 141 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 142 | if ms.LoadMessageInfo() == nil { 143 | ms.StoreMessageInfo(mi) 144 | } 145 | return ms 146 | } 147 | return mi.MessageOf(x) 148 | } 149 | 150 | // Deprecated: Use PingResponse.ProtoReflect.Descriptor instead. 151 | func (*PingResponse) Descriptor() ([]byte, []int) { 152 | return file_test_proto_rawDescGZIP(), []int{2} 153 | } 154 | 155 | func (x *PingResponse) GetValue() string { 156 | if x != nil { 157 | return x.Value 158 | } 159 | return "" 160 | } 161 | 162 | func (x *PingResponse) GetCounter() int32 { 163 | if x != nil { 164 | return x.Counter 165 | } 166 | return 0 167 | } 168 | 169 | type ResponseMetadata struct { 170 | state protoimpl.MessageState 171 | sizeCache protoimpl.SizeCache 172 | unknownFields protoimpl.UnknownFields 173 | 174 | Hostname string `protobuf:"bytes,99,opt,name=hostname,proto3" json:"hostname,omitempty"` 175 | UpstreamError string `protobuf:"bytes,100,opt,name=upstream_error,json=upstreamError,proto3" json:"upstream_error,omitempty"` 176 | } 177 | 178 | func (x *ResponseMetadata) Reset() { 179 | *x = ResponseMetadata{} 180 | if protoimpl.UnsafeEnabled { 181 | mi := &file_test_proto_msgTypes[3] 182 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 183 | ms.StoreMessageInfo(mi) 184 | } 185 | } 186 | 187 | func (x *ResponseMetadata) String() string { 188 | return protoimpl.X.MessageStringOf(x) 189 | } 190 | 191 | func (*ResponseMetadata) ProtoMessage() {} 192 | 193 | func (x *ResponseMetadata) ProtoReflect() protoreflect.Message { 194 | mi := &file_test_proto_msgTypes[3] 195 | if protoimpl.UnsafeEnabled && x != nil { 196 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 197 | if ms.LoadMessageInfo() == nil { 198 | ms.StoreMessageInfo(mi) 199 | } 200 | return ms 201 | } 202 | return mi.MessageOf(x) 203 | } 204 | 205 | // Deprecated: Use ResponseMetadata.ProtoReflect.Descriptor instead. 206 | func (*ResponseMetadata) Descriptor() ([]byte, []int) { 207 | return file_test_proto_rawDescGZIP(), []int{3} 208 | } 209 | 210 | func (x *ResponseMetadata) GetHostname() string { 211 | if x != nil { 212 | return x.Hostname 213 | } 214 | return "" 215 | } 216 | 217 | func (x *ResponseMetadata) GetUpstreamError() string { 218 | if x != nil { 219 | return x.UpstreamError 220 | } 221 | return "" 222 | } 223 | 224 | type ResponseMetadataPrepender struct { 225 | state protoimpl.MessageState 226 | sizeCache protoimpl.SizeCache 227 | unknownFields protoimpl.UnknownFields 228 | 229 | Metadata *ResponseMetadata `protobuf:"bytes,99,opt,name=metadata,proto3" json:"metadata,omitempty"` 230 | } 231 | 232 | func (x *ResponseMetadataPrepender) Reset() { 233 | *x = ResponseMetadataPrepender{} 234 | if protoimpl.UnsafeEnabled { 235 | mi := &file_test_proto_msgTypes[4] 236 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 237 | ms.StoreMessageInfo(mi) 238 | } 239 | } 240 | 241 | func (x *ResponseMetadataPrepender) String() string { 242 | return protoimpl.X.MessageStringOf(x) 243 | } 244 | 245 | func (*ResponseMetadataPrepender) ProtoMessage() {} 246 | 247 | func (x *ResponseMetadataPrepender) ProtoReflect() protoreflect.Message { 248 | mi := &file_test_proto_msgTypes[4] 249 | if protoimpl.UnsafeEnabled && x != nil { 250 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 251 | if ms.LoadMessageInfo() == nil { 252 | ms.StoreMessageInfo(mi) 253 | } 254 | return ms 255 | } 256 | return mi.MessageOf(x) 257 | } 258 | 259 | // Deprecated: Use ResponseMetadataPrepender.ProtoReflect.Descriptor instead. 260 | func (*ResponseMetadataPrepender) Descriptor() ([]byte, []int) { 261 | return file_test_proto_rawDescGZIP(), []int{4} 262 | } 263 | 264 | func (x *ResponseMetadataPrepender) GetMetadata() *ResponseMetadata { 265 | if x != nil { 266 | return x.Metadata 267 | } 268 | return nil 269 | } 270 | 271 | type MultiPingResponse struct { 272 | state protoimpl.MessageState 273 | sizeCache protoimpl.SizeCache 274 | unknownFields protoimpl.UnknownFields 275 | 276 | Metadata *ResponseMetadata `protobuf:"bytes,99,opt,name=metadata,proto3" json:"metadata,omitempty"` 277 | Value string `protobuf:"bytes,1,opt,name=Value,proto3" json:"Value,omitempty"` 278 | Counter int32 `protobuf:"varint,2,opt,name=counter,proto3" json:"counter,omitempty"` 279 | Server string `protobuf:"bytes,3,opt,name=server,proto3" json:"server,omitempty"` 280 | } 281 | 282 | func (x *MultiPingResponse) Reset() { 283 | *x = MultiPingResponse{} 284 | if protoimpl.UnsafeEnabled { 285 | mi := &file_test_proto_msgTypes[5] 286 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 287 | ms.StoreMessageInfo(mi) 288 | } 289 | } 290 | 291 | func (x *MultiPingResponse) String() string { 292 | return protoimpl.X.MessageStringOf(x) 293 | } 294 | 295 | func (*MultiPingResponse) ProtoMessage() {} 296 | 297 | func (x *MultiPingResponse) ProtoReflect() protoreflect.Message { 298 | mi := &file_test_proto_msgTypes[5] 299 | if protoimpl.UnsafeEnabled && x != nil { 300 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 301 | if ms.LoadMessageInfo() == nil { 302 | ms.StoreMessageInfo(mi) 303 | } 304 | return ms 305 | } 306 | return mi.MessageOf(x) 307 | } 308 | 309 | // Deprecated: Use MultiPingResponse.ProtoReflect.Descriptor instead. 310 | func (*MultiPingResponse) Descriptor() ([]byte, []int) { 311 | return file_test_proto_rawDescGZIP(), []int{5} 312 | } 313 | 314 | func (x *MultiPingResponse) GetMetadata() *ResponseMetadata { 315 | if x != nil { 316 | return x.Metadata 317 | } 318 | return nil 319 | } 320 | 321 | func (x *MultiPingResponse) GetValue() string { 322 | if x != nil { 323 | return x.Value 324 | } 325 | return "" 326 | } 327 | 328 | func (x *MultiPingResponse) GetCounter() int32 { 329 | if x != nil { 330 | return x.Counter 331 | } 332 | return 0 333 | } 334 | 335 | func (x *MultiPingResponse) GetServer() string { 336 | if x != nil { 337 | return x.Server 338 | } 339 | return "" 340 | } 341 | 342 | type MultiPingReply struct { 343 | state protoimpl.MessageState 344 | sizeCache protoimpl.SizeCache 345 | unknownFields protoimpl.UnknownFields 346 | 347 | Response []*MultiPingResponse `protobuf:"bytes,1,rep,name=response,proto3" json:"response,omitempty"` 348 | } 349 | 350 | func (x *MultiPingReply) Reset() { 351 | *x = MultiPingReply{} 352 | if protoimpl.UnsafeEnabled { 353 | mi := &file_test_proto_msgTypes[6] 354 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 355 | ms.StoreMessageInfo(mi) 356 | } 357 | } 358 | 359 | func (x *MultiPingReply) String() string { 360 | return protoimpl.X.MessageStringOf(x) 361 | } 362 | 363 | func (*MultiPingReply) ProtoMessage() {} 364 | 365 | func (x *MultiPingReply) ProtoReflect() protoreflect.Message { 366 | mi := &file_test_proto_msgTypes[6] 367 | if protoimpl.UnsafeEnabled && x != nil { 368 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 369 | if ms.LoadMessageInfo() == nil { 370 | ms.StoreMessageInfo(mi) 371 | } 372 | return ms 373 | } 374 | return mi.MessageOf(x) 375 | } 376 | 377 | // Deprecated: Use MultiPingReply.ProtoReflect.Descriptor instead. 378 | func (*MultiPingReply) Descriptor() ([]byte, []int) { 379 | return file_test_proto_rawDescGZIP(), []int{6} 380 | } 381 | 382 | func (x *MultiPingReply) GetResponse() []*MultiPingResponse { 383 | if x != nil { 384 | return x.Response 385 | } 386 | return nil 387 | } 388 | 389 | type EmptyReply struct { 390 | state protoimpl.MessageState 391 | sizeCache protoimpl.SizeCache 392 | unknownFields protoimpl.UnknownFields 393 | 394 | Response []*EmptyResponse `protobuf:"bytes,1,rep,name=response,proto3" json:"response,omitempty"` 395 | } 396 | 397 | func (x *EmptyReply) Reset() { 398 | *x = EmptyReply{} 399 | if protoimpl.UnsafeEnabled { 400 | mi := &file_test_proto_msgTypes[7] 401 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 402 | ms.StoreMessageInfo(mi) 403 | } 404 | } 405 | 406 | func (x *EmptyReply) String() string { 407 | return protoimpl.X.MessageStringOf(x) 408 | } 409 | 410 | func (*EmptyReply) ProtoMessage() {} 411 | 412 | func (x *EmptyReply) ProtoReflect() protoreflect.Message { 413 | mi := &file_test_proto_msgTypes[7] 414 | if protoimpl.UnsafeEnabled && x != nil { 415 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 416 | if ms.LoadMessageInfo() == nil { 417 | ms.StoreMessageInfo(mi) 418 | } 419 | return ms 420 | } 421 | return mi.MessageOf(x) 422 | } 423 | 424 | // Deprecated: Use EmptyReply.ProtoReflect.Descriptor instead. 425 | func (*EmptyReply) Descriptor() ([]byte, []int) { 426 | return file_test_proto_rawDescGZIP(), []int{7} 427 | } 428 | 429 | func (x *EmptyReply) GetResponse() []*EmptyResponse { 430 | if x != nil { 431 | return x.Response 432 | } 433 | return nil 434 | } 435 | 436 | type EmptyResponse struct { 437 | state protoimpl.MessageState 438 | sizeCache protoimpl.SizeCache 439 | unknownFields protoimpl.UnknownFields 440 | 441 | Metadata *ResponseMetadata `protobuf:"bytes,99,opt,name=metadata,proto3" json:"metadata,omitempty"` 442 | } 443 | 444 | func (x *EmptyResponse) Reset() { 445 | *x = EmptyResponse{} 446 | if protoimpl.UnsafeEnabled { 447 | mi := &file_test_proto_msgTypes[8] 448 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 449 | ms.StoreMessageInfo(mi) 450 | } 451 | } 452 | 453 | func (x *EmptyResponse) String() string { 454 | return protoimpl.X.MessageStringOf(x) 455 | } 456 | 457 | func (*EmptyResponse) ProtoMessage() {} 458 | 459 | func (x *EmptyResponse) ProtoReflect() protoreflect.Message { 460 | mi := &file_test_proto_msgTypes[8] 461 | if protoimpl.UnsafeEnabled && x != nil { 462 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 463 | if ms.LoadMessageInfo() == nil { 464 | ms.StoreMessageInfo(mi) 465 | } 466 | return ms 467 | } 468 | return mi.MessageOf(x) 469 | } 470 | 471 | // Deprecated: Use EmptyResponse.ProtoReflect.Descriptor instead. 472 | func (*EmptyResponse) Descriptor() ([]byte, []int) { 473 | return file_test_proto_rawDescGZIP(), []int{8} 474 | } 475 | 476 | func (x *EmptyResponse) GetMetadata() *ResponseMetadata { 477 | if x != nil { 478 | return x.Metadata 479 | } 480 | return nil 481 | } 482 | 483 | var File_test_proto protoreflect.FileDescriptor 484 | 485 | var file_test_proto_rawDesc = []byte{ 486 | 0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x74, 0x61, 487 | 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x07, 0x0a, 488 | 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x23, 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 489 | 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 490 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3e, 0x0a, 0x0c, 0x50, 491 | 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 492 | 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 493 | 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 494 | 0x28, 0x05, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x22, 0x55, 0x0a, 0x10, 0x52, 495 | 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 496 | 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x63, 0x20, 0x01, 0x28, 497 | 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x75, 498 | 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x64, 0x20, 499 | 0x01, 0x28, 0x09, 0x52, 0x0d, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x72, 0x72, 500 | 0x6f, 0x72, 0x22, 0x5a, 0x0a, 0x19, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, 501 | 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x50, 0x72, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 502 | 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x63, 0x20, 0x01, 0x28, 503 | 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 504 | 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 505 | 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x9a, 506 | 0x01, 0x0a, 0x11, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 507 | 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 508 | 0x18, 0x63, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 509 | 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 510 | 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 511 | 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 512 | 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 513 | 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 514 | 0x74, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x03, 0x20, 515 | 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x50, 0x0a, 0x0e, 0x4d, 516 | 0x75, 0x6c, 0x74, 0x69, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x3e, 0x0a, 517 | 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 518 | 0x22, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 519 | 0x6f, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 520 | 0x6e, 0x73, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x48, 0x0a, 521 | 0x0a, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x3a, 0x0a, 0x08, 0x72, 522 | 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 523 | 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 524 | 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x08, 0x72, 525 | 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4e, 0x0a, 0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 526 | 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 527 | 0x64, 0x61, 0x74, 0x61, 0x18, 0x63, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x61, 0x6c, 528 | 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x73, 529 | 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 530 | 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x32, 0xfd, 0x02, 0x0a, 0x0b, 0x54, 0x65, 0x73, 0x74, 531 | 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x50, 0x69, 0x6e, 0x67, 0x45, 532 | 0x6d, 0x70, 0x74, 0x79, 0x12, 0x16, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 533 | 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x74, 534 | 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 535 | 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 536 | 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 537 | 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 538 | 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 539 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 540 | 0x73, 0x65, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x09, 0x50, 0x69, 0x6e, 0x67, 0x45, 0x72, 0x72, 0x6f, 541 | 0x72, 0x12, 0x1c, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 542 | 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 543 | 0x16, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 544 | 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x08, 0x50, 0x69, 0x6e, 545 | 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 546 | 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 547 | 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 548 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 549 | 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x4f, 0x0a, 0x0a, 0x50, 0x69, 0x6e, 0x67, 0x53, 0x74, 550 | 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1c, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 551 | 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 552 | 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 553 | 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 554 | 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x32, 0xec, 0x03, 0x0a, 0x0c, 0x4d, 0x75, 0x6c, 0x74, 555 | 0x69, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x46, 0x0a, 0x09, 0x50, 0x69, 0x6e, 0x67, 556 | 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x16, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 557 | 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, 558 | 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 559 | 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 560 | 0x12, 0x47, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 561 | 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 562 | 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 563 | 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x50, 0x69, 564 | 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x09, 0x50, 0x69, 0x6e, 565 | 0x67, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1c, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 566 | 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 567 | 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 568 | 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x70, 0x6c, 569 | 0x79, 0x22, 0x00, 0x12, 0x50, 0x0a, 0x08, 0x50, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 570 | 0x1c, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 571 | 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 572 | 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 573 | 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 574 | 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x54, 0x0a, 0x0a, 0x50, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x72, 575 | 0x65, 0x61, 0x6d, 0x12, 0x1c, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 576 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 577 | 0x74, 0x1a, 0x22, 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 578 | 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 579 | 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x59, 0x0a, 0x0f, 0x50, 580 | 0x69, 0x6e, 0x67, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1c, 581 | 0x2e, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 582 | 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, 583 | 0x61, 0x6c, 0x6f, 0x73, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 584 | 0x75, 0x6c, 0x74, 0x69, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 585 | 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 586 | } 587 | 588 | var ( 589 | file_test_proto_rawDescOnce sync.Once 590 | file_test_proto_rawDescData = file_test_proto_rawDesc 591 | ) 592 | 593 | func file_test_proto_rawDescGZIP() []byte { 594 | file_test_proto_rawDescOnce.Do(func() { 595 | file_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_proto_rawDescData) 596 | }) 597 | return file_test_proto_rawDescData 598 | } 599 | 600 | var ( 601 | file_test_proto_msgTypes = make([]protoimpl.MessageInfo, 9) 602 | file_test_proto_goTypes = []interface{}{ 603 | (*Empty)(nil), // 0: talos.testproto.Empty 604 | (*PingRequest)(nil), // 1: talos.testproto.PingRequest 605 | (*PingResponse)(nil), // 2: talos.testproto.PingResponse 606 | (*ResponseMetadata)(nil), // 3: talos.testproto.ResponseMetadata 607 | (*ResponseMetadataPrepender)(nil), // 4: talos.testproto.ResponseMetadataPrepender 608 | (*MultiPingResponse)(nil), // 5: talos.testproto.MultiPingResponse 609 | (*MultiPingReply)(nil), // 6: talos.testproto.MultiPingReply 610 | (*EmptyReply)(nil), // 7: talos.testproto.EmptyReply 611 | (*EmptyResponse)(nil), // 8: talos.testproto.EmptyResponse 612 | } 613 | ) 614 | var file_test_proto_depIdxs = []int32{ 615 | 3, // 0: talos.testproto.ResponseMetadataPrepender.metadata:type_name -> talos.testproto.ResponseMetadata 616 | 3, // 1: talos.testproto.MultiPingResponse.metadata:type_name -> talos.testproto.ResponseMetadata 617 | 5, // 2: talos.testproto.MultiPingReply.response:type_name -> talos.testproto.MultiPingResponse 618 | 8, // 3: talos.testproto.EmptyReply.response:type_name -> talos.testproto.EmptyResponse 619 | 3, // 4: talos.testproto.EmptyResponse.metadata:type_name -> talos.testproto.ResponseMetadata 620 | 0, // 5: talos.testproto.TestService.PingEmpty:input_type -> talos.testproto.Empty 621 | 1, // 6: talos.testproto.TestService.Ping:input_type -> talos.testproto.PingRequest 622 | 1, // 7: talos.testproto.TestService.PingError:input_type -> talos.testproto.PingRequest 623 | 1, // 8: talos.testproto.TestService.PingList:input_type -> talos.testproto.PingRequest 624 | 1, // 9: talos.testproto.TestService.PingStream:input_type -> talos.testproto.PingRequest 625 | 0, // 10: talos.testproto.MultiService.PingEmpty:input_type -> talos.testproto.Empty 626 | 1, // 11: talos.testproto.MultiService.Ping:input_type -> talos.testproto.PingRequest 627 | 1, // 12: talos.testproto.MultiService.PingError:input_type -> talos.testproto.PingRequest 628 | 1, // 13: talos.testproto.MultiService.PingList:input_type -> talos.testproto.PingRequest 629 | 1, // 14: talos.testproto.MultiService.PingStream:input_type -> talos.testproto.PingRequest 630 | 1, // 15: talos.testproto.MultiService.PingStreamError:input_type -> talos.testproto.PingRequest 631 | 2, // 16: talos.testproto.TestService.PingEmpty:output_type -> talos.testproto.PingResponse 632 | 2, // 17: talos.testproto.TestService.Ping:output_type -> talos.testproto.PingResponse 633 | 0, // 18: talos.testproto.TestService.PingError:output_type -> talos.testproto.Empty 634 | 2, // 19: talos.testproto.TestService.PingList:output_type -> talos.testproto.PingResponse 635 | 2, // 20: talos.testproto.TestService.PingStream:output_type -> talos.testproto.PingResponse 636 | 6, // 21: talos.testproto.MultiService.PingEmpty:output_type -> talos.testproto.MultiPingReply 637 | 6, // 22: talos.testproto.MultiService.Ping:output_type -> talos.testproto.MultiPingReply 638 | 7, // 23: talos.testproto.MultiService.PingError:output_type -> talos.testproto.EmptyReply 639 | 5, // 24: talos.testproto.MultiService.PingList:output_type -> talos.testproto.MultiPingResponse 640 | 5, // 25: talos.testproto.MultiService.PingStream:output_type -> talos.testproto.MultiPingResponse 641 | 5, // 26: talos.testproto.MultiService.PingStreamError:output_type -> talos.testproto.MultiPingResponse 642 | 16, // [16:27] is the sub-list for method output_type 643 | 5, // [5:16] is the sub-list for method input_type 644 | 5, // [5:5] is the sub-list for extension type_name 645 | 5, // [5:5] is the sub-list for extension extendee 646 | 0, // [0:5] is the sub-list for field type_name 647 | } 648 | 649 | func init() { file_test_proto_init() } 650 | func file_test_proto_init() { 651 | if File_test_proto != nil { 652 | return 653 | } 654 | if !protoimpl.UnsafeEnabled { 655 | file_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 656 | switch v := v.(*Empty); i { 657 | case 0: 658 | return &v.state 659 | case 1: 660 | return &v.sizeCache 661 | case 2: 662 | return &v.unknownFields 663 | default: 664 | return nil 665 | } 666 | } 667 | file_test_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 668 | switch v := v.(*PingRequest); i { 669 | case 0: 670 | return &v.state 671 | case 1: 672 | return &v.sizeCache 673 | case 2: 674 | return &v.unknownFields 675 | default: 676 | return nil 677 | } 678 | } 679 | file_test_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 680 | switch v := v.(*PingResponse); i { 681 | case 0: 682 | return &v.state 683 | case 1: 684 | return &v.sizeCache 685 | case 2: 686 | return &v.unknownFields 687 | default: 688 | return nil 689 | } 690 | } 691 | file_test_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { 692 | switch v := v.(*ResponseMetadata); i { 693 | case 0: 694 | return &v.state 695 | case 1: 696 | return &v.sizeCache 697 | case 2: 698 | return &v.unknownFields 699 | default: 700 | return nil 701 | } 702 | } 703 | file_test_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { 704 | switch v := v.(*ResponseMetadataPrepender); i { 705 | case 0: 706 | return &v.state 707 | case 1: 708 | return &v.sizeCache 709 | case 2: 710 | return &v.unknownFields 711 | default: 712 | return nil 713 | } 714 | } 715 | file_test_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { 716 | switch v := v.(*MultiPingResponse); i { 717 | case 0: 718 | return &v.state 719 | case 1: 720 | return &v.sizeCache 721 | case 2: 722 | return &v.unknownFields 723 | default: 724 | return nil 725 | } 726 | } 727 | file_test_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { 728 | switch v := v.(*MultiPingReply); i { 729 | case 0: 730 | return &v.state 731 | case 1: 732 | return &v.sizeCache 733 | case 2: 734 | return &v.unknownFields 735 | default: 736 | return nil 737 | } 738 | } 739 | file_test_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { 740 | switch v := v.(*EmptyReply); i { 741 | case 0: 742 | return &v.state 743 | case 1: 744 | return &v.sizeCache 745 | case 2: 746 | return &v.unknownFields 747 | default: 748 | return nil 749 | } 750 | } 751 | file_test_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { 752 | switch v := v.(*EmptyResponse); i { 753 | case 0: 754 | return &v.state 755 | case 1: 756 | return &v.sizeCache 757 | case 2: 758 | return &v.unknownFields 759 | default: 760 | return nil 761 | } 762 | } 763 | } 764 | type x struct{} 765 | out := protoimpl.TypeBuilder{ 766 | File: protoimpl.DescBuilder{ 767 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 768 | RawDescriptor: file_test_proto_rawDesc, 769 | NumEnums: 0, 770 | NumMessages: 9, 771 | NumExtensions: 0, 772 | NumServices: 2, 773 | }, 774 | GoTypes: file_test_proto_goTypes, 775 | DependencyIndexes: file_test_proto_depIdxs, 776 | MessageInfos: file_test_proto_msgTypes, 777 | }.Build() 778 | File_test_proto = out.File 779 | file_test_proto_rawDesc = nil 780 | file_test_proto_goTypes = nil 781 | file_test_proto_depIdxs = nil 782 | } 783 | -------------------------------------------------------------------------------- /testservice/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package talos.testproto; 4 | 5 | message Empty {} 6 | 7 | message PingRequest { string value = 1; } 8 | 9 | message PingResponse { 10 | string Value = 1; 11 | int32 counter = 2; 12 | } 13 | 14 | service TestService { 15 | rpc PingEmpty(Empty) returns (PingResponse) {} 16 | 17 | rpc Ping(PingRequest) returns (PingResponse) {} 18 | 19 | rpc PingError(PingRequest) returns (Empty) {} 20 | 21 | rpc PingList(PingRequest) returns (stream PingResponse) {} 22 | 23 | rpc PingStream(stream PingRequest) returns (stream PingResponse) {} 24 | } 25 | 26 | message ResponseMetadata { 27 | string hostname = 99; 28 | string upstream_error = 100; 29 | } 30 | 31 | message ResponseMetadataPrepender { ResponseMetadata metadata = 99; } 32 | 33 | message MultiPingResponse { 34 | ResponseMetadata metadata = 99; 35 | string Value = 1; 36 | int32 counter = 2; 37 | string server = 3; 38 | } 39 | 40 | message MultiPingReply { repeated MultiPingResponse response = 1; } 41 | 42 | message EmptyReply { repeated EmptyResponse response = 1; } 43 | 44 | message EmptyResponse { ResponseMetadata metadata = 99; } 45 | 46 | service MultiService { 47 | rpc PingEmpty(Empty) returns (MultiPingReply) {} 48 | 49 | rpc Ping(PingRequest) returns (MultiPingReply) {} 50 | 51 | rpc PingError(PingRequest) returns (EmptyReply) {} 52 | 53 | rpc PingList(PingRequest) returns (stream MultiPingResponse) {} 54 | 55 | rpc PingStream(stream PingRequest) returns (stream MultiPingResponse) {} 56 | 57 | rpc PingStreamError(stream PingRequest) returns (stream MultiPingResponse) {} 58 | } 59 | -------------------------------------------------------------------------------- /testservice/test_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package talos_testproto 4 | 5 | import ( 6 | context "context" 7 | 8 | grpc "google.golang.org/grpc" 9 | codes "google.golang.org/grpc/codes" 10 | status "google.golang.org/grpc/status" 11 | ) 12 | 13 | // This is a compile-time assertion to ensure that this generated file 14 | // is compatible with the grpc package it is being compiled against. 15 | // Requires gRPC-Go v1.32.0 or later. 16 | const _ = grpc.SupportPackageIsVersion7 17 | 18 | // TestServiceClient is the client API for TestService service. 19 | // 20 | // 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. 21 | type TestServiceClient interface { 22 | PingEmpty(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*PingResponse, error) 23 | Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) 24 | PingError(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*Empty, error) 25 | PingList(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (TestService_PingListClient, error) 26 | PingStream(ctx context.Context, opts ...grpc.CallOption) (TestService_PingStreamClient, error) 27 | } 28 | 29 | type testServiceClient struct { 30 | cc grpc.ClientConnInterface 31 | } 32 | 33 | func NewTestServiceClient(cc grpc.ClientConnInterface) TestServiceClient { 34 | return &testServiceClient{cc} 35 | } 36 | 37 | func (c *testServiceClient) PingEmpty(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*PingResponse, error) { 38 | out := new(PingResponse) 39 | err := c.cc.Invoke(ctx, "/talos.testproto.TestService/PingEmpty", in, out, opts...) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return out, nil 44 | } 45 | 46 | func (c *testServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) { 47 | out := new(PingResponse) 48 | err := c.cc.Invoke(ctx, "/talos.testproto.TestService/Ping", in, out, opts...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return out, nil 53 | } 54 | 55 | func (c *testServiceClient) PingError(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*Empty, error) { 56 | out := new(Empty) 57 | err := c.cc.Invoke(ctx, "/talos.testproto.TestService/PingError", in, out, opts...) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return out, nil 62 | } 63 | 64 | func (c *testServiceClient) PingList(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (TestService_PingListClient, error) { 65 | stream, err := c.cc.NewStream(ctx, &TestService_ServiceDesc.Streams[0], "/talos.testproto.TestService/PingList", opts...) 66 | if err != nil { 67 | return nil, err 68 | } 69 | x := &testServicePingListClient{stream} 70 | if err := x.ClientStream.SendMsg(in); err != nil { 71 | return nil, err 72 | } 73 | if err := x.ClientStream.CloseSend(); err != nil { 74 | return nil, err 75 | } 76 | return x, nil 77 | } 78 | 79 | type TestService_PingListClient interface { 80 | Recv() (*PingResponse, error) 81 | grpc.ClientStream 82 | } 83 | 84 | type testServicePingListClient struct { 85 | grpc.ClientStream 86 | } 87 | 88 | func (x *testServicePingListClient) Recv() (*PingResponse, error) { 89 | m := new(PingResponse) 90 | if err := x.ClientStream.RecvMsg(m); err != nil { 91 | return nil, err 92 | } 93 | return m, nil 94 | } 95 | 96 | func (c *testServiceClient) PingStream(ctx context.Context, opts ...grpc.CallOption) (TestService_PingStreamClient, error) { 97 | stream, err := c.cc.NewStream(ctx, &TestService_ServiceDesc.Streams[1], "/talos.testproto.TestService/PingStream", opts...) 98 | if err != nil { 99 | return nil, err 100 | } 101 | x := &testServicePingStreamClient{stream} 102 | return x, nil 103 | } 104 | 105 | type TestService_PingStreamClient interface { 106 | Send(*PingRequest) error 107 | Recv() (*PingResponse, error) 108 | grpc.ClientStream 109 | } 110 | 111 | type testServicePingStreamClient struct { 112 | grpc.ClientStream 113 | } 114 | 115 | func (x *testServicePingStreamClient) Send(m *PingRequest) error { 116 | return x.ClientStream.SendMsg(m) 117 | } 118 | 119 | func (x *testServicePingStreamClient) Recv() (*PingResponse, error) { 120 | m := new(PingResponse) 121 | if err := x.ClientStream.RecvMsg(m); err != nil { 122 | return nil, err 123 | } 124 | return m, nil 125 | } 126 | 127 | // TestServiceServer is the server API for TestService service. 128 | // All implementations must embed UnimplementedTestServiceServer 129 | // for forward compatibility 130 | type TestServiceServer interface { 131 | PingEmpty(context.Context, *Empty) (*PingResponse, error) 132 | Ping(context.Context, *PingRequest) (*PingResponse, error) 133 | PingError(context.Context, *PingRequest) (*Empty, error) 134 | PingList(*PingRequest, TestService_PingListServer) error 135 | PingStream(TestService_PingStreamServer) error 136 | mustEmbedUnimplementedTestServiceServer() 137 | } 138 | 139 | // UnimplementedTestServiceServer must be embedded to have forward compatible implementations. 140 | type UnimplementedTestServiceServer struct{} 141 | 142 | func (UnimplementedTestServiceServer) PingEmpty(context.Context, *Empty) (*PingResponse, error) { 143 | return nil, status.Errorf(codes.Unimplemented, "method PingEmpty not implemented") 144 | } 145 | 146 | func (UnimplementedTestServiceServer) Ping(context.Context, *PingRequest) (*PingResponse, error) { 147 | return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented") 148 | } 149 | 150 | func (UnimplementedTestServiceServer) PingError(context.Context, *PingRequest) (*Empty, error) { 151 | return nil, status.Errorf(codes.Unimplemented, "method PingError not implemented") 152 | } 153 | 154 | func (UnimplementedTestServiceServer) PingList(*PingRequest, TestService_PingListServer) error { 155 | return status.Errorf(codes.Unimplemented, "method PingList not implemented") 156 | } 157 | 158 | func (UnimplementedTestServiceServer) PingStream(TestService_PingStreamServer) error { 159 | return status.Errorf(codes.Unimplemented, "method PingStream not implemented") 160 | } 161 | func (UnimplementedTestServiceServer) mustEmbedUnimplementedTestServiceServer() {} 162 | 163 | // UnsafeTestServiceServer may be embedded to opt out of forward compatibility for this service. 164 | // Use of this interface is not recommended, as added methods to TestServiceServer will 165 | // result in compilation errors. 166 | type UnsafeTestServiceServer interface { 167 | mustEmbedUnimplementedTestServiceServer() 168 | } 169 | 170 | func RegisterTestServiceServer(s grpc.ServiceRegistrar, srv TestServiceServer) { 171 | s.RegisterService(&TestService_ServiceDesc, srv) 172 | } 173 | 174 | func _TestService_PingEmpty_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 175 | in := new(Empty) 176 | if err := dec(in); err != nil { 177 | return nil, err 178 | } 179 | if interceptor == nil { 180 | return srv.(TestServiceServer).PingEmpty(ctx, in) 181 | } 182 | info := &grpc.UnaryServerInfo{ 183 | Server: srv, 184 | FullMethod: "/talos.testproto.TestService/PingEmpty", 185 | } 186 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 187 | return srv.(TestServiceServer).PingEmpty(ctx, req.(*Empty)) 188 | } 189 | return interceptor(ctx, in, info, handler) 190 | } 191 | 192 | func _TestService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 193 | in := new(PingRequest) 194 | if err := dec(in); err != nil { 195 | return nil, err 196 | } 197 | if interceptor == nil { 198 | return srv.(TestServiceServer).Ping(ctx, in) 199 | } 200 | info := &grpc.UnaryServerInfo{ 201 | Server: srv, 202 | FullMethod: "/talos.testproto.TestService/Ping", 203 | } 204 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 205 | return srv.(TestServiceServer).Ping(ctx, req.(*PingRequest)) 206 | } 207 | return interceptor(ctx, in, info, handler) 208 | } 209 | 210 | func _TestService_PingError_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 211 | in := new(PingRequest) 212 | if err := dec(in); err != nil { 213 | return nil, err 214 | } 215 | if interceptor == nil { 216 | return srv.(TestServiceServer).PingError(ctx, in) 217 | } 218 | info := &grpc.UnaryServerInfo{ 219 | Server: srv, 220 | FullMethod: "/talos.testproto.TestService/PingError", 221 | } 222 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 223 | return srv.(TestServiceServer).PingError(ctx, req.(*PingRequest)) 224 | } 225 | return interceptor(ctx, in, info, handler) 226 | } 227 | 228 | func _TestService_PingList_Handler(srv interface{}, stream grpc.ServerStream) error { 229 | m := new(PingRequest) 230 | if err := stream.RecvMsg(m); err != nil { 231 | return err 232 | } 233 | return srv.(TestServiceServer).PingList(m, &testServicePingListServer{stream}) 234 | } 235 | 236 | type TestService_PingListServer interface { 237 | Send(*PingResponse) error 238 | grpc.ServerStream 239 | } 240 | 241 | type testServicePingListServer struct { 242 | grpc.ServerStream 243 | } 244 | 245 | func (x *testServicePingListServer) Send(m *PingResponse) error { 246 | return x.ServerStream.SendMsg(m) 247 | } 248 | 249 | func _TestService_PingStream_Handler(srv interface{}, stream grpc.ServerStream) error { 250 | return srv.(TestServiceServer).PingStream(&testServicePingStreamServer{stream}) 251 | } 252 | 253 | type TestService_PingStreamServer interface { 254 | Send(*PingResponse) error 255 | Recv() (*PingRequest, error) 256 | grpc.ServerStream 257 | } 258 | 259 | type testServicePingStreamServer struct { 260 | grpc.ServerStream 261 | } 262 | 263 | func (x *testServicePingStreamServer) Send(m *PingResponse) error { 264 | return x.ServerStream.SendMsg(m) 265 | } 266 | 267 | func (x *testServicePingStreamServer) Recv() (*PingRequest, error) { 268 | m := new(PingRequest) 269 | if err := x.ServerStream.RecvMsg(m); err != nil { 270 | return nil, err 271 | } 272 | return m, nil 273 | } 274 | 275 | // TestService_ServiceDesc is the grpc.ServiceDesc for TestService service. 276 | // It's only intended for direct use with grpc.RegisterService, 277 | // and not to be introspected or modified (even as a copy) 278 | var TestService_ServiceDesc = grpc.ServiceDesc{ 279 | ServiceName: "talos.testproto.TestService", 280 | HandlerType: (*TestServiceServer)(nil), 281 | Methods: []grpc.MethodDesc{ 282 | { 283 | MethodName: "PingEmpty", 284 | Handler: _TestService_PingEmpty_Handler, 285 | }, 286 | { 287 | MethodName: "Ping", 288 | Handler: _TestService_Ping_Handler, 289 | }, 290 | { 291 | MethodName: "PingError", 292 | Handler: _TestService_PingError_Handler, 293 | }, 294 | }, 295 | Streams: []grpc.StreamDesc{ 296 | { 297 | StreamName: "PingList", 298 | Handler: _TestService_PingList_Handler, 299 | ServerStreams: true, 300 | }, 301 | { 302 | StreamName: "PingStream", 303 | Handler: _TestService_PingStream_Handler, 304 | ServerStreams: true, 305 | ClientStreams: true, 306 | }, 307 | }, 308 | Metadata: "test.proto", 309 | } 310 | 311 | // MultiServiceClient is the client API for MultiService service. 312 | // 313 | // 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. 314 | type MultiServiceClient interface { 315 | PingEmpty(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*MultiPingReply, error) 316 | Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*MultiPingReply, error) 317 | PingError(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*EmptyReply, error) 318 | PingList(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (MultiService_PingListClient, error) 319 | PingStream(ctx context.Context, opts ...grpc.CallOption) (MultiService_PingStreamClient, error) 320 | PingStreamError(ctx context.Context, opts ...grpc.CallOption) (MultiService_PingStreamErrorClient, error) 321 | } 322 | 323 | type multiServiceClient struct { 324 | cc grpc.ClientConnInterface 325 | } 326 | 327 | func NewMultiServiceClient(cc grpc.ClientConnInterface) MultiServiceClient { 328 | return &multiServiceClient{cc} 329 | } 330 | 331 | func (c *multiServiceClient) PingEmpty(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*MultiPingReply, error) { 332 | out := new(MultiPingReply) 333 | err := c.cc.Invoke(ctx, "/talos.testproto.MultiService/PingEmpty", in, out, opts...) 334 | if err != nil { 335 | return nil, err 336 | } 337 | return out, nil 338 | } 339 | 340 | func (c *multiServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*MultiPingReply, error) { 341 | out := new(MultiPingReply) 342 | err := c.cc.Invoke(ctx, "/talos.testproto.MultiService/Ping", in, out, opts...) 343 | if err != nil { 344 | return nil, err 345 | } 346 | return out, nil 347 | } 348 | 349 | func (c *multiServiceClient) PingError(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*EmptyReply, error) { 350 | out := new(EmptyReply) 351 | err := c.cc.Invoke(ctx, "/talos.testproto.MultiService/PingError", in, out, opts...) 352 | if err != nil { 353 | return nil, err 354 | } 355 | return out, nil 356 | } 357 | 358 | func (c *multiServiceClient) PingList(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (MultiService_PingListClient, error) { 359 | stream, err := c.cc.NewStream(ctx, &MultiService_ServiceDesc.Streams[0], "/talos.testproto.MultiService/PingList", opts...) 360 | if err != nil { 361 | return nil, err 362 | } 363 | x := &multiServicePingListClient{stream} 364 | if err := x.ClientStream.SendMsg(in); err != nil { 365 | return nil, err 366 | } 367 | if err := x.ClientStream.CloseSend(); err != nil { 368 | return nil, err 369 | } 370 | return x, nil 371 | } 372 | 373 | type MultiService_PingListClient interface { 374 | Recv() (*MultiPingResponse, error) 375 | grpc.ClientStream 376 | } 377 | 378 | type multiServicePingListClient struct { 379 | grpc.ClientStream 380 | } 381 | 382 | func (x *multiServicePingListClient) Recv() (*MultiPingResponse, error) { 383 | m := new(MultiPingResponse) 384 | if err := x.ClientStream.RecvMsg(m); err != nil { 385 | return nil, err 386 | } 387 | return m, nil 388 | } 389 | 390 | func (c *multiServiceClient) PingStream(ctx context.Context, opts ...grpc.CallOption) (MultiService_PingStreamClient, error) { 391 | stream, err := c.cc.NewStream(ctx, &MultiService_ServiceDesc.Streams[1], "/talos.testproto.MultiService/PingStream", opts...) 392 | if err != nil { 393 | return nil, err 394 | } 395 | x := &multiServicePingStreamClient{stream} 396 | return x, nil 397 | } 398 | 399 | type MultiService_PingStreamClient interface { 400 | Send(*PingRequest) error 401 | Recv() (*MultiPingResponse, error) 402 | grpc.ClientStream 403 | } 404 | 405 | type multiServicePingStreamClient struct { 406 | grpc.ClientStream 407 | } 408 | 409 | func (x *multiServicePingStreamClient) Send(m *PingRequest) error { 410 | return x.ClientStream.SendMsg(m) 411 | } 412 | 413 | func (x *multiServicePingStreamClient) Recv() (*MultiPingResponse, error) { 414 | m := new(MultiPingResponse) 415 | if err := x.ClientStream.RecvMsg(m); err != nil { 416 | return nil, err 417 | } 418 | return m, nil 419 | } 420 | 421 | func (c *multiServiceClient) PingStreamError(ctx context.Context, opts ...grpc.CallOption) (MultiService_PingStreamErrorClient, error) { 422 | stream, err := c.cc.NewStream(ctx, &MultiService_ServiceDesc.Streams[2], "/talos.testproto.MultiService/PingStreamError", opts...) 423 | if err != nil { 424 | return nil, err 425 | } 426 | x := &multiServicePingStreamErrorClient{stream} 427 | return x, nil 428 | } 429 | 430 | type MultiService_PingStreamErrorClient interface { 431 | Send(*PingRequest) error 432 | Recv() (*MultiPingResponse, error) 433 | grpc.ClientStream 434 | } 435 | 436 | type multiServicePingStreamErrorClient struct { 437 | grpc.ClientStream 438 | } 439 | 440 | func (x *multiServicePingStreamErrorClient) Send(m *PingRequest) error { 441 | return x.ClientStream.SendMsg(m) 442 | } 443 | 444 | func (x *multiServicePingStreamErrorClient) Recv() (*MultiPingResponse, error) { 445 | m := new(MultiPingResponse) 446 | if err := x.ClientStream.RecvMsg(m); err != nil { 447 | return nil, err 448 | } 449 | return m, nil 450 | } 451 | 452 | // MultiServiceServer is the server API for MultiService service. 453 | // All implementations must embed UnimplementedMultiServiceServer 454 | // for forward compatibility 455 | type MultiServiceServer interface { 456 | PingEmpty(context.Context, *Empty) (*MultiPingReply, error) 457 | Ping(context.Context, *PingRequest) (*MultiPingReply, error) 458 | PingError(context.Context, *PingRequest) (*EmptyReply, error) 459 | PingList(*PingRequest, MultiService_PingListServer) error 460 | PingStream(MultiService_PingStreamServer) error 461 | PingStreamError(MultiService_PingStreamErrorServer) error 462 | mustEmbedUnimplementedMultiServiceServer() 463 | } 464 | 465 | // UnimplementedMultiServiceServer must be embedded to have forward compatible implementations. 466 | type UnimplementedMultiServiceServer struct{} 467 | 468 | func (UnimplementedMultiServiceServer) PingEmpty(context.Context, *Empty) (*MultiPingReply, error) { 469 | return nil, status.Errorf(codes.Unimplemented, "method PingEmpty not implemented") 470 | } 471 | 472 | func (UnimplementedMultiServiceServer) Ping(context.Context, *PingRequest) (*MultiPingReply, error) { 473 | return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented") 474 | } 475 | 476 | func (UnimplementedMultiServiceServer) PingError(context.Context, *PingRequest) (*EmptyReply, error) { 477 | return nil, status.Errorf(codes.Unimplemented, "method PingError not implemented") 478 | } 479 | 480 | func (UnimplementedMultiServiceServer) PingList(*PingRequest, MultiService_PingListServer) error { 481 | return status.Errorf(codes.Unimplemented, "method PingList not implemented") 482 | } 483 | 484 | func (UnimplementedMultiServiceServer) PingStream(MultiService_PingStreamServer) error { 485 | return status.Errorf(codes.Unimplemented, "method PingStream not implemented") 486 | } 487 | 488 | func (UnimplementedMultiServiceServer) PingStreamError(MultiService_PingStreamErrorServer) error { 489 | return status.Errorf(codes.Unimplemented, "method PingStreamError not implemented") 490 | } 491 | func (UnimplementedMultiServiceServer) mustEmbedUnimplementedMultiServiceServer() {} 492 | 493 | // UnsafeMultiServiceServer may be embedded to opt out of forward compatibility for this service. 494 | // Use of this interface is not recommended, as added methods to MultiServiceServer will 495 | // result in compilation errors. 496 | type UnsafeMultiServiceServer interface { 497 | mustEmbedUnimplementedMultiServiceServer() 498 | } 499 | 500 | func RegisterMultiServiceServer(s grpc.ServiceRegistrar, srv MultiServiceServer) { 501 | s.RegisterService(&MultiService_ServiceDesc, srv) 502 | } 503 | 504 | func _MultiService_PingEmpty_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 505 | in := new(Empty) 506 | if err := dec(in); err != nil { 507 | return nil, err 508 | } 509 | if interceptor == nil { 510 | return srv.(MultiServiceServer).PingEmpty(ctx, in) 511 | } 512 | info := &grpc.UnaryServerInfo{ 513 | Server: srv, 514 | FullMethod: "/talos.testproto.MultiService/PingEmpty", 515 | } 516 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 517 | return srv.(MultiServiceServer).PingEmpty(ctx, req.(*Empty)) 518 | } 519 | return interceptor(ctx, in, info, handler) 520 | } 521 | 522 | func _MultiService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 523 | in := new(PingRequest) 524 | if err := dec(in); err != nil { 525 | return nil, err 526 | } 527 | if interceptor == nil { 528 | return srv.(MultiServiceServer).Ping(ctx, in) 529 | } 530 | info := &grpc.UnaryServerInfo{ 531 | Server: srv, 532 | FullMethod: "/talos.testproto.MultiService/Ping", 533 | } 534 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 535 | return srv.(MultiServiceServer).Ping(ctx, req.(*PingRequest)) 536 | } 537 | return interceptor(ctx, in, info, handler) 538 | } 539 | 540 | func _MultiService_PingError_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 541 | in := new(PingRequest) 542 | if err := dec(in); err != nil { 543 | return nil, err 544 | } 545 | if interceptor == nil { 546 | return srv.(MultiServiceServer).PingError(ctx, in) 547 | } 548 | info := &grpc.UnaryServerInfo{ 549 | Server: srv, 550 | FullMethod: "/talos.testproto.MultiService/PingError", 551 | } 552 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 553 | return srv.(MultiServiceServer).PingError(ctx, req.(*PingRequest)) 554 | } 555 | return interceptor(ctx, in, info, handler) 556 | } 557 | 558 | func _MultiService_PingList_Handler(srv interface{}, stream grpc.ServerStream) error { 559 | m := new(PingRequest) 560 | if err := stream.RecvMsg(m); err != nil { 561 | return err 562 | } 563 | return srv.(MultiServiceServer).PingList(m, &multiServicePingListServer{stream}) 564 | } 565 | 566 | type MultiService_PingListServer interface { 567 | Send(*MultiPingResponse) error 568 | grpc.ServerStream 569 | } 570 | 571 | type multiServicePingListServer struct { 572 | grpc.ServerStream 573 | } 574 | 575 | func (x *multiServicePingListServer) Send(m *MultiPingResponse) error { 576 | return x.ServerStream.SendMsg(m) 577 | } 578 | 579 | func _MultiService_PingStream_Handler(srv interface{}, stream grpc.ServerStream) error { 580 | return srv.(MultiServiceServer).PingStream(&multiServicePingStreamServer{stream}) 581 | } 582 | 583 | type MultiService_PingStreamServer interface { 584 | Send(*MultiPingResponse) error 585 | Recv() (*PingRequest, error) 586 | grpc.ServerStream 587 | } 588 | 589 | type multiServicePingStreamServer struct { 590 | grpc.ServerStream 591 | } 592 | 593 | func (x *multiServicePingStreamServer) Send(m *MultiPingResponse) error { 594 | return x.ServerStream.SendMsg(m) 595 | } 596 | 597 | func (x *multiServicePingStreamServer) Recv() (*PingRequest, error) { 598 | m := new(PingRequest) 599 | if err := x.ServerStream.RecvMsg(m); err != nil { 600 | return nil, err 601 | } 602 | return m, nil 603 | } 604 | 605 | func _MultiService_PingStreamError_Handler(srv interface{}, stream grpc.ServerStream) error { 606 | return srv.(MultiServiceServer).PingStreamError(&multiServicePingStreamErrorServer{stream}) 607 | } 608 | 609 | type MultiService_PingStreamErrorServer interface { 610 | Send(*MultiPingResponse) error 611 | Recv() (*PingRequest, error) 612 | grpc.ServerStream 613 | } 614 | 615 | type multiServicePingStreamErrorServer struct { 616 | grpc.ServerStream 617 | } 618 | 619 | func (x *multiServicePingStreamErrorServer) Send(m *MultiPingResponse) error { 620 | return x.ServerStream.SendMsg(m) 621 | } 622 | 623 | func (x *multiServicePingStreamErrorServer) Recv() (*PingRequest, error) { 624 | m := new(PingRequest) 625 | if err := x.ServerStream.RecvMsg(m); err != nil { 626 | return nil, err 627 | } 628 | return m, nil 629 | } 630 | 631 | // MultiService_ServiceDesc is the grpc.ServiceDesc for MultiService service. 632 | // It's only intended for direct use with grpc.RegisterService, 633 | // and not to be introspected or modified (even as a copy) 634 | var MultiService_ServiceDesc = grpc.ServiceDesc{ 635 | ServiceName: "talos.testproto.MultiService", 636 | HandlerType: (*MultiServiceServer)(nil), 637 | Methods: []grpc.MethodDesc{ 638 | { 639 | MethodName: "PingEmpty", 640 | Handler: _MultiService_PingEmpty_Handler, 641 | }, 642 | { 643 | MethodName: "Ping", 644 | Handler: _MultiService_Ping_Handler, 645 | }, 646 | { 647 | MethodName: "PingError", 648 | Handler: _MultiService_PingError_Handler, 649 | }, 650 | }, 651 | Streams: []grpc.StreamDesc{ 652 | { 653 | StreamName: "PingList", 654 | Handler: _MultiService_PingList_Handler, 655 | ServerStreams: true, 656 | }, 657 | { 658 | StreamName: "PingStream", 659 | Handler: _MultiService_PingStream_Handler, 660 | ServerStreams: true, 661 | ClientStreams: true, 662 | }, 663 | { 664 | StreamName: "PingStreamError", 665 | Handler: _MultiService_PingStreamError_Handler, 666 | ServerStreams: true, 667 | ClientStreams: true, 668 | }, 669 | }, 670 | Metadata: "test.proto", 671 | } 672 | --------------------------------------------------------------------------------