commit 686cdb80b535fd5c173a86c427bd10807b31f68c Author: Ryan McGuire Date: Tue Jul 8 17:39:01 2025 +0000 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b42f428 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +config.y*ml +go.work* +docker-compose-sample* diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..db708c8 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,133 @@ +name: Build and Publish +on: + push: + tags: ["v*"] + branches: ["main"] + +env: + PACKAGE_NAME: go-server-with-otel + BINARY_PATH: bin + BINARY_NAME: go-server-with-otel + GO_MOD_PATH: gitea.libretechconsulting.com/rmcguire/go-server-with-otel + GO_GIT_HOST: gitea.libretechconsulting.com + VER_PKG: gitea.libretechconsulting.com/rmcguire/go-app/pkg/config.Version + VERSION: ${{ github.ref_name }} + PLATFORMS: linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 + DOCKER_REGISTRY: gitea.libretechconsulting.com + DOCKER_USER: rmcguire + DOCKER_REPO: rmcguire/go-server-with-otel + DOCKER_IMG: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_REPO }} + CHART_DIR: helm/ + +jobs: + release: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') # Only run on tag push + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Go Environment + uses: actions/setup-go@v4 + with: + go-version: '1.23' + + - name: Build Binary + run: make build + + - name: Upload Binaries to Generic Registry + env: + API_TOKEN: ${{ secrets.API_TOKEN }} + run: | + for platform in $PLATFORMS; do + OS=$(echo $platform | cut -d/ -f1) + ARCH=$(echo $platform | cut -d/ -f2) + BINARY_FILE="${BINARY_PATH}/${PACKAGE_NAME}-${OS}-${ARCH}" + echo "Uploading $BINARY_FILE" + if [ -f "$BINARY_FILE" ]; then + curl -X PUT \ + -H "Authorization: token ${API_TOKEN}" \ + --upload-file "$BINARY_FILE" \ + "${GITHUB_SERVER_URL}/api/packages/${GITHUB_REPOSITORY_OWNER}/generic/${PACKAGE_NAME}/${{ github.ref_name }}/${PACKAGE_NAME}-${OS}-${ARCH}" + else + echo "Error: Binary $BINARY_FILE not found." + exit 1 + fi + done + + - name: Run Go List + continue-on-error: true + env: + TAG_NAME: ${{ github.ref_name }} # Use the pushed tag name + run: | + if [[ "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + GOPROXY=proxy.golang.org go list -m ${GO_GIT_HOST}/${GITHUB_REPOSITORY}@$TAG_NAME + else + echo "Error: Invalid tag format '$TAG_NAME'. Expected 'vX.X.X'." + exit 1 + fi + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Custom Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ env.DOCKER_USER }} + password: ${{ secrets.API_TOKEN }} + + - name: Build and Push Docker Image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ${{ env.DOCKER_IMG }}:${{ github.ref_name }} + ${{ env.DOCKER_IMG }}:latest + build-args: | + VER_PKG=${{ env.VER_PKG }} + VERSION=${{ github.ref_name }} + + # Detect if the helm chart was updated + check-chart: + runs-on: ubuntu-latest + outputs: + chart-updated: ${{ steps.filter.outputs.chart }} + steps: + - uses: actions/checkout@v4 + - name: Check Chart Changed + uses: dorny/paths-filter@v3 + id: filter + with: + base: ${{ github.ref }} + filters: | + chart: + - helm/Chart.yaml + + helm-release: + runs-on: ubuntu-latest + needs: check-chart + if: ${{ needs.check-chart.outputs.chart-updated == 'true' }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Install Helm + env: + BINARY_NAME: helm + run: | + curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + + - name: Package Chart + run: | + helm package --app-version ${VERSION} ${CHART_DIR} + + - name: Publish Chart + env: + API_TOKEN: ${{ secrets.API_TOKEN }} + run: | + curl -X POST \ + -H "Authorization: token ${API_TOKEN}" \ + --upload-file ./${PACKAGE_NAME}-*.tgz \ + https://gitea.libretechconsulting.com/api/packages/${GITHUB_REPOSITORY_OWNER}/helm/api/charts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3e76b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# 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 + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# Environment +.env + +bin/* diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b79f986 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://raw.githubusercontent.com/mfussenegger/dapconfig-schema/master/dapconfig-schema.json", + "version": "0.2.0", + "configurations": [ + { + "name": "Run Server", + "type": "delve", + "request": "launch", + "program": "${workspaceFolder}", + "env": { + "APP_NAME": "Go HTTP with OTEL" + }, + "args": [] + } + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c2b59e1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM golang:1-alpine AS build +WORKDIR /app + +ENV GO111MODULE=auto CGO_ENABLED=0 GOOS=linux + +ARG GOPROXY +ARG GONOSUMDB=gitea.libretechconsulting.com +ARG VER_PKG=gitea.libretechconsulting.com/rmcguire/go-app/pkg/config.Version +ARG VERSION=(devel) +ARG APP_NAME=demo-app + +COPY ./go.mod ./go.sum ./ +RUN go mod download + +COPY ./ /app +RUN go build -C . -v -ldflags "-extldflags '-static' -X ${VER_PKG}=${VERSION}" -o ${APP_NAME} . + +FROM alpine:latest + +ARG APP_NAME=demo-app + +WORKDIR /app +USER 100:101 + +COPY --from=build --chown=100:101 /app/${APP_NAME} /app/ + +ENTRYPOINT [ "/app/${APP_NAME}" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4d42a02 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 rmcguire + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..09c8874 --- /dev/null +++ b/Makefile @@ -0,0 +1,89 @@ +.PHONY: all test build docker install clean proto check_protoc + +CMD_NAME := go-server-with-otel +VERSION ?= development +API_DIR := api/ +SCHEMA_DIR := contrib/ +PROTO_DIRS := $(wildcard proto/demo/app/*) # TODO: Update path (probably not demo) +PLATFORMS := linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 +OUTPUT_DIR := bin +VER_PKG := gitea.libretechconsulting.com/rmcguire/go-app/pkg/config.Version +GIT_REPO := gitea.libretechconsulting.com/rmcguire/go-server-with-otel + +all: proto test build docker + +proto: check_protoc $(API_DIR) + protoc --proto_path=proto --proto_path=proto/google \ + --go_out=$(API_DIR) --go_opt=paths=source_relative \ + --go-grpc_out=$(API_DIR) --go-grpc_opt=paths=source_relative \ + --grpc-gateway_out=$(API_DIR) --grpc-gateway_opt=paths=source_relative \ + --openapiv2_out=$(SCHEMA_DIR) \ + --openapiv2_opt allow_merge=true \ + --openapiv2_opt merge_file_name=$(CMD_NAME) \ + $(foreach dir, $(PROTO_DIRS), $(wildcard $(dir)/*.proto)) + +test: + go test -v ./... + +build: test + @echo "Building for platforms: $(PLATFORMS)" + @for platform in $(PLATFORMS); do \ + OS=$$(echo $$platform | cut -d/ -f1); \ + ARCH=$$(echo $$platform | cut -d/ -f2); \ + OUTPUT="$(OUTPUT_DIR)/$(CMD_NAME)-$$OS-$$ARCH"; \ + mkdir -vp $(OUTPUT_DIR); \ + echo "Building for $$platform into $$OUTPUT"; \ + GOOS=$$OS GOARCH=$$ARCH go build -ldflags "-X $(VER_PKG)=$(VERSION)" -o $$OUTPUT .; \ + echo "Built $$OUTPUT"; \ + done + go build -ldflags "-X $(VER_PKG)=$(VERSION)" -o bin/${CMD_NAME} + +schema: + go run . -schema > contrib/schema.json + +docker: + @echo "Building Docker image $(GIT_REPO):$(VERSION)" + docker build \ + --build-arg VER_PKG=$(VER_PKG) \ + --build-arg VERSION=$(VERSION) \ + --build-arg APP_NAME=$(CMD_NAME) \ + -t $(GIT_REPO):$(VERSION) . + docker push $(GIT_REPO):$(VERSION) + +install: + go install -v -ldflags "-X $(VER_PKG)=$(VERSION)" . + +clean: + rm -rf bin/${CMD_NAME} + +check_protoc: + @if ! command -v protoc-gen-go > /dev/null; then \ + echo "Error: protoc-gen-go not found in PATH"; \ + exit 1; \ + fi + @if ! command -v protoc-gen-go-grpc > /dev/null; then \ + echo "Error: protoc-gen-go-grpc not found in PATH"; \ + exit 1; \ + fi + +rename: + @echo "Current module path: $(GIT_REPO)" + @echo "Usage: make rename NAME=your/new/module/name" + @if [ -z "$(NAME)" ]; then \ + echo "No name provided. Aborting."; \ + exit 1; \ + fi + @echo "New name: $(NAME)" + @echo "Are you sure you want to proceed? (y/N): " && read CONFIRM && if [ "$$CONFIRM" != "y" ] && [ "$$CONFIRM" != "Y" ]; then \ + echo "Aborted."; \ + exit 1; \ + fi + @find . -type f -a \ + \( -name '*.go' -o -name 'go.mod' \ + -o -name 'go.sum' -o -name '*.proto' \ + -o -name 'Makefile' \ + -o -name '*.yml' -o -name '*.yaml' \ + \) \ + -not -path './.git' -not -path './.git/*' \ + -exec sed -i "s|$(GIT_REPO)|$(NAME)|g" {} + + @echo "Project renamed to $(NAME)" diff --git a/README.md b/README.md new file mode 100644 index 0000000..3273688 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# go-server-with-otel 🚀 + +A powerful and flexible template for building Go HTTP + GRPC servers with full OpenTelemetry (OTEL) support. +Bootstrapped with the go-app framework to provide all the bells and whistles right out of the box. +Ideal for rapidly creating production-ready microservices. + +Check out the [go-app framework](https://gitea.libretechconsulting.com/rmcguire/go-app) for more detail there. + +## 🌟 Features + +- **📈 OpenTelemetry (OTEL) Metrics & Traces** – Comprehensive observability with built-in support for metrics and traces. +- 📝 Logging with Zerolog – High-performance structured logging with zerolog for ultra-fast, leveled logging. +- **💬 GRPC + GRPC-Gateway** – Supports RESTful JSON APIs alongside gRPC with auto-generated Swagger (OpenAPI2) specs. +- 🌐 HTTP and GRPC Middleware – Flexible middleware support for HTTP and GRPC to enhance request handling, authentication, and observability. +- **📦 Multi-Arch Builds** – Robust Makefile that supports building for multiple architectures (amd64, arm64, etc.). +- **🐳 Docker Image Generation** – Easily build Docker images using `make docker`. +- **📜 Config Schema Generation** – Automatically generate JSON schemas for configuration validation. +- **📝 Proto Compilation** – Seamlessly compile .proto files with `make proto`. +- **🔄 Project Renaming** – Easily rename your project using `make rename NAME=your.gitremote.com/pathto/repo`. +- **📦 Helm Chart** – Deploy your application with Kubernetes using the provided Helm chart. +- **🤖 Gitea CI Integration** – Out-of-the-box Gitea CI pipeline configuration with `.gitea/workflows/ci.yaml`. +- **⚙️ Expandable Configuration** – Extend your app-specific configuration with go-app's built-in logging, HTTP, and GRPC config support. + +--- + +## 📚 Getting Started + +1. Install tools: + - Install make, protoc, and go using brew, apt, etc.. + - Install protoc plugins (provided in go.mod tool()): + - `go get -v tool && go install -v tool` + +1. **Rename your package:** + ```sh + make rename NAME=my.gitremote.com/pathto/repo + ``` + +1. **Review the config struct:** + Update and customize your app-specific configuration. This merges with go-app's configuration, providing logging, HTTP, and GRPC config for free. + +1. **Generate a new JSON schema:** + ```sh + make schema + ``` + - Ensure your structs have `yaml` and `json` tags. + - With the `yaml-language-server` LSP plugin, the schema will be auto-detected in your `config.yaml`. + +1. **Compile proto files:** + ```sh + make proto + ``` + - Add paths under `proto/` as necessary. + +1. **Implement your application logic.** + +1. **Update Gitea CI configuration:** + Modify parameters in `.gitea/workflows/ci.yaml` as needed. + +1. Tag your release: `git tag v0.1.0` and push `git push --tags` + +--- + +## 📂 Project Structure +- `proto/` - Protobuf definitions and generated files. +- `api/` - Auto-generated code for proto, grpc-gateway, and OpenAPI2 spec +- `pkg/config/` - Custom config, merged with go-app configuration +- `pkg/demo(http|grpc)/` - HTTP and GRPC server implementations +- `helm/` - Helm chart for deploying your application to Kubernetes. +- `.gitea/workflows/` - CI pipelines for automated builds and tests. + +--- + +## 🔥 Ready to build something awesome? Let's go! 🎉 diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..dc1ada2 --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +# Demo app TODO + +- [ ] Update README for tagging/versioning/pipeline info +- [x] Update README for detail on installing protoc tools and make +- [x] Rename project +- [x] Finish grpc sample implementation +- [x] Add Dockerfile +- [x] Add gitea CI diff --git a/api/demo/app/v1alpha1/app.pb.go b/api/demo/app/v1alpha1/app.pb.go new file mode 100644 index 0000000..7148251 --- /dev/null +++ b/api/demo/app/v1alpha1/app.pb.go @@ -0,0 +1,210 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc v5.29.3 +// source: demo/app/v1alpha1/app.proto + +package demo + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Options for random fact, in this case +// just a language +type GetDemoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Language *string `protobuf:"bytes,1,opt,name=language,proto3,oneof" json:"language,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDemoRequest) Reset() { + *x = GetDemoRequest{} + mi := &file_demo_app_v1alpha1_app_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDemoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDemoRequest) ProtoMessage() {} + +func (x *GetDemoRequest) ProtoReflect() protoreflect.Message { + mi := &file_demo_app_v1alpha1_app_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDemoRequest.ProtoReflect.Descriptor instead. +func (*GetDemoRequest) Descriptor() ([]byte, []int) { + return file_demo_app_v1alpha1_app_proto_rawDescGZIP(), []int{0} +} + +func (x *GetDemoRequest) GetLanguage() string { + if x != nil && x.Language != nil { + return *x.Language + } + return "" +} + +// Returns a randome fact, because this is a demo app +// so what else do we do? +type GetDemoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Fact string `protobuf:"bytes,2,opt,name=fact,proto3" json:"fact,omitempty"` + Source string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"` + Language string `protobuf:"bytes,4,opt,name=language,proto3" json:"language,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDemoResponse) Reset() { + *x = GetDemoResponse{} + mi := &file_demo_app_v1alpha1_app_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDemoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDemoResponse) ProtoMessage() {} + +func (x *GetDemoResponse) ProtoReflect() protoreflect.Message { + mi := &file_demo_app_v1alpha1_app_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDemoResponse.ProtoReflect.Descriptor instead. +func (*GetDemoResponse) Descriptor() ([]byte, []int) { + return file_demo_app_v1alpha1_app_proto_rawDescGZIP(), []int{1} +} + +func (x *GetDemoResponse) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *GetDemoResponse) GetFact() string { + if x != nil { + return x.Fact + } + return "" +} + +func (x *GetDemoResponse) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *GetDemoResponse) GetLanguage() string { + if x != nil { + return x.Language + } + return "" +} + +var File_demo_app_v1alpha1_app_proto protoreflect.FileDescriptor + +const file_demo_app_v1alpha1_app_proto_rawDesc = "" + + "\n" + + "\x1bdemo/app/v1alpha1/app.proto\x12\x11demo.app.v1alpha1\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/protobuf/timestamp.proto\">\n" + + "\x0eGetDemoRequest\x12\x1f\n" + + "\blanguage\x18\x01 \x01(\tH\x00R\blanguage\x88\x01\x01B\v\n" + + "\t_language\"\x93\x01\n" + + "\x0fGetDemoResponse\x128\n" + + "\ttimestamp\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12\x12\n" + + "\x04fact\x18\x02 \x01(\tR\x04fact\x12\x16\n" + + "\x06source\x18\x03 \x01(\tR\x06source\x12\x1a\n" + + "\blanguage\x18\x04 \x01(\tR\blanguage2z\n" + + "\x0eDemoAppService\x12h\n" + + "\aGetDemo\x12!.demo.app.v1alpha1.GetDemoRequest\x1a\".demo.app.v1alpha1.GetDemoResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/v1alpha1/demoBNZLgitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/v1alpha1/demob\x06proto3" + +var ( + file_demo_app_v1alpha1_app_proto_rawDescOnce sync.Once + file_demo_app_v1alpha1_app_proto_rawDescData []byte +) + +func file_demo_app_v1alpha1_app_proto_rawDescGZIP() []byte { + file_demo_app_v1alpha1_app_proto_rawDescOnce.Do(func() { + file_demo_app_v1alpha1_app_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_demo_app_v1alpha1_app_proto_rawDesc), len(file_demo_app_v1alpha1_app_proto_rawDesc))) + }) + return file_demo_app_v1alpha1_app_proto_rawDescData +} + +var file_demo_app_v1alpha1_app_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_demo_app_v1alpha1_app_proto_goTypes = []any{ + (*GetDemoRequest)(nil), // 0: demo.app.v1alpha1.GetDemoRequest + (*GetDemoResponse)(nil), // 1: demo.app.v1alpha1.GetDemoResponse + (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp +} +var file_demo_app_v1alpha1_app_proto_depIdxs = []int32{ + 2, // 0: demo.app.v1alpha1.GetDemoResponse.timestamp:type_name -> google.protobuf.Timestamp + 0, // 1: demo.app.v1alpha1.DemoAppService.GetDemo:input_type -> demo.app.v1alpha1.GetDemoRequest + 1, // 2: demo.app.v1alpha1.DemoAppService.GetDemo:output_type -> demo.app.v1alpha1.GetDemoResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_demo_app_v1alpha1_app_proto_init() } +func file_demo_app_v1alpha1_app_proto_init() { + if File_demo_app_v1alpha1_app_proto != nil { + return + } + file_demo_app_v1alpha1_app_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_demo_app_v1alpha1_app_proto_rawDesc), len(file_demo_app_v1alpha1_app_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_demo_app_v1alpha1_app_proto_goTypes, + DependencyIndexes: file_demo_app_v1alpha1_app_proto_depIdxs, + MessageInfos: file_demo_app_v1alpha1_app_proto_msgTypes, + }.Build() + File_demo_app_v1alpha1_app_proto = out.File + file_demo_app_v1alpha1_app_proto_goTypes = nil + file_demo_app_v1alpha1_app_proto_depIdxs = nil +} diff --git a/api/demo/app/v1alpha1/app.pb.gw.go b/api/demo/app/v1alpha1/app.pb.gw.go new file mode 100644 index 0000000..7e042b8 --- /dev/null +++ b/api/demo/app/v1alpha1/app.pb.gw.go @@ -0,0 +1,163 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: demo/app/v1alpha1/app.proto + +/* +Package demo is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package demo + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +var filter_DemoAppService_GetDemo_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_DemoAppService_GetDemo_0(ctx context.Context, marshaler runtime.Marshaler, client DemoAppServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetDemoRequest + metadata runtime.ServerMetadata + ) + io.Copy(io.Discard, req.Body) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DemoAppService_GetDemo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.GetDemo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_DemoAppService_GetDemo_0(ctx context.Context, marshaler runtime.Marshaler, server DemoAppServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetDemoRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DemoAppService_GetDemo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.GetDemo(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterDemoAppServiceHandlerServer registers the http handlers for service DemoAppService to "mux". +// UnaryRPC :call DemoAppServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterDemoAppServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterDemoAppServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server DemoAppServiceServer) error { + mux.Handle(http.MethodGet, pattern_DemoAppService_GetDemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/demo.app.v1alpha1.DemoAppService/GetDemo", runtime.WithHTTPPathPattern("/v1alpha1/demo")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_DemoAppService_GetDemo_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_DemoAppService_GetDemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterDemoAppServiceHandlerFromEndpoint is same as RegisterDemoAppServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterDemoAppServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterDemoAppServiceHandler(ctx, mux, conn) +} + +// RegisterDemoAppServiceHandler registers the http handlers for service DemoAppService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterDemoAppServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterDemoAppServiceHandlerClient(ctx, mux, NewDemoAppServiceClient(conn)) +} + +// RegisterDemoAppServiceHandlerClient registers the http handlers for service DemoAppService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "DemoAppServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "DemoAppServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "DemoAppServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterDemoAppServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client DemoAppServiceClient) error { + mux.Handle(http.MethodGet, pattern_DemoAppService_GetDemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/demo.app.v1alpha1.DemoAppService/GetDemo", runtime.WithHTTPPathPattern("/v1alpha1/demo")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DemoAppService_GetDemo_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_DemoAppService_GetDemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_DemoAppService_GetDemo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1alpha1", "demo"}, "")) +) + +var ( + forward_DemoAppService_GetDemo_0 = runtime.ForwardResponseMessage +) diff --git a/api/demo/app/v1alpha1/app_grpc.pb.go b/api/demo/app/v1alpha1/app_grpc.pb.go new file mode 100644 index 0000000..46d49c3 --- /dev/null +++ b/api/demo/app/v1alpha1/app_grpc.pb.go @@ -0,0 +1,121 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.3 +// source: demo/app/v1alpha1/app.proto + +package demo + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + DemoAppService_GetDemo_FullMethodName = "/demo.app.v1alpha1.DemoAppService/GetDemo" +) + +// DemoAppServiceClient is the client API for DemoAppService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type DemoAppServiceClient interface { + GetDemo(ctx context.Context, in *GetDemoRequest, opts ...grpc.CallOption) (*GetDemoResponse, error) +} + +type demoAppServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewDemoAppServiceClient(cc grpc.ClientConnInterface) DemoAppServiceClient { + return &demoAppServiceClient{cc} +} + +func (c *demoAppServiceClient) GetDemo(ctx context.Context, in *GetDemoRequest, opts ...grpc.CallOption) (*GetDemoResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetDemoResponse) + err := c.cc.Invoke(ctx, DemoAppService_GetDemo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DemoAppServiceServer is the server API for DemoAppService service. +// All implementations must embed UnimplementedDemoAppServiceServer +// for forward compatibility. +type DemoAppServiceServer interface { + GetDemo(context.Context, *GetDemoRequest) (*GetDemoResponse, error) + mustEmbedUnimplementedDemoAppServiceServer() +} + +// UnimplementedDemoAppServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDemoAppServiceServer struct{} + +func (UnimplementedDemoAppServiceServer) GetDemo(context.Context, *GetDemoRequest) (*GetDemoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetDemo not implemented") +} +func (UnimplementedDemoAppServiceServer) mustEmbedUnimplementedDemoAppServiceServer() {} +func (UnimplementedDemoAppServiceServer) testEmbeddedByValue() {} + +// UnsafeDemoAppServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DemoAppServiceServer will +// result in compilation errors. +type UnsafeDemoAppServiceServer interface { + mustEmbedUnimplementedDemoAppServiceServer() +} + +func RegisterDemoAppServiceServer(s grpc.ServiceRegistrar, srv DemoAppServiceServer) { + // If the following call pancis, it indicates UnimplementedDemoAppServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&DemoAppService_ServiceDesc, srv) +} + +func _DemoAppService_GetDemo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetDemoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DemoAppServiceServer).GetDemo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DemoAppService_GetDemo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DemoAppServiceServer).GetDemo(ctx, req.(*GetDemoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// DemoAppService_ServiceDesc is the grpc.ServiceDesc for DemoAppService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DemoAppService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "demo.app.v1alpha1.DemoAppService", + HandlerType: (*DemoAppServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetDemo", + Handler: _DemoAppService_GetDemo_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "demo/app/v1alpha1/app.proto", +} diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..14e49e2 --- /dev/null +++ b/config.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=contrib/schema.json + +# Custom demo-app config +timezone: EST5EDT +opts: + factLang: en + factType: random + +# go-app config +name: Demo go-app +logging: + format: console + level: trace + enabled: true +otel: + enabled: true + stdoutEnabled: false +http: + enabled: true + listen: :8080 + logRequests: true +grpc: + enabled: true + enableReflection: true + listen: :8081 + grpcGatewayPath: /api + enableGRPCGateway: true + enableInstrumentation: true + logRequests: true diff --git a/contrib/go-server-with-otel.swagger.json b/contrib/go-server-with-otel.swagger.json new file mode 100644 index 0000000..df72bf5 --- /dev/null +++ b/contrib/go-server-with-otel.swagger.json @@ -0,0 +1,99 @@ +{ + "swagger": "2.0", + "info": { + "title": "demo/app/v1alpha1/app.proto", + "version": "version not set" + }, + "tags": [ + { + "name": "DemoAppService" + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/v1alpha1/demo": { + "get": { + "operationId": "DemoAppService_GetDemo", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1GetDemoResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "language", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "DemoAppService" + ] + } + } + }, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "v1alpha1GetDemoResponse": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "format": "date-time" + }, + "fact": { + "type": "string" + }, + "source": { + "type": "string" + }, + "language": { + "type": "string" + } + }, + "title": "Returns a randome fact, because this is a demo app\nso what else do we do?" + } + } +} diff --git a/contrib/schema.json b/contrib/schema.json new file mode 100644 index 0000000..2e0d80a --- /dev/null +++ b/contrib/schema.json @@ -0,0 +1,143 @@ +{ + "definitions": { + "ConfigDemoOpts": { + "properties": { + "factLang": { + "default": "en", + "type": "string" + }, + "factType": { + "default": "random", + "enum": [ + "today", + "random" + ], + "type": "string" + } + }, + "type": "object" + }, + "ConfigGRPCConfig": { + "properties": { + "enableGRPCGateway": { + "default": true, + "type": "boolean" + }, + "enableInstrumentation": { + "type": "boolean" + }, + "enableReflection": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "grpcGatewayPath": { + "default": "/grpc-api", + "type": "string" + }, + "listen": { + "type": "string" + }, + "logRequests": { + "type": "boolean" + } + }, + "type": "object" + }, + "ConfigHTTPConfig": { + "properties": { + "enabled": { + "type": "boolean" + }, + "idleTimeout": { + "type": "string" + }, + "listen": { + "type": "string" + }, + "logRequests": { + "type": "boolean" + }, + "readTimeout": { + "type": "string" + }, + "writeTimeout": { + "type": "string" + } + }, + "type": "object" + }, + "ConfigLogConfig": { + "properties": { + "enabled": { + "type": "boolean" + }, + "format": { + "type": "string" + }, + "level": { + "type": "string" + }, + "output": { + "type": "string" + }, + "timeFormat": { + "type": "string" + } + }, + "type": "object" + }, + "ConfigOTELConfig": { + "properties": { + "enabled": { + "type": "boolean" + }, + "metricIntervalSecs": { + "type": "integer" + }, + "prometheusEnabled": { + "type": "boolean" + }, + "prometheusPath": { + "type": "string" + }, + "stdoutEnabled": { + "type": "boolean" + } + }, + "type": "object" + } + }, + "properties": { + "environment": { + "type": "string" + }, + "grpc": { + "$ref": "#/definitions/ConfigGRPCConfig" + }, + "http": { + "$ref": "#/definitions/ConfigHTTPConfig" + }, + "logging": { + "$ref": "#/definitions/ConfigLogConfig" + }, + "name": { + "type": "string" + }, + "opts": { + "$ref": "#/definitions/ConfigDemoOpts" + }, + "otel": { + "$ref": "#/definitions/ConfigOTELConfig" + }, + "timezone": { + "default": "UTC", + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object" +} diff --git a/env-sample b/env-sample new file mode 100644 index 0000000..2cf253f --- /dev/null +++ b/env-sample @@ -0,0 +1,14 @@ +# App Config +APP_NAME="go-server-with-otel" +APP_LOG_LEVEL=trace ## For testing only +APP_LOG_FORMAT=console ## console, json +APP_LOG_TIME_FORMAT=long ## long, short, unix, rfc3339, off + +# App OTEL Config +APP_OTEL_STDOUT_ENABLED=true ## For testing only +APP_OTEL_METRIC_INTERVAL_SECS=15 + +# OTEL SDK Config +OTEL_EXPORTER_OTLP_ENDPOINT="otel-collector.otel.svc.cluster.local" # Set to your otel collector +OTEL_SERVICE_NAME="go-server-with-otel" +OTEL_RESOURCE_ATTRIBUTES="env=development,service.version=(devel)" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..19cecc4 --- /dev/null +++ b/go.mod @@ -0,0 +1,66 @@ +module gitea.libretechconsulting.com/rmcguire/go-server-with-otel + +go 1.24.1 + +require ( + gitea.libretechconsulting.com/rmcguire/go-app v0.9.2 + github.com/go-resty/resty/v2 v2.16.5 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 + github.com/rs/zerolog v1.34.0 + go.opentelemetry.io/otel/trace v1.35.0 + golang.org/x/sys v0.31.0 + google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 + google.golang.org/grpc v1.71.0 + google.golang.org/protobuf v1.36.6 +) + +require ( + github.com/caarlos0/env/v11 v11.3.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/swaggest/jsonschema-go v0.3.73 // indirect + github.com/swaggest/refl v1.3.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_golang v1.21.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.63.0 // indirect + github.com/prometheus/procfs v0.16.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect +) + +tool ( + github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway + github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 + google.golang.org/grpc/cmd/protoc-gen-go-grpc + google.golang.org/protobuf/cmd/protoc-gen-go +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..35959fd --- /dev/null +++ b/go.sum @@ -0,0 +1,145 @@ +gitea.libretechconsulting.com/rmcguire/go-app v0.9.2 h1:DTbGae0TR7O+kKI1ZE8txgFnGb0vsYX/urFUFuoZfQM= +gitea.libretechconsulting.com/rmcguire/go-app v0.9.2/go.mod h1:W6YHFSGf4nJrgs9DqEaw+3J6ufIARsr1zpOs/V6gRTQ= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bool64/dev v0.2.39 h1:kP8DnMGlWXhGYJEZE/J0l/gVBdbuhoPGL+MJG4QbofE= +github.com/bool64/dev v0.2.39/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 h1:KcFzXwzM/kGhIRHvc8jdixfIJjVzuUJdnv+5xsPutog= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= +github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +github.com/swaggest/jsonschema-go v0.3.73 h1:gU1pBzF3pkZ1GDD3dRMdQoCjrA0sldJ+QcM7aSSPgvc= +github.com/swaggest/jsonschema-go v0.3.73/go.mod h1:qp+Ym2DIXHlHzch3HKz50gPf2wJhKOrAB/VYqLS2oJU= +github.com/swaggest/refl v1.3.1 h1:XGplEkYftR7p9cz1lsiwXMM2yzmOymTE9vneVVpaOh4= +github.com/swaggest/refl v1.3.1/go.mod h1:4uUVFVfPJ0NSX9FPwMPspeHos9wPFlCMGoPRllUbpvA= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= +go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk= +go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= +google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..5b04e4a --- /dev/null +++ b/go.work.sum @@ -0,0 +1,152 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.4-20250130201111-63bb56e20495.1/go.mod h1:novQBstnxcGpfKf8qGRATqn1anQKwMJIbH5Q581jibU= +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc= +cloud.google.com/go/accessapproval v1.8.3/go.mod h1:3speETyAv63TDrDmo5lIkpVueFkQcQchkiw/TAMbBo4= +cloud.google.com/go/accesscontextmanager v1.9.3/go.mod h1:S1MEQV5YjkAKBoMekpGrkXKfrBdsi4x6Dybfq6gZ8BU= +cloud.google.com/go/aiplatform v1.74.0/go.mod h1:hVEw30CetNut5FrblYd1AJUWRVSIjoyIvp0EVUh51HA= +cloud.google.com/go/analytics v0.26.0/go.mod h1:KZWJfs8uX/+lTjdIjvT58SFa86V9KM6aPXwZKK6uNVI= +cloud.google.com/go/apigateway v1.7.3/go.mod h1:uK0iRHdl2rdTe79bHW/bTsKhhXPcFihjUdb7RzhTPf4= +cloud.google.com/go/apigeeconnect v1.7.3/go.mod h1:2ZkT5VCAqhYrDqf4dz7lGp4N/+LeNBSfou8Qs5bIuSg= +cloud.google.com/go/apigeeregistry v0.9.3/go.mod h1:oNCP2VjOeI6U8yuOuTmU4pkffdcXzR5KxeUD71gF+Dg= +cloud.google.com/go/appengine v1.9.3/go.mod h1:DtLsE/z3JufM/pCEIyVYebJ0h9UNPpN64GZQrYgOSyM= +cloud.google.com/go/area120 v0.9.3/go.mod h1:F3vxS/+hqzrjJo55Xvda3Jznjjbd+4Foo43SN5eMd8M= +cloud.google.com/go/artifactregistry v1.16.1/go.mod h1:sPvFPZhfMavpiongKwfg93EOwJ18Tnj9DIwTU9xWUgs= +cloud.google.com/go/asset v1.20.4/go.mod h1:DP09pZ+SoFWUZyPZx26xVroHk+6+9umnQv+01yfJxbM= +cloud.google.com/go/assuredworkloads v1.12.3/go.mod h1:iGBkyMGdtlsxhCi4Ys5SeuvIrPTeI6HeuEJt7qJgJT8= +cloud.google.com/go/automl v1.14.4/go.mod h1:sVfsJ+g46y7QiQXpVs9nZ/h8ntdujHm5xhjHW32b3n4= +cloud.google.com/go/baremetalsolution v1.3.3/go.mod h1:uF9g08RfmXTF6ZKbXxixy5cGMGFcG6137Z99XjxLOUI= +cloud.google.com/go/batch v1.12.0/go.mod h1:CATSBh/JglNv+tEU/x21Z47zNatLQ/gpGnpyKOzbbcM= +cloud.google.com/go/beyondcorp v1.1.3/go.mod h1:3SlVKnlczNTSQFuH5SSyLuRd4KaBSc8FH/911TuF/Cc= +cloud.google.com/go/bigquery v1.66.2/go.mod h1:+Yd6dRyW8D/FYEjUGodIbu0QaoEmgav7Lwhotup6njo= +cloud.google.com/go/bigtable v1.35.0/go.mod h1:EabtwwmTcOJFXp+oMZAT/jZkyDIjNwrv53TrS4DGrrM= +cloud.google.com/go/billing v1.20.1/go.mod h1:DhT80hUZ9gz5UqaxtK/LNoDELfxH73704VTce+JZqrY= +cloud.google.com/go/binaryauthorization v1.9.3/go.mod h1:f3xcb/7vWklDoF+q2EaAIS+/A/e1278IgiYxonRX+Jk= +cloud.google.com/go/certificatemanager v1.9.3/go.mod h1:O5T4Lg/dHbDHLFFooV2Mh/VsT3Mj2CzPEWRo4qw5prc= +cloud.google.com/go/channel v1.19.2/go.mod h1:syX5opXGXFt17DHCyCdbdlM464Tx0gHMi46UlEWY9Gg= +cloud.google.com/go/cloudbuild v1.22.0/go.mod h1:p99MbQrzcENHb/MqU3R6rpqFRk/X+lNG3PdZEIhM95Y= +cloud.google.com/go/clouddms v1.8.4/go.mod h1:RadeJ3KozRwy4K/gAs7W74ZU3GmGgVq5K8sRqNs3HfA= +cloud.google.com/go/cloudtasks v1.13.3/go.mod h1:f9XRvmuFTm3VhIKzkzLCPyINSU3rjjvFUsFVGR5wi24= +cloud.google.com/go/compute v1.34.0/go.mod h1:zWZwtLwZQyonEvIQBuIa0WvraMYK69J5eDCOw9VZU4g= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/contactcenterinsights v1.17.1/go.mod h1:n8OiNv7buLA2AkGVkfuvtW3HU13AdTmEwAlAu46bfxY= +cloud.google.com/go/container v1.42.2/go.mod h1:y71YW7uR5Ck+9Vsbst0AF2F3UMgqmsN4SP8JR9xEsR8= +cloud.google.com/go/containeranalysis v0.13.3/go.mod h1:0SYnagA1Ivb7qPqKNYPkCtphhkJn3IzgaSp3mj+9XAY= +cloud.google.com/go/datacatalog v1.24.3/go.mod h1:Z4g33XblDxWGHngDzcpfeOU0b1ERlDPTuQoYG6NkF1s= +cloud.google.com/go/dataflow v0.10.3/go.mod h1:5EuVGDh5Tg4mDePWXMMGAG6QYAQhLNyzxdNQ0A1FfW4= +cloud.google.com/go/dataform v0.10.3/go.mod h1:8SruzxHYCxtvG53gXqDZvZCx12BlsUchuV/JQFtyTCw= +cloud.google.com/go/datafusion v1.8.3/go.mod h1:hyglMzE57KRf0Rf/N2VRPcHCwKfZAAucx+LATY6Jc6Q= +cloud.google.com/go/datalabeling v0.9.3/go.mod h1:3LDFUgOx+EuNUzDyjU7VElO8L+b5LeaZEFA/ZU1O1XU= +cloud.google.com/go/dataplex v1.22.0/go.mod h1:g166QMCGHvwc3qlTG4p34n+lHwu7JFfaNpMfI2uO7b8= +cloud.google.com/go/dataproc/v2 v2.11.0/go.mod h1:9vgGrn57ra7KBqz+B2KD+ltzEXvnHAUClFgq/ryU99g= +cloud.google.com/go/dataqna v0.9.3/go.mod h1:PiAfkXxa2LZYxMnOWVYWz3KgY7txdFg9HEMQPb4u1JA= +cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew= +cloud.google.com/go/datastream v1.13.0/go.mod h1:GrL2+KC8mV4GjbVG43Syo5yyDXp3EH+t6N2HnZb1GOQ= +cloud.google.com/go/deploy v1.26.2/go.mod h1:XpS3sG/ivkXCfzbzJXY9DXTeCJ5r68gIyeOgVGxGNEs= +cloud.google.com/go/dialogflow v1.66.0/go.mod h1:BPiRTnnXP/tHLot5h/U62Xcp+i6ekRj/bq6uq88p+Lw= +cloud.google.com/go/dlp v1.21.0/go.mod h1:Y9HOVtPoArpL9sI1O33aN/vK9QRwDERU9PEJJfM8DvE= +cloud.google.com/go/documentai v1.35.2/go.mod h1:oh/0YXosgEq3hVhyH4ZQ7VNXPaveRO4eLVM3tBSZOsI= +cloud.google.com/go/domains v0.10.3/go.mod h1:m7sLe18p0PQab56bVH3JATYOJqyRHhmbye6gz7isC7o= +cloud.google.com/go/edgecontainer v1.4.1/go.mod h1:ubMQvXSxsvtEjJLyqcPFrdWrHfvjQxdoyt+SUrAi5ek= +cloud.google.com/go/errorreporting v0.3.2/go.mod h1:s5kjs5r3l6A8UUyIsgvAhGq6tkqyBCUss0FRpsoVTww= +cloud.google.com/go/essentialcontacts v1.7.3/go.mod h1:uimfZgDbhWNCmBpwUUPHe4vcMY2azsq/axC9f7vZFKI= +cloud.google.com/go/eventarc v1.15.1/go.mod h1:K2luolBpwaVOujZQyx6wdG4n2Xum4t0q1cMBmY1xVyI= +cloud.google.com/go/filestore v1.9.3/go.mod h1:Me0ZRT5JngT/aZPIKpIK6N4JGMzrFHRtGHd9ayUS4R4= +cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= +cloud.google.com/go/functions v1.19.3/go.mod h1:nOZ34tGWMmwfiSJjoH/16+Ko5106x+1Iji29wzrBeOo= +cloud.google.com/go/gkebackup v1.6.3/go.mod h1:JJzGsA8/suXpTDtqI7n9RZW97PXa2CIp+n8aRC/y57k= +cloud.google.com/go/gkeconnect v0.12.1/go.mod h1:L1dhGY8LjINmWfR30vneozonQKRSIi5DWGIHjOqo58A= +cloud.google.com/go/gkehub v0.15.3/go.mod h1:nzFT/Q+4HdQES/F+FP1QACEEWR9Hd+Sh00qgiH636cU= +cloud.google.com/go/gkemulticloud v1.5.1/go.mod h1:OdmhfSPXuJ0Kn9dQ2I3Ou7XZ3QK8caV4XVOJZwrIa3s= +cloud.google.com/go/gsuiteaddons v1.7.4/go.mod h1:gpE2RUok+HUhuK7RPE/fCOEgnTffS0lCHRaAZLxAMeE= +cloud.google.com/go/iam v1.4.0/go.mod h1:gMBgqPaERlriaOV0CUl//XUzDhSfXevn4OEUbg6VRs4= +cloud.google.com/go/iap v1.10.3/go.mod h1:xKgn7bocMuCFYhzRizRWP635E2LNPnIXT7DW0TlyPJ8= +cloud.google.com/go/ids v1.5.3/go.mod h1:a2MX8g18Eqs7yxD/pnEdid42SyBUm9LIzSWf8Jux9OY= +cloud.google.com/go/iot v1.8.3/go.mod h1:dYhrZh+vUxIQ9m3uajyKRSW7moF/n0rYmA2PhYAkMFE= +cloud.google.com/go/kms v1.21.0/go.mod h1:zoFXMhVVK7lQ3JC9xmhHMoQhnjEDZFoLAr5YMwzBLtk= +cloud.google.com/go/language v1.14.3/go.mod h1:hjamj+KH//QzF561ZuU2J+82DdMlFUjmiGVWpovGGSA= +cloud.google.com/go/lifesciences v0.10.3/go.mod h1:hnUUFht+KcZcliixAg+iOh88FUwAzDQQt5tWd7iIpNg= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs= +cloud.google.com/go/managedidentities v1.7.3/go.mod h1:H9hO2aMkjlpY+CNnKWRh+WoQiUIDO8457wWzUGsdtLA= +cloud.google.com/go/maps v1.19.0/go.mod h1:goHUXrmzoZvQjUVd0KGhH8t3AYRm17P8b+fsyR1UAmQ= +cloud.google.com/go/mediatranslation v0.9.3/go.mod h1:KTrFV0dh7duYKDjmuzjM++2Wn6yw/I5sjZQVV5k3BAA= +cloud.google.com/go/memcache v1.11.3/go.mod h1:UeWI9cmY7hvjU1EU6dwJcQb6EFG4GaM3KNXOO2OFsbI= +cloud.google.com/go/metastore v1.14.3/go.mod h1:HlbGVOvg0ubBLVFRk3Otj3gtuzInuzO/TImOBwsKlG4= +cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= +cloud.google.com/go/networkconnectivity v1.16.1/go.mod h1:GBC1iOLkblcnhcnfRV92j4KzqGBrEI6tT7LP52nZCTk= +cloud.google.com/go/networkmanagement v1.18.0/go.mod h1:yTxpAFuvQOOKgL3W7+k2Rp1bSKTxyRcZ5xNHGdHUM6w= +cloud.google.com/go/networksecurity v0.10.3/go.mod h1:G85ABVcPscEgpw+gcu+HUxNZJWjn3yhTqEU7+SsltFM= +cloud.google.com/go/notebooks v1.12.3/go.mod h1:I0pMxZct+8Rega2LYrXL8jGAGZgLchSmh8Ksc+0xNyA= +cloud.google.com/go/optimization v1.7.3/go.mod h1:GlYFp4Mju0ybK5FlOUtV6zvWC00TIScdbsPyF6Iv144= +cloud.google.com/go/orchestration v1.11.4/go.mod h1:UKR2JwogaZmDGnAcBgAQgCPn89QMqhXFUCYVhHd31vs= +cloud.google.com/go/orgpolicy v1.14.2/go.mod h1:2fTDMT3X048iFKxc6DEgkG+a/gN+68qEgtPrHItKMzo= +cloud.google.com/go/osconfig v1.14.3/go.mod h1:9D2MS1Etne18r/mAeW5jtto3toc9H1qu9wLNDG3NvQg= +cloud.google.com/go/oslogin v1.14.3/go.mod h1:fDEGODTG/W9ZGUTHTlMh8euXWC1fTcgjJ9Kcxxy14a8= +cloud.google.com/go/phishingprotection v0.9.3/go.mod h1:ylzN9HruB/X7dD50I4sk+FfYzuPx9fm5JWsYI0t7ncc= +cloud.google.com/go/policytroubleshooter v1.11.3/go.mod h1:AFHlORqh4AnMC0twc2yPKfzlozp3DO0yo9OfOd9aNOs= +cloud.google.com/go/privatecatalog v0.10.4/go.mod h1:n/vXBT+Wq8B4nSRUJNDsmqla5BYjbVxOlHzS6PjiF+w= +cloud.google.com/go/pubsub v1.47.0/go.mod h1:LaENesmga+2u0nDtLkIOILskxsfvn/BXX9Ak1NFxOs8= +cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI= +cloud.google.com/go/recaptchaenterprise/v2 v2.19.4/go.mod h1:WaglfocMJGkqZVdXY/FVB7OhoVRONPS4uXqtNn6HfX0= +cloud.google.com/go/recommendationengine v0.9.3/go.mod h1:QRnX5aM7DCvtqtSs7I0zay5Zfq3fzxqnsPbZF7pa1G8= +cloud.google.com/go/recommender v1.13.3/go.mod h1:6yAmcfqJRKglZrVuTHsieTFEm4ai9JtY3nQzmX4TC0Q= +cloud.google.com/go/redis v1.18.0/go.mod h1:fJ8dEQJQ7DY+mJRMkSafxQCuc8nOyPUwo9tXJqjvNEY= +cloud.google.com/go/resourcemanager v1.10.3/go.mod h1:JSQDy1JA3K7wtaFH23FBGld4dMtzqCoOpwY55XYR8gs= +cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw= +cloud.google.com/go/retail v1.19.2/go.mod h1:71tRFYAcR4MhrZ1YZzaJxr030LvaZiIcupH7bXfFBcY= +cloud.google.com/go/run v1.9.0/go.mod h1:Dh0+mizUbtBOpPEzeXMM22t8qYQpyWpfmUiWQ0+94DU= +cloud.google.com/go/scheduler v1.11.4/go.mod h1:0ylvH3syJnRi8EDVo9ETHW/vzpITR/b+XNnoF+GPSz4= +cloud.google.com/go/secretmanager v1.14.5/go.mod h1:GXznZF3qqPZDGZQqETZwZqHw4R6KCaYVvcGiRBA+aqY= +cloud.google.com/go/security v1.18.3/go.mod h1:NmlSnEe7vzenMRoTLehUwa/ZTZHDQE59IPRevHcpCe4= +cloud.google.com/go/securitycenter v1.36.0/go.mod h1:AErAQqIvrSrk8cpiItJG1+ATl7SD7vQ6lgTFy/Tcs4Q= +cloud.google.com/go/servicedirectory v1.12.3/go.mod h1:dwTKSCYRD6IZMrqoBCIvZek+aOYK/6+jBzOGw8ks5aY= +cloud.google.com/go/shell v1.8.3/go.mod h1:OYcrgWF6JSp/uk76sNTtYFlMD0ho2+Cdzc7U3P/bF54= +cloud.google.com/go/spanner v1.76.1/go.mod h1:YtwoE+zObKY7+ZeDCBtZ2ukM+1/iPaMfUM+KnTh/sx0= +cloud.google.com/go/speech v1.26.0/go.mod h1:78bqDV2SgwFlP/M4n3i3PwLthFq6ta7qmyG6lUV7UCA= +cloud.google.com/go/storagetransfer v1.12.1/go.mod h1:hQqbfs8/LTmObJyCC0KrlBw8yBJ2bSFlaGila0qBMk4= +cloud.google.com/go/talent v1.8.0/go.mod h1:/gvOzSrtMcfTL/9xWhdYaZATaxUNhQ+L+3ZaGOGs7bA= +cloud.google.com/go/texttospeech v1.11.0/go.mod h1:7M2ro3I2QfIEvArFk1TJ+pqXJqhszDtxUpnIv/150As= +cloud.google.com/go/tpu v1.8.0/go.mod h1:XyNzyK1xc55WvL5rZEML0Z9/TUHDfnq0uICkQw6rWMo= +cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= +cloud.google.com/go/translate v1.12.3/go.mod h1:qINOVpgmgBnY4YTFHdfVO4nLrSBlpvlIyosqpGEgyEg= +cloud.google.com/go/video v1.23.3/go.mod h1:Kvh/BheubZxGZDXSb0iO6YX7ZNcaYHbLjnnaC8Qyy3g= +cloud.google.com/go/videointelligence v1.12.3/go.mod h1:dUA6V+NH7CVgX6TePq0IelVeBMGzvehxKPR4FGf1dtw= +cloud.google.com/go/vision/v2 v2.9.3/go.mod h1:weAcT8aNYSgrWWVTC2PuJTc7fcXKvUeAyDq8B6HkLSg= +cloud.google.com/go/vmmigration v1.8.3/go.mod h1:8CzUpK9eBzohgpL4RvBVtW4sY/sDliVyQonTFQfWcJ4= +cloud.google.com/go/vmwareengine v1.3.3/go.mod h1:G7vz05KGijha0c0dj1INRKyDAaQW8TRMZt/FrfOZVXc= +cloud.google.com/go/vpcaccess v1.8.3/go.mod h1:bqOhyeSh/nEmLIsIUoCiQCBHeNPNjaK9M3bIvKxFdsY= +cloud.google.com/go/webrisk v1.10.3/go.mod h1:rRAqCA5/EQOX8ZEEF4HMIrLHGTK/Y1hEQgWMnih+jAw= +cloud.google.com/go/websecurityscanner v1.7.3/go.mod h1:gy0Kmct4GNLoCePWs9xkQym1D7D59ld5AjhXrjipxSs= +cloud.google.com/go/workflows v1.13.3/go.mod h1:Xi7wggEt/ljoEcyk+CB/Oa1AHBCk0T1f5UH/exBB5CE= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/bufbuild/protovalidate-go v0.9.1/go.mod h1:5jptBxfvlY51RhX32zR6875JfPBRXUsQjyZjm/NqkLQ= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/google/cel-go v0.23.0/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/helm/.gitignore b/helm/.gitignore new file mode 100644 index 0000000..f791801 --- /dev/null +++ b/helm/.gitignore @@ -0,0 +1,2 @@ +charts/ +Chart.lock diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..1b7d1c5 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,30 @@ +apiVersion: v2 +name: go-server-with-otel +description: Golang HTTP and GRPC server with OTEL + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "v0.1.0" + +dependencies: + - name: hull + repository: https://vidispine.github.io/hull + version: 1.32.2 + diff --git a/helm/library-hull.yaml b/helm/library-hull.yaml new file mode 100644 index 0000000..6d338db --- /dev/null +++ b/helm/library-hull.yaml @@ -0,0 +1,643 @@ +################################ +### values.yaml for HULL +### The basic pre-configuration takes place here. +### +### Do not change this file, use additional values.hull.yaml +### to overwrite the selected fields! +################################ + +################################################### +### CONFIG +config: + general: + rbac: true + fullnameOverride: "" + nameOverride: "" + namespaceOverride: "" + noObjectNamePrefixes: false + createImagePullSecretsFromRegistries: true + globalImageRegistryServer: "" + globalImageRegistryToFirstRegistrySecretServer: false + serialization: + configmap: + enabled: true + fileExtensions: + json: toPrettyJson + yml: toYaml + yaml: toYaml + secret: + enabled: true + fileExtensions: + json: toPrettyJson + yml: toYaml + yaml: toYaml + render: + passes: 3 + emptyLabels: false + emptyAnnotations: false + emptyTemplateLabels: false + emptyTemplateAnnotations: false + emptyHullObjects: false + postRender: + globalStringReplacements: + instanceKey: + enabled: false + string: _HULL_OBJECT_TYPE_DEFAULT_ + replacement: OBJECT_INSTANCE_KEY + instanceKeyResolved: + enabled: false + string: _HULL_OBJECT_TYPE_DEFAULT_ + replacement: OBJECT_INSTANCE_KEY_RESOLVED + instanceName: + enabled: false + string: _HULL_OBJECT_TYPE_DEFAULT_ + replacement: OBJECT_INSTANCE_NAME + errorChecks: + objectYamlValid: true + hullGetTransformationReferenceValid: true + containerImageValid: true + virtualFolderDataPathExists: true + virtualFolderDataInlineValid: false + debug: + renderBrokenHullGetTransformationReferences: false + renderNilWhenInlineIsNil: false + renderPathMissingWhenPathIsNonExistent: false + metadata: + labels: + common: + 'app.kubernetes.io/managed-by': + 'app.kubernetes.io/version': + 'app.kubernetes.io/part-of': + 'app.kubernetes.io/name': + 'app.kubernetes.io/instance': + 'app.kubernetes.io/component': + 'helm.sh/chart': + 'vidispine.hull/version': + custom: {} + annotations: + hashes: false + custom: {} + data: {} + specific: {} + templates: + pod: + global: {} + container: + global: {} +################################################### + +################################################### +### OBJECTS +objects: + +# NAMESPACE + namespace: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + staticName: true + annotations: {} + labels: {} +################################################### + +# CONFIGMAPS + configmap: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# SECRETS + secret: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# REGISTRIES + registry: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# SERVICEACCOUNTS + serviceaccount: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + default: + enabled: _HT?eq (dig "serviceAccountName" "" _HT*hull.config.templates.pod.global) "" + annotations: {} + labels: {} +################################################### + +# ROLES + role: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: {} + default: + enabled: _HT?eq (dig "serviceAccountName" "" _HT*hull.config.templates.pod.global) "" + rules: {} +################################################### + +# ROLEBINDINGS + rolebinding: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + default: + enabled: _HT?eq (dig "serviceAccountName" "" _HT*hull.config.templates.pod.global) "" + roleRef: + apiGroup: "rbac.authorization.k8s.io" + kind: "Role" + name: _HT^default + subjects: + - kind: ServiceAccount + name: _HT^default + namespace: _HT**Release.Namespace +################################################### + +# CLUSTERROLES + clusterrole: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# CLUSTERROLEBINDINGS + clusterrolebinding: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# CUSTOMRESOURCEDEFINITIONS (deprecated with Helm3) +# customresourcedefinitions: +# _HULL_OBJECT_TYPE_DEFAULT_: +# enabled: true +# annotations: {} +# labels: {} +################################################### + +# CUSTOMRESOURCES + customresource: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# PERSISTENTVOLUMECLAIMS + persistentvolumeclaim: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# PERSISTENTVOLUMES + persistentvolume: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# STORAGECLASSES + storageclass: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# SERVICES + service: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + ports: + _HULL_OBJECT_TYPE_DEFAULT_: {} + +################################################### + +# INGRESSES + ingress: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + tls: + _HULL_OBJECT_TYPE_DEFAULT_: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: + http: + paths: + _HULL_OBJECT_TYPE_DEFAULT_: {} + +################################################### + +# INGRESSCLASSES + ingressclass: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + +################################################### + +# DEPLOYMENTS + deployment: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + templateAnnotations: {} + templateLabels: {} + pod: + initContainers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + containers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumes: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# JOBS + job: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + templateAnnotations: {} + templateLabels: {} + pod: + initContainers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + containers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumes: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# CRONJOBS + cronjob: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + job: + templateAnnotations: {} + templateLabels: {} + pod: + initContainers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + containers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumes: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# DAEMONSETS + daemonset: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + templateAnnotations: {} + templateLabels: {} + pod: + initContainers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + containers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumes: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# STATEFULSETS + statefulset: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + templateAnnotations: {} + templateLabels: {} + pod: + initContainers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + containers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumes: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# SERVICEMONITORS + servicemonitor: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# HORIZONTALPODAUTOSCALER + horizontalpodautoscaler: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# PODDISRUPTIONBUDGET + poddisruptionbudget: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# PRIORITYCLASS + priorityclass: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# ENDPOINTS + endpoints: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# ENDPOINTSLICE + endpointslice: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# LIMITRANGE + limitrange: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# MUTATINGWEBHOOKCONFIGURATION + mutatingwebhookconfiguration: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + webhooks: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# VALIDATINGWEBHOOKCONFIGURATION + validatingwebhookconfiguration: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + webhooks: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# RESOURCEQUOTA + resourcequota: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################## + +# NETWORKPOLICY + networkpolicy: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################## + +# GATEWAY API - BACKENDLBPOLICY + backendlbpolicy: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + targetRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + +################################################## + +# GATEWAY API - BACKENDTLSPOLICY + backendtlspolicy: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + targetRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - GATEWAYCLASS + gatewayclass: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################## + +# GATEWAY API - GATEWAY + gateway: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + addresses: + _HULL_OBJECT_TYPE_DEFAULT_: {} + listeners: + _HULL_OBJECT_TYPE_DEFAULT_: + tls: + certificateRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + frontendValidation: + caCertificateRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + allowedRoutes: + kinds: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - GRPCROUTE + grpcroute: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + parentRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: + matches: + _HULL_OBJECT_TYPE_DEFAULT_: {} + filters: + _HULL_OBJECT_TYPE_DEFAULT_: {} + backendRefs: + _HULL_OBJECT_TYPE_DEFAULT_: + filters: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - REFERENCEGRANT + referencegrant: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + from: + _HULL_OBJECT_TYPE_DEFAULT_: {} + to: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - TCPROUTE + tcproute: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + parentRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: + backendRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - TLSROUTE + tlsroute: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + parentRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: + backendRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - UDPROUTE + udproute: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + parentRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: + backendRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - HTTPROUTE + httproute: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + parentRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: + matches: + _HULL_OBJECT_TYPE_DEFAULT_: {} + filters: + _HULL_OBJECT_TYPE_DEFAULT_: {} + backendRefs: + _HULL_OBJECT_TYPE_DEFAULT_: + filters: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## diff --git a/helm/templates/app.yaml b/helm/templates/app.yaml new file mode 100644 index 0000000..1739244 --- /dev/null +++ b/helm/templates/app.yaml @@ -0,0 +1 @@ +{{- include "hull.objects.prepare.all" (dict "HULL_ROOT_KEY" "hull" "ROOT_CONTEXT" $) }} diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 0000000..07e988c --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,195 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/vidispine/hull/refs/heads/main/hull/values.schema.json +hull: + config: + ## go-server-with-otel custom settings (config.yaml) + appConfig: + # Custom app config + timezone: EST5EDT + opts: + factLang: en + factType: random + # go-app config + name: Demo go-app + logging: + format: json + level: debug + enabled: true + otel: + enabled: true + stdoutEnabled: false + http: + enabled: true + listen: :8080 + logRequests: true + grpc: + enabled: true + enableReflection: true + listen: :8081 + grpcGatewayPath: /api + enableGRPCGateway: true + enableInstrumentation: true + logRequests: true + ## Chart settings + settings: + resources: {} # Applies to the app container + repo: gitea.libretechconsulting.com/rmcguire/go-server-with-otel + tag: _HT**Chart.AppVersion + + httpPort: 8080 # Should match appConfig http.listen + grpcPort: 8081 # Should match appConfig grpc.listen + + # Use this as a shortcut, or create your own hull.objects.httproute + httproute: + enabled: false + hostnames: + - app.mydomain.com + gatewayName: istio-ingressgateway + gatewayNamespace: istio-system + + # Use this as a shortcut, or create your own hull.objects.grpcroute + grpcroute: + enabled: false + hostnames: + - app.mydomain.com + gatewayName: istio-ingressgateway + gatewayNamespace: istio-system + + otelServiceName: go-server-with-otel + otelResourceAttributes: app=go-server-with-otel + otlpEndpoint: http://otel.otel.svc.cluster.local:4317 # Replace me + + serviceType: ClusterIP + serviceLbIP: "" # Used if serviceTyps=LoadBalancer + + general: + rbac: false + render: + passes: 2 + # Applies to all objects + metadata: + labels: + custom: + app: _HT**Release.Name + version: _HT**Chart.AppVersion + + objects: + configmap: + config: + data: + config.yaml: + serialization: toYaml + inline: _HT*hull.config.appConfig + environment: + data: + OTEL_EXPORTER_OTLP_ENDPOINT: + serialization: none + inline: _HT*hull.config.settings.otlpEndpoint + OTEL_SERVICE_NAME: + serialization: none + inline: _HT*hull.config.settings.otelServiceName + OTEL_RESOURCE_ATTRIBUTES: + serialization: none + inline: _HT! + {{ printf "deployment.name=%s,%s" _HT**Release.Name _HT*hull.config.settings.otelResourceAttributes }} + serviceaccount: + default: + enabled: false + role: + default: + enabled: false + rolebinding: + default: + enabled: false + + deployment: + main: + pod: + containers: + main: + image: + repository: _HT*hull.config.settings.repo + tag: _HT*hull.config.settings.tag + imagePullPolicy: Always + args: + - -config + - /app/config.yaml + ports: + http: + containerPort: _HT*hull.config.settings.httpPort + grpc: + containerPort: _HT*hull.config.settings.grpcPort + envFrom: + main: + configMapRef: + name: environment + resources: _HT*hull.config.settings.resources + readinessProbe: + httpGet: + path: /health + port: 8080 + scheme: HTTP + periodSeconds: 10 + failureThreshold: 2 + livenessProbe: + httpGet: + path: /health + port: 8080 + scheme: HTTP + periodSeconds: 10 + failureThreshold: 2 + volumeMounts: + config: + name: config + mountPath: /app/config.yaml + subPath: config.yaml + volumes: + environment: + configMap: + name: environment + config: + configMap: + name: config + + service: + main: + type: _HT*hull.config.settings.serviceType + loadBalancerIP: _HT*hull.config.settings.serviceLbIP + ports: + http: + port: _HT*hull.config.settings.httpPort + targetPort: http + grpc: + port: _HT*hull.config.settings.grpcPort + targetPort: grpc + + httproute: + main: + enabled: _HT*hull.config.settings.httproute.enabled + hostnames: _HT*hull.config.settings.httproute.hostnames + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: _HT*hull.config.settings.httproute.gatewayName + namespace: _HT*hull.config.settings.httproute.gatewayNamespace + rules: + - backendRefs: + - group: "" + kind: Service + name: _HT^main + port: _HT*hull.config.settings.httpPort + + grpcroute: + main: + enabled: _HT*hull.config.settings.grpcroute.enabled + hostnames: _HT*hull.config.settings.grpcroute.hostnames + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: _HT*hull.config.settings.grpcroute.gatewayName + namespace: _HT*hull.config.settings.grpcroute.gatewayNamespace + rules: + - backendRefs: + - group: "" + kind: Service + name: _HT^main + port: _HT*hull.config.settings.grpcPort diff --git a/main.go b/main.go new file mode 100644 index 0000000..45db6ef --- /dev/null +++ b/main.go @@ -0,0 +1,89 @@ +// This template contains a simple +// app with OTEL bootstrap that will create an +// HTTP server configured by environment that exports +// spans and metrics to an OTEL collector if configured +// to do so. Will also stand up a prometheus metrics +// endpoint. +// +// Configuration and logger stored in context +// Reference implementation of the provided packages +package main + +import ( + "context" + "flag" + "fmt" + "os" + "os/signal" + + "github.com/rs/zerolog/log" + "golang.org/x/sys/unix" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/app" + optsgrpc "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/grpc/opts" + optshttp "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/http/opts" + + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/config" + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/demogrpc" + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/demohttp" +) + +var flagSchema bool + +func main() { + ctx, cncl := signal.NotifyContext(context.Background(), os.Interrupt, unix.SIGTERM) + defer cncl() + + // Load configuration and setup logging. The go-app framework + // will handle loading config and environment into our demo + // app config struct which embeds app.AooConfig + ctx, demoApp := app.MustLoadConfigInto(ctx, &config.DemoConfig{}) + + // Print schema if that's all we have to do + if flagSchema { + printSchema() + os.Exit(1) + } + + log.Debug().Any("demoAppMergedConfig", demoApp).Msg("demo app config prepared") + + // Prepare servers + demoHTTP := demohttp.NewDemoHTTPServer(ctx, demoApp) + demoGRPC := demogrpc.NewDemoGRPCServer(ctx, demoApp) + + // Prepare app + app := &app.App{ + AppContext: ctx, + GRPC: &optsgrpc.AppGRPC{ + Services: demoGRPC.GetServices(), + GRPCDialOpts: demoGRPC.GetDialOpts(), + }, + HTTP: &optshttp.AppHTTP{ + Ctx: ctx, + HealthChecks: demoHTTP.GetHealthCheckFuncs(), + Funcs: demoHTTP.GetHandleFuncs(), + }, + } + + // Launch app + app.MustRun() + + // Wait for app to complete + // Perform any extra shutdown here + <-app.Done() +} + +// flag.Parse will be called by go-app +func init() { + flag.BoolVar(&flagSchema, "schema", false, "generate json schema and exit") +} + +func printSchema() { + bytes, err := app.CustomSchema(&config.DemoConfig{}) + if err != nil { + panic(err) + } + + fmt.Println(string(bytes)) + os.Exit(0) +} diff --git a/pkg/config/custom.go b/pkg/config/custom.go new file mode 100644 index 0000000..d2997eb --- /dev/null +++ b/pkg/config/custom.go @@ -0,0 +1,30 @@ +// This serves as a reference sample configuration +// to show how to merge custom config fields into +// go-app configuration +package config + +import ( + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" +) + +type DemoConfig struct { + Timezone string `yaml:"timezone" json:"timezone,omitempty" default:"UTC"` + Opts *DemoOpts `yaml:"opts" json:"opts,omitempty"` + + // Embeds go-app config, used by go-app to + // merge custom config into go-app config, and to produce + // a complete configuration json schema + *config.AppConfig +} + +type DemoOpts struct { + FactLang string `yaml:"factLang" json:"factLang,omitempty" default:"en"` + FactType FactType `yaml:"factType" json:"factType,omitempty" default:"random" enum:"today,random"` +} + +type FactType string + +const ( + TypeToday FactType = "today" + TypeRandom FactType = "random" +) diff --git a/pkg/demogrpc/demo.go b/pkg/demogrpc/demo.go new file mode 100644 index 0000000..ddc628b --- /dev/null +++ b/pkg/demogrpc/demo.go @@ -0,0 +1,86 @@ +package demogrpc + +import ( + "context" + "fmt" + + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + + grpccodes "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" + + pb "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/demo/app/v1alpha1" + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/config" +) + +const ( + defaultFactType = config.TypeRandom + factsBaseURI = "https://uselessfacts.jsph.pl/api/v2/facts" +) + +type DemoGRPCServer struct { + tracer trace.Tracer + ctx context.Context + cfg *config.DemoConfig + client *resty.Client + pb.UnimplementedDemoAppServiceServer +} + +func NewDemoGRPCServer(ctx context.Context, cfg *config.DemoConfig) *DemoGRPCServer { + if cfg.Opts == nil { + cfg.Opts = &config.DemoOpts{} + } + + if cfg.Opts.FactType == "" { + cfg.Opts.FactType = config.TypeRandom + } + + return &DemoGRPCServer{ + ctx: ctx, + cfg: cfg, + tracer: otel.GetTracer(ctx, "demoGRPCServer"), + client: resty.New().SetBaseURL(factsBaseURI), + } +} + +func (d *DemoGRPCServer) GetDemo(ctx context.Context, req *pb.GetDemoRequest) ( + *pb.GetDemoResponse, error, +) { + ctx, span := d.tracer.Start(ctx, "getDemo", trace.WithAttributes( + attribute.String("language", req.GetLanguage()), + )) + defer span.End() + + r := d.client.R().SetContext(ctx) + + if req.GetLanguage() != "" { + r = r.SetQueryParam("language", req.GetLanguage()) + } + + fact := new(RandomFact) + r.SetResult(fact) + + resp, err := r.Get(fmt.Sprintf("/%s", d.cfg.Opts.FactType)) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + + zerolog.Ctx(d.ctx).Err(err).Send() + + return nil, status.Errorf(grpccodes.Unknown, "%s: %s", + err.Error(), string(resp.Body())) + } + + span.SetAttributes(attribute.String("fact", fact.Text)) + span.SetStatus(codes.Ok, "") + + zerolog.Ctx(d.ctx).Debug().Str("fact", fact.Text).Msg("retrieved fact") + + return fact.FactToProto(), nil +} diff --git a/pkg/demogrpc/fact.go b/pkg/demogrpc/fact.go new file mode 100644 index 0000000..8a1603b --- /dev/null +++ b/pkg/demogrpc/fact.go @@ -0,0 +1,25 @@ +package demogrpc + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + pb "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/demo/app/v1alpha1" +) + +type RandomFact struct { + ID string `json:"id,omitempty"` + Text string `json:"text,omitempty"` + Source string `json:"source,omitempty"` + SourceURL string `json:"source_url,omitempty"` + Language string `json:"language,omitempty"` + Permalink string `json:"permalink,omitempty"` +} + +func (f *RandomFact) FactToProto() *pb.GetDemoResponse { + return &pb.GetDemoResponse{ + Timestamp: timestamppb.Now(), + Fact: f.Text, + Source: f.SourceURL, + Language: f.Language, + } +} diff --git a/pkg/demogrpc/server.go b/pkg/demogrpc/server.go new file mode 100644 index 0000000..c8f6bac --- /dev/null +++ b/pkg/demogrpc/server.go @@ -0,0 +1,30 @@ +package demogrpc + +import ( + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/grpc/opts" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + demoAppPb "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/demo/app/v1alpha1" +) + +func (ds *DemoGRPCServer) GetDialOpts() []grpc.DialOption { + return []grpc.DialOption{ + // NOTE: Necessary for grpc-gateway to connect to grpc server + // Update if grpc service has credentials, tls, etc.. + grpc.WithTransportCredentials(insecure.NewCredentials()), + } +} + +func (ds *DemoGRPCServer) GetServices() []*opts.GRPCService { + return []*opts.GRPCService{ + { + Name: "Demo App", + Type: &demoAppPb.DemoAppService_ServiceDesc, + Service: ds, + GwRegistrationFuncs: []opts.GwRegistrationFunc{ + demoAppPb.RegisterDemoAppServiceHandler, + }, + }, + } +} diff --git a/pkg/demohttp/server.go b/pkg/demohttp/server.go new file mode 100644 index 0000000..c0f6dd2 --- /dev/null +++ b/pkg/demohttp/server.go @@ -0,0 +1,44 @@ +package demohttp + +import ( + "context" + "net/http" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/http/opts" + + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/config" +) + +type DemoHTTPServer struct { + ctx context.Context + cfg *config.DemoConfig +} + +func NewDemoHTTPServer(ctx context.Context, cfg *config.DemoConfig) *DemoHTTPServer { + return &DemoHTTPServer{ + ctx: ctx, + cfg: cfg, + } +} + +func (dh *DemoHTTPServer) GetHandleFuncs() []opts.HTTPFunc { + // TODO: Implement real http handle funcs + return []opts.HTTPFunc{ + { + Path: "/test", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) + w.Write([]byte("unimplemented demo app")) + }, + }, + } +} + +func (dh *DemoHTTPServer) GetHealthCheckFuncs() []opts.HealthCheckFunc { + return []opts.HealthCheckFunc{ + func(ctx context.Context) error { + // TODO: Implement real health checks + return nil + }, + } +} diff --git a/proto/demo/app/v1alpha1/app.proto b/proto/demo/app/v1alpha1/app.proto new file mode 100644 index 0000000..2301aa3 --- /dev/null +++ b/proto/demo/app/v1alpha1/app.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; +package demo.app.v1alpha1; + +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/v1alpha1/demo"; + +// Options for random fact, in this case +// just a language +message GetDemoRequest { + optional string language = 1; +} + +// Returns a randome fact, because this is a demo app +// so what else do we do? +message GetDemoResponse { + google.protobuf.Timestamp timestamp = 1; + string fact = 2; + string source = 3; + string language = 4; +} + +service DemoAppService { + rpc GetDemo(GetDemoRequest) returns (GetDemoResponse) { + option (google.api.http) = {get: "/v1alpha1/demo"}; // grpc-gateway endpoint + } +} diff --git a/proto/google/README.md b/proto/google/README.md new file mode 100644 index 0000000..3d5a283 --- /dev/null +++ b/proto/google/README.md @@ -0,0 +1,22 @@ +# Google API Proto + +These are required for grpc-gateway. Ideally these are either +added to a git submodule, retrieved from a Makefile, downloaded by +buf, or retrieved by a CI job. + +**These are not guaranteed to be up to date** + +## Repo and Files + +These come from the following repo, and are required for +grpc gateway [per their documentation]() + +* Repo: [googleapis git repo](https://github.com/googleapis/googleapis) +* Docs: [grpc-gateay docs](https://github.com/grpc-ecosystem/grpc-gateway?tab=readme-ov-file#2-with-custom-annotations) + +### Files + +- [annotations.proto](https://raw.githubusercontent.com/googleapis/googleapis/refs/heads/master/google/api/annotations.proto) +- [field_behavior.proto](https://github.com/googleapis/googleapis/raw/refs/heads/master/google/api/field_behavior.proto) +- [http.proto](https://github.com/googleapis/googleapis/raw/refs/heads/master/google/api/http.proto) +- [httpbody.proto](https://github.com/googleapis/googleapis/raw/refs/heads/master/google/api/httpbody.proto) diff --git a/proto/google/api/annotations.proto b/proto/google/api/annotations.proto new file mode 100644 index 0000000..417edd8 --- /dev/null +++ b/proto/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/proto/google/api/field_behavior.proto b/proto/google/api/field_behavior.proto new file mode 100644 index 0000000..1fdaaed --- /dev/null +++ b/proto/google/api/field_behavior.proto @@ -0,0 +1,104 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "FieldBehaviorProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.FieldOptions { + // A designation of a specific field behavior (required, output only, etc.) + // in protobuf messages. + // + // Examples: + // + // string name = 1 [(google.api.field_behavior) = REQUIRED]; + // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + // google.protobuf.Duration ttl = 1 + // [(google.api.field_behavior) = INPUT_ONLY]; + // google.protobuf.Timestamp expire_time = 1 + // [(google.api.field_behavior) = OUTPUT_ONLY, + // (google.api.field_behavior) = IMMUTABLE]; + repeated google.api.FieldBehavior field_behavior = 1052 [packed = false]; +} + +// An indicator of the behavior of a given field (for example, that a field +// is required in requests, or given as output but ignored as input). +// This **does not** change the behavior in protocol buffers itself; it only +// denotes the behavior and may affect how API tooling handles the field. +// +// Note: This enum **may** receive new values in the future. +enum FieldBehavior { + // Conventional default for enums. Do not use this. + FIELD_BEHAVIOR_UNSPECIFIED = 0; + + // Specifically denotes a field as optional. + // While all fields in protocol buffers are optional, this may be specified + // for emphasis if appropriate. + OPTIONAL = 1; + + // Denotes a field as required. + // This indicates that the field **must** be provided as part of the request, + // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + REQUIRED = 2; + + // Denotes a field as output only. + // This indicates that the field is provided in responses, but including the + // field in a request does nothing (the server *must* ignore it and + // *must not* throw an error as a result of the field's presence). + OUTPUT_ONLY = 3; + + // Denotes a field as input only. + // This indicates that the field is provided in requests, and the + // corresponding field is not included in output. + INPUT_ONLY = 4; + + // Denotes a field as immutable. + // This indicates that the field may be set once in a request to create a + // resource, but may not be changed thereafter. + IMMUTABLE = 5; + + // Denotes that a (repeated) field is an unordered list. + // This indicates that the service may provide the elements of the list + // in any arbitrary order, rather than the order the user originally + // provided. Additionally, the list's order may or may not be stable. + UNORDERED_LIST = 6; + + // Denotes that this field returns a non-empty default value if not set. + // This indicates that if the user provides the empty value in a request, + // a non-empty value will be returned. The user will not be aware of what + // non-empty value to expect. + NON_EMPTY_DEFAULT = 7; + + // Denotes that the field in a resource (a message annotated with + // google.api.resource) is used in the resource name to uniquely identify the + // resource. For AIP-compliant APIs, this should only be applied to the + // `name` field on the resource. + // + // This behavior should not be applied to references to other resources within + // the message. + // + // The identifier field of resources often have different field behavior + // depending on the request it is embedded in (e.g. for Create methods name + // is optional and unused, while for Update methods it is required). Instead + // of method-specific annotations, only `IDENTIFIER` is required. + IDENTIFIER = 8; +} diff --git a/proto/google/api/http.proto b/proto/google/api/http.proto new file mode 100644 index 0000000..57621b5 --- /dev/null +++ b/proto/google/api/http.proto @@ -0,0 +1,370 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// - HTTP: `GET /v1/messages/123456` +// - gRPC: `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// - HTTP: `GET /v1/messages/123456?revision=2&sub.subfield=foo` +// - gRPC: `GetMessage(message_id: "123456" revision: 2 sub: +// SubMessage(subfield: "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// - HTTP: `PATCH /v1/messages/123456 { "text": "Hi!" }` +// - gRPC: `UpdateMessage(message_id: "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// - HTTP: `PATCH /v1/messages/123456 { "text": "Hi!" }` +// - gRPC: `UpdateMessage(message_id: "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// - HTTP: `GET /v1/messages/123456` +// - gRPC: `GetMessage(message_id: "123456")` +// +// - HTTP: `GET /v1/users/me/messages/123456` +// - gRPC: `GetMessage(user_id: "me" message_id: "123456")` +// +// Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They +// are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL +// query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP +// request body, all +// fields are passed via URL path and URL query parameters. +// +// Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// The following example selects a gRPC method and applies an `HttpRule` to it: +// +// http: +// rules: +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax + // details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/proto/google/api/httpbody.proto b/proto/google/api/httpbody.proto new file mode 100644 index 0000000..e3e17c8 --- /dev/null +++ b/proto/google/api/httpbody.proto @@ -0,0 +1,80 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/any.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/httpbody;httpbody"; +option java_multiple_files = true; +option java_outer_classname = "HttpBodyProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Message that represents an arbitrary HTTP body. It should only be used for +// payload formats that can't be represented as JSON, such as raw binary or +// an HTML page. +// +// +// This message can be used both in streaming and non-streaming API methods in +// the request as well as the response. +// +// It can be used as a top-level request field, which is convenient if one +// wants to extract parameters from either the URL or HTTP template into the +// request fields and also want access to the raw HTTP body. +// +// Example: +// +// message GetResourceRequest { +// // A unique request id. +// string request_id = 1; +// +// // The raw HTTP body is bound to this field. +// google.api.HttpBody http_body = 2; +// +// } +// +// service ResourceService { +// rpc GetResource(GetResourceRequest) +// returns (google.api.HttpBody); +// rpc UpdateResource(google.api.HttpBody) +// returns (google.protobuf.Empty); +// +// } +// +// Example with streaming methods: +// +// service CaldavService { +// rpc GetCalendar(stream google.api.HttpBody) +// returns (stream google.api.HttpBody); +// rpc UpdateCalendar(stream google.api.HttpBody) +// returns (stream google.api.HttpBody); +// +// } +// +// Use of this type only changes how the request and response bodies are +// handled, all other features will continue to work unchanged. +message HttpBody { + // The HTTP Content-Type header value specifying the content type of the body. + string content_type = 1; + + // The HTTP request/response body as raw binary. + bytes data = 2; + + // Application specific response metadata. Must be set in the first response + // for streaming APIs. + repeated google.protobuf.Any extensions = 3; +}