diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml new file mode 100644 index 0000000..792d98f --- /dev/null +++ b/.github/workflows/golangci-lint.yaml @@ -0,0 +1,43 @@ +name: "Lint" + +on: + push: + branches: + - main + paths: + - '**.go' + - '.golangci.yaml' + pull_request: + paths: + - '**.go' + - '.golangci.yaml' + +permissions: + contents: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: 'stable' + + - name: golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + version: v1.56 + + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + # Heavy usage of the action can result in workflow run failures caused by rate limiting. + # GitHub provides a more generous allowance for Authenticated API requests. + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: fmt + run: task fmt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b73953 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Taskfile / local tools +.task +build + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..d02e2bc --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,184 @@ +# We may as well allow multiple golangci-lint invocations at once. +run: + allow-parallel-runners: true + +# golangci-lint by default ignores some staticcheck and vet raised issues that +# are actually important to catch. The following ensures that we do not ignore +# those tools ever. +issues: + exclude-rules: + - path: (.+)_test.go + linters: + - bodyclose + - stylecheck + - goconst + - gosec + exclude-use-default: false + max-same-issues: 0 # 0 is unlimited +linters: + disable-all: true + enable: + # Enabled by default linters: we want all except errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + # Not enabled by default: we want a good chunk + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - cyclop + - durationcheck + - errname + - errorlint + - exhaustive + - exhaustruct + - exportloopref + - gci + - gocheckcompilerdirectives + - gocognit + - goconst + - gocritic + - gocyclo + - gofmt + - gofumpt + - goimports + - goprintffuncname + - gosec + - misspell + - nakedret + - nilerr + - noctx + - nolintlint + - reassign + - revive + - rowserrcheck + - sqlclosecheck + - stylecheck + - tenv + - typecheck + - unconvert + - unparam + - usestdlibvars + - wastedassign + - whitespace +linters-settings: + # A default case ensures we have checked everything. We should not require + # every enum to be checked if we want to default. + exhaustive: + default-signifies-exhaustive: true + # If we want to opt out of a lint, we require an explanation. + nolintlint: + allow-unused: false + require-explanation: true + require-specific: true + # We do not want every usage of fmt.Errorf to use %w. + errorlint: + errorf: false + # If gofumpt is run outside of a module, it assumes Go 1.0 rather than the + # latest Go. We always want the latest formatting. + # + # https://github.com/mvdan/gofumpt/issues/137 + gofumpt: + lang-version: "1.22" + gosec: + excludes: + - G104 # unhandled errors, we exclude for the same reason we do not use errcheck + # Complexity analysis: the recommendations are to be between 10-20, with a + # default of 30 for gocyclo and gocognit, and a default of 10 for cyclop. We + # will choose the middle of the range for cyclo analysis, which should be + # good enough for a lot of cases. We can bump to 20 later if necessary. The + # cognitive analysis is a bit overly sensitive for large switch statements + # (say a function just switches to return a bunch of different strings), so + # we will keep its larger default of 30. + # + # cyclop provides no extra benefit to gocyclo because we are not using + # package average, but that's a weird metric nothing else adds. + cyclop: + max-complexity: 16 + gocyclo: + min-complexity: 30 + gocognit: + min-complexity: 30 + gci: + sections: + - standard # stdlib + - default # everything not std, not within project + - prefix(github.com/redpanda-data/common-go) + # Gocritic is a meta linter that has very good lints, and most of the + # experimental ones are very good too. There are only a few we want to opt + # out of specifically. + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - evalOrder + - importShadow + # disabled due to 1.18 failures + - hugeParam + - rangeValCopy + - typeDefFirst + - paramTypeCombine + - unnamedResult + #settings: + # hugeParam: + # sizeThreshold: 256 + # rangeValCopy: + # sizeThreshold: 256 + # Revive is yet another metalinter with a bunch of useful lints. The below + # opts in to all of the ones we would like to use. + revive: + ignore-generated-header: true + enable-all-rules: true + severity: warning + confidence: 0.7 + rules: + # removed because replacing the version of a proto is easier if we use it + # as alias + - name: redundant-import-alias + disabled: true + - name: add-constant + disabled: true + - name: argument-limit + disabled: true + - name: banned-characters + disabled: true + - name: cognitive-complexity + disabled: true + - name: confusing-naming + disabled: true + - name: cyclomatic + disabled: true + - name: file-header + disabled: true + - name: flag-parameter + disabled: true + - name: function-result-limit + disabled: true + - name: function-length + disabled: true + - name: import-shadowing + disabled: true + - name: line-length-limit + disabled: true + - name: max-public-structs + disabled: true + - name: modifies-parameter + disabled: true + - name: nested-structs + disabled: true + - name: package-comments # https://github.com/mgechev/revive/issues/740; stylecheck's ST1000 is better + disabled: true + - name: redefines-builtin-id + disabled: true + - name: unhandled-error + disabled: true + - name: var-naming + disabled: true diff --git a/README.md b/README.md index 411fef4..0d16ad6 100644 --- a/README.md +++ b/README.md @@ -1 +1,18 @@ -# common-go +# Redpanda Common Go Repository + +Welcome to the Redpanda Common Go Repository! This repository serves as a hub +for sharing code across various Redpanda code bases. While the community +is welcome to utilize the code herein, please note that we do not offer +any guarantees, support, or consider feature requests. + +This repository is used to share code between different Redpanda code bases. +It may be used by the community, but we do not provide any guarantees, support +and won't consider any feature requests. + +## API Module + +The API Module contains a collection of helper functions designed to streamline +various tasks, including pagination, error construction, and boilerplate code +for the gRPC/connect API. These utilities are aimed at simplifying development +workflows within the Redpanda ecosystem. For detailed documentation, please +refer to the [./api] directory. diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..3bec79c --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,59 @@ +version: "3" + +vars: + BUILD_ROOT: "{{ .ROOT_DIR }}/build" + GO_VERSION: 1.22.0 + GO_BUILD_ROOT: '{{.BUILD_ROOT}}/go/{{.GO_VERSION}}' + MODULES: + sh: find . -maxdepth 2 -name go.mod -execdir pwd \; + PATH_PREFIX: PATH={{.BUILD_ROOT}}/bin:{{.GO_BUILD_ROOT}}/bin:{{.BUILD_ROOT}}/bin/go:$PATH GOBIN={{ .BUILD_ROOT }}/bin/go GOROOT= + +includes: + install: taskfiles/install.yaml + +tasks: + lint: + desc: Lint all Go code + deps: ['install:golangci-lint'] + cmds: + - for: { var: MODULES } + task: lint:dir + vars: + DIRECTORY: '{{.ITEM}}' + + lint:dir: + label: lint:dir {{ .DIRECTORY }} + desc: Lint Go code on the provided directory + deps: ['install:golangci-lint'] + vars: + DIRECTORY: '{{ .DIRECTORY }}' + sources: + - '{{ .DIRECTORY }}/**/*.go' + cmds: + - '{{ .BUILD_ROOT }}/bin/go/goimports -l -w -local "github.com/redpanda-data/common-go" {{.DIRECTORY}}' + + fmt: + desc: Run all formatters + cmds: + - for: { var: MODULES } + task: fmt:dir + vars: + DIRECTORY: '{{.ITEM}}' + + fmt:dir: + label: fmt:dir {{ .DIRECTORY }} + desc: Run all of the Go formatters on the provided directory, excluding any generated folders + deps: + - 'install:go' + - 'install:gofumpt' + - 'install:goimports' + - 'install:gci' + vars: + DIRECTORY: '{{ .DIRECTORY }}' + sources: + - '{{ .DIRECTORY }}/**/*.go' + cmds: + - '{{ .BUILD_ROOT }}/bin/go/goimports -l -w -local "github.com/redpanda-data/common-go" {{.DIRECTORY}}' + - '{{ .BUILD_ROOT }}/bin/go/gofumpt -l -w {{.DIRECTORY}}' + - '{{ .BUILD_ROOT }}/bin/go/gci write -s default -s standard -s "prefix(github.com/redpanda-data/common-go)" {{.DIRECTORY}}' + - if [[ $CI == "true" ]]; then git --no-pager diff --exit-code; fi diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..c158407 --- /dev/null +++ b/api/README.md @@ -0,0 +1,30 @@ +# API + +The API package contains code that can be shared among multiple projects that are involved +in serving or consuming any public or internal Redpanda API. Redpanda uses connectrpc +and therefore this project is heavily built around that. + +## Errors + +The errors package contains several helpers for constructing new connectrpc errors, +as well as custom HTTP error handler for the gRPC gateway. + +**Creating new Connect errors:** + +```go +err := apierrors.NewConnectError( + connect.CodeUnimplemented, + errors.New("the redpanda admin api must be configured to use this endpoint"), + apierrors.NewErrorInfo( + apierrors.DomainDataplane, + v1alpha1.Reason_REASON_FEATURE_NOT_CONFIGURED.String(), + ), + apierrors.NewHelp(apierrors.NewHelpLink("Redpanda Console Configuration Reference", "https://docs.redpanda.com/current/reference/console/config/")), +) +``` + +**Mount Redpanda gRPC Gateway Error Handler** + +```go + +``` diff --git a/api/errrors/connectrpc.go b/api/errrors/connectrpc.go new file mode 100644 index 0000000..f32fd84 --- /dev/null +++ b/api/errrors/connectrpc.go @@ -0,0 +1,80 @@ +package errrors + +import ( + "connectrpc.com/connect" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/protobuf/proto" +) + +// NewConnectError is a helper function to construct a new connect error. This +// function should always be used over instantiating connect errors directly, as +// we can ensure that certain error details will always be provided. +func NewConnectError( + code connect.Code, + innerErr error, + errInfo *errdetails.ErrorInfo, + errDetails ...proto.Message, +) *connect.Error { + connectErr := connect.NewError(code, innerErr) + + if detail, detailErr := connect.NewErrorDetail(errInfo); detailErr == nil { + connectErr.AddDetail(detail) + } + + for _, msg := range errDetails { + // We may sometimes pass in a nil object so that this function is easier + // to use. In this case we just want to skip it. + if msg == nil { + continue + } + detail, detailErr := connect.NewErrorDetail(msg) + if detailErr != nil { + continue + } + connectErr.AddDetail(detail) + } + + return connectErr +} + +// KeyVal is a key/value pair that is used to provide additional metadata labels. +type KeyVal struct { + Key string + Value string +} + +// NewErrorInfo is a helper function to create a new ErrorInfo detail. +func NewErrorInfo(domain Domain, reason string, metadata ...KeyVal) *errdetails.ErrorInfo { + var md map[string]string + if len(metadata) > 0 { + md = make(map[string]string, len(metadata)) + + for _, keyVal := range metadata { + md[keyVal.Key] = keyVal.Value + } + } + + return &errdetails.ErrorInfo{ + Reason: reason, + Domain: string(domain), + Metadata: md, + } +} + +// NewBadRequest is a constructor for creating bad request +func NewBadRequest(fieldValidations ...*errdetails.BadRequest_FieldViolation) *errdetails.BadRequest { + return &errdetails.BadRequest{FieldViolations: fieldValidations} +} + +// NewHelp constructs a new errdetails.Help with one or more provided errdetails.Help_Link. +func NewHelp(links ...*errdetails.Help_Link) *errdetails.Help { + return &errdetails.Help{Links: links} +} + +// NewHelpLink constructs a new link that can be put into the errdetails.Help. +func NewHelpLink(description, url string) *errdetails.Help_Link { + return &errdetails.Help_Link{ + Description: description, + Url: url, + } +} diff --git a/api/errrors/domain.go b/api/errrors/domain.go new file mode 100644 index 0000000..a9c7e45 --- /dev/null +++ b/api/errrors/domain.go @@ -0,0 +1,10 @@ +package errrors + +type Domain string + +const ( + // DomainDataplane defines the string for the proto error domain "dataplane". + DomainDataplane Domain = "redpanda.com/dataplane" + // DomainControlplane defines the string for the proto error domain "controlplane". + DomainControlplane Domain = "redpanda.com/controlplane" +) diff --git a/api/errrors/googlerpc.go b/api/errrors/googlerpc.go new file mode 100644 index 0000000..45042b0 --- /dev/null +++ b/api/errrors/googlerpc.go @@ -0,0 +1,37 @@ +package errrors + +import ( + commonv1alpha1 "buf.build/gen/go/redpandadata/common/protocolbuffers/go/redpanda/api/common/v1alpha1" + "connectrpc.com/connect" + "google.golang.org/genproto/googleapis/rpc/code" + spb "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/protobuf/types/known/anypb" +) + +// StatusToNice converts a google.rpc.Status to commonv1alpha1.ErrorStatus, +// which is "nicer" variant with Code as Enum. +func StatusToNice(s *spb.Status) *commonv1alpha1.ErrorStatus { + return &commonv1alpha1.ErrorStatus{ + Code: code.Code(s.Code), + Message: s.Message, + Details: s.Details, + } +} + +// ConnectErrorToGoogleStatus converts a connect.Error into the gRPC compliant +// spb.Status type that can be used to present errors. +func ConnectErrorToGoogleStatus(connectErr *connect.Error) *spb.Status { + st := &spb.Status{ + Code: int32(connectErr.Code()), + Message: connectErr.Message(), + } + for _, detail := range connectErr.Details() { + anyDetail := &anypb.Any{ + TypeUrl: detail.Type(), + Value: detail.Bytes(), + } + st.Details = append(st.Details, anyDetail) + } + + return st +} diff --git a/api/errrors/http.go b/api/errrors/http.go new file mode 100644 index 0000000..b5d396e --- /dev/null +++ b/api/errrors/http.go @@ -0,0 +1,167 @@ +package errrors + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/textproto" + "strings" + + "connectrpc.com/connect" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + spb "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" +) + +// ProtoJSONMarshaler can be used to marshal a proto message to JSON using our +// preferred marshal and unmarshal settings. +var ProtoJSONMarshaler = &runtime.JSONPb{ + MarshalOptions: protojson.MarshalOptions{ + // UseProtoNames ensures that we serialize to snake_cased property names. + UseProtoNames: true, + // Do not use EmitUnpopulated, so we don't emit nulls (they are ugly, and + // provide no benefit. they transport no information, even in "normal" json). + EmitUnpopulated: false, + // Instead, use EmitDefaultValues, which is new and like EmitUnpopulated, but + // skips nulls (which we consider ugly, and provides no benefit over skipping + // the field). + EmitDefaultValues: true, + }, + UnmarshalOptions: protojson.UnmarshalOptions{ + DiscardUnknown: true, + }, +} + +// HandleHTTPError serializes the given error and writes it using the protoJSONMarshaler. +// This function can handle errors of type connect.Error as well, so that err details are +// printed properly. +// +// This function can be used to write connect.Error on an HTTP endpoint. +func HandleHTTPError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) { + var st *spb.Status + + var connectErr *connect.Error + if errors.As(err, &connectErr) { + st = ConnectErrorToGoogleStatus(connectErr) + } else { + st = &spb.Status{ + Code: int32(connect.CodeOf(err)), + Message: err.Error(), + } + } + + NiceHTTPErrorHandler(ctx, nil, ProtoJSONMarshaler, w, r, status.ErrorProto(st)) +} + +// NiceHTTPErrorHandler is a clone of grpc-gateway's +// runtime.DefaultHTTPErrorHandler, with one difference: it uses a modified +// variant of google.rpc.Status, where code is ENUM instead of int32. +func NiceHTTPErrorHandler( + ctx context.Context, + _ *runtime.ServeMux, + marshaler runtime.Marshaler, + w http.ResponseWriter, + r *http.Request, + err error, +) { + const fallback = `{"code":"INTERNAL", "message":"failed to marshal error message"}` + + var customStatus *runtime.HTTPStatusError + if errors.As(err, &customStatus) { + err = customStatus.Err + } + s := status.Convert(err) + pb := StatusToNice(s.Proto()) + + w.Header().Del("Trailer") + w.Header().Del("Transfer-Encoding") + + contentType := marshaler.ContentType(pb) + w.Header().Set("Content-Type", contentType) + + if s.Code() == codes.Unauthenticated { + w.Header().Set("WWW-Authenticate", s.Message()) + } + + buf, merr := marshaler.Marshal(pb) + if merr != nil { + grpclog.Infof("Failed to marshal error message %q: %v", s, merr) + w.WriteHeader(http.StatusInternalServerError) + if _, err := io.WriteString(w, fallback); err != nil { + grpclog.Infof("Failed to write response: %v", err) + } + return + } + + md, ok := runtime.ServerMetadataFromContext(ctx) + if !ok { + grpclog.Infof("Failed to extract ServerMetadata from context") + } + + handleForwardResponseServerMetadata(w, md) + + // RFC 7230 https://tools.ietf.org/html/rfc7230#section-4.1.2 Unless the request + // includes a TE header field indicating "trailers" is acceptable, as described + // in Section 4.3, a server SHOULD NOT generate trailer fields that it believes + // are necessary for the user agent to receive. + doForwardTrailers := requestAcceptsTrailers(r) + + if doForwardTrailers { + handleForwardResponseTrailerHeader(w, md) + w.Header().Set("Transfer-Encoding", "chunked") + } + + st := runtime.HTTPStatusFromCode(s.Code()) + if customStatus != nil { + st = customStatus.HTTPStatus + } + + w.WriteHeader(st) + if _, err := w.Write(buf); err != nil { + grpclog.Infof("Failed to write response: %v", err) + } + + if doForwardTrailers { + handleForwardResponseTrailer(w, md) + } +} + +var defaultOutgoingHeaderMatcher = func(key string) (string, bool) { + return fmt.Sprintf("%s%s", runtime.MetadataHeaderPrefix, key), true +} + +func handleForwardResponseServerMetadata(w http.ResponseWriter, md runtime.ServerMetadata) { + for k, vs := range md.HeaderMD { + if h, ok := defaultOutgoingHeaderMatcher(k); ok { + for _, v := range vs { + w.Header().Add(h, v) + } + } + } +} + +func requestAcceptsTrailers(req *http.Request) bool { + te := req.Header.Get("TE") + return strings.Contains(strings.ToLower(te), "trailers") +} + +func handleForwardResponseTrailerHeader(w http.ResponseWriter, md runtime.ServerMetadata) { + for k := range md.TrailerMD { + tKey := textproto.CanonicalMIMEHeaderKey(fmt.Sprintf("%s%s", runtime.MetadataTrailerPrefix, k)) + w.Header().Add("Trailer", tKey) + } +} + +func handleForwardResponseTrailer(w http.ResponseWriter, md runtime.ServerMetadata) { + for k, vs := range md.TrailerMD { + tKey := fmt.Sprintf("%s%s", runtime.MetadataTrailerPrefix, k) + for _, v := range vs { + w.Header().Add(tKey, v) + } + } +} diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..de2f8a3 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,21 @@ +module github.com/redpanda-data/common-go/api + +go 1.22.0 + +require ( + buf.build/gen/go/redpandadata/common/protocolbuffers/go v1.32.0-20240130111152-723ec1649aaa.1 + connectrpc.com/connect v1.15.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 + google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe + google.golang.org/grpc v1.62.0 + google.golang.org/protobuf v1.32.0 +) + +require ( + github.com/golang/protobuf v1.5.3 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect +) diff --git a/api/go.sum b/api/go.sum new file mode 100644 index 0000000..b6929ee --- /dev/null +++ b/api/go.sum @@ -0,0 +1,32 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.32.0-20230914171853-63dfe56cc2c4.1/go.mod h1:tiTMKD8j6Pd/D2WzREoweufjzaJKHZg35f/VGcZ2v3I= +buf.build/gen/go/redpandadata/common/protocolbuffers/go v1.32.0-20240130111152-723ec1649aaa.1 h1:WQVOTW6QIYPBHuwfwe+CMdMJ0YvjO/CF29ZEXr2zvvE= +buf.build/gen/go/redpandadata/common/protocolbuffers/go v1.32.0-20240130111152-723ec1649aaa.1/go.mod h1:T+yeYA9NuQZilN/puq5TDsQwrl8MlajzpKqqgVrT8pk= +connectrpc.com/connect v1.15.0 h1:lFdeCbZrVVDydAqwr4xGV2y+ULn+0Z73s5JBj2LikWo= +connectrpc.com/connect v1.15.0/go.mod h1:bQmjpDY8xItMnttnurVgOkHUBMRT9cpsNi2O4AjKhmA= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= +google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/api/grpcgateway/forward_response.go b/api/grpcgateway/forward_response.go new file mode 100644 index 0000000..63b4097 --- /dev/null +++ b/api/grpcgateway/forward_response.go @@ -0,0 +1,35 @@ +package grpcgateway + +import ( + "context" + "net/http" + "strconv" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/protobuf/proto" +) + +// GetHTTPResponseModifier returns a ForwardResponseOption that sets a specific +// http status code based on the header value in 'x-http-code'. +func GetHTTPResponseModifier() func(ctx context.Context, w http.ResponseWriter, p proto.Message) error { + return func(ctx context.Context, w http.ResponseWriter, p proto.Message) error { + md, ok := runtime.ServerMetadataFromContext(ctx) + if !ok { + return nil + } + + // set http status cod + if vals := md.HeaderMD.Get("x-http-code"); len(vals) > 0 { + code, err := strconv.Atoi(vals[0]) + if err != nil { + return err + } + // delete the headers to not expose any grpc-metadata in http response + delete(md.HeaderMD, "x-http-code") + delete(w.Header(), "Grpc-Metadata-X-Http-Code") + w.WriteHeader(code) + } + + return nil + } +} diff --git a/taskfiles/install.yaml b/taskfiles/install.yaml new file mode 100644 index 0000000..443d303 --- /dev/null +++ b/taskfiles/install.yaml @@ -0,0 +1,65 @@ +version: "3" + +tasks: + go: + desc: install golang compiler + run: once + vars: + GOLANG_URL_DEFAULT: https://golang.org/dl/go{{.GO_VERSION}}.{{OS}}-{{ARCH}}.tar.gz + GOLANG_URL: '{{default .GOLANG_URL_DEFAULT .GOLANG_URL}}' + cmds: + - rm -rf {{.GO_BUILD_ROOT}} + - mkdir -p '{{.GO_BUILD_ROOT}}' + - curl -sSLf --retry 3 --retry-connrefused --retry-delay 2 '{{.GOLANG_URL}}' | tar -xz -C '{{.GO_BUILD_ROOT}}' --strip 1 + status: + - test -f '{{.GO_BUILD_ROOT}}/bin/go' + - '[[ $({{.GO_BUILD_ROOT}}/bin/go version) == *"go version go{{ .GO_VERSION }}"* ]]' + + golangci-lint: + desc: Installs golangci-lint + run: once + vars: + GO_LINT_VERSION: 1.56.2 + cmds: + - mkdir -p {{ .BUILD_ROOT}}/bin + - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "{{ .BUILD_ROOT }}"/bin v{{ .GO_LINT_VERSION }} + status: + - '[ -f ''{{ .BUILD_ROOT }}/bin'' ] || command -v {{ .BUILD_ROOT }}/bin/golangci-lint >/dev/null 2>&1' + - '[[ $({{ .BUILD_ROOT }}/bin/golangci-lint --version) == *"version {{ .GO_LINT_VERSION }} built"* ]]' + + goimports: + desc: Installs https://pkg.go.dev/golang.org/x/tools/cmd/goimports + run: once + deps: + - go + cmds: + - '{{.PATH_PREFIX}} go install golang.org/x/tools/cmd/goimports@latest' + status: + - '[ -f ''{{ .BUILD_ROOT }}/bin/go'' ] || command -v {{ .BUILD_ROOT }}/bin/go/goimports >/dev/null 2>&1' + + gci: + desc: Installs https://github.com/daixiang0/gci + run: once + deps: + - go + vars: + GCI_VERSION: 0.12.3 + cmds: + - '{{.PATH_PREFIX}} go install github.com/daixiang0/gci@v{{.GCI_VERSION}}' + status: + - '[ -f ''{{ .BUILD_ROOT }}/bin/go'' ] || command -v {{ .BUILD_ROOT }}/bin/go/gci >/dev/null 2>&1' + - '[[ $(cat {{ .BUILD_ROOT }}/.gci_version) == {{.GCI_VERSION}} ]]' + + gofumpt: + desc: Install gofumpt formatter + run: once + deps: + - go + vars: + GOFUMPT_VERSION: 0.6.0 + cmds: + - | + {{ .PATH_PREFIX }} go install mvdan.cc/gofumpt@v{{ .GOFUMPT_VERSION }} + status: + - '[ -f ''{{ .BUILD_ROOT }}/bin/go'' ] || command -v {{ .BUILD_ROOT }}/bin/go/gofumpt >/dev/null 2>&1' + - '[[ $({{ .BUILD_ROOT }}/bin/go/gofumpt --version) == v{{.GOFUMPT_VERSION}} ]]' \ No newline at end of file