Skip to content

Commit 1d1276e

Browse files
authored
Add buf-plugin-method-options plugin (#1)
Add a plugin that checks that RPC methods define a set of options that are currently required for Qdrant Cloud APIs. At this moment, the list of required options is: - `google.api.http` - `qdrant.cloud.common.v1.permissions`
1 parent 28af947 commit 1d1276e

File tree

16 files changed

+537
-0
lines changed

16 files changed

+537
-0
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# This team will own the entire repository
2+
* @qdrant/cloud-unit-platform

.github/dependabot.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "gomod"
4+
directory: "/"
5+
schedule:
6+
interval: "weekly"
7+
8+
- package-ecosystem: "docker"
9+
directory: "/"
10+
schedule:
11+
interval: "weekly"
12+
13+
- package-ecosystem: "github-actions"
14+
directory: "/"
15+
schedule:
16+
interval: "weekly"

.github/release-drafter.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
categories:
2+
- title: '🚀 Features'
3+
labels:
4+
- 'feature'
5+
- 'feat'
6+
- 'enhancement'
7+
- 'enh'
8+
- title: '🐛 Bug Fixes'
9+
labels:
10+
- 'fix'
11+
- 'bugfix'
12+
- 'bug'
13+
- title: '🧰 Maintenance'
14+
labels:
15+
- 'chore'
16+
- 'dependencies'
17+
template: |
18+
# What’s Changed
19+
20+
$CHANGES
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Enforce Labels
2+
3+
on:
4+
pull_request:
5+
types: [opened, edited, labeled, unlabeled, synchronize]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
enforce-label:
12+
permissions:
13+
contents: read # for TimonVS/pr-labeler-action to read config file
14+
pull-requests: write # for TimonVS/pr-labeler-action to add labels in PR
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: TimonVS/pr-labeler-action@v5
18+
with:
19+
repo-token: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/pr-workflow.yaml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: PR Workflow
2+
on:
3+
pull_request:
4+
types: [synchronize, opened, reopened]
5+
branches: ["main"]
6+
env:
7+
GOPRIVATE: github.com/qdrant/qdrant-cloud-public-api
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
linter:
14+
name: Linter
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 10 # Sets a timeout of 10 minutes for this job (default is 1 minute)
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
with:
21+
persist-credentials: false
22+
23+
- name: Setup access for private go modules
24+
run: |
25+
git config --global url.'https://${{ secrets.GH_REPO_READ_TOKEN }}@github.com'.insteadOf 'https://github.com'
26+
27+
- uses: actions/setup-go@v5
28+
with:
29+
go-version: "^1.24"
30+
cache: false
31+
32+
- name: Check Go Formatting
33+
run: |
34+
files=$(gofmt -l .) && echo $files && [ -z "$files" ]
35+
36+
- name: Golang CI Lint
37+
uses: golangci/golangci-lint-action@v7
38+
with:
39+
version: v2.0.1
40+
args: --timeout 10m
41+
42+
unit-tests:
43+
name: Unit Tests
44+
runs-on: ubuntu-latest
45+
steps:
46+
- name: Checkout
47+
uses: actions/checkout@v4
48+
with:
49+
persist-credentials: false
50+
51+
- name: Setup access for private go modules
52+
run: |
53+
git config --global url.'https://${{ secrets.GH_REPO_READ_TOKEN }}@github.com'.insteadOf 'https://github.com'
54+
55+
- uses: actions/setup-go@v5
56+
with:
57+
go-version: "^1.24"
58+
59+
- name: Unit tests
60+
run: |
61+
make test_unit

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,9 @@ go.work.sum
2323

2424
# env file
2525
.env
26+
27+
.DS_Store
28+
.idea
29+
*.log
30+
tmp/
31+
bin/

Makefile

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
.PHONY: help
2+
help: ## Display this help.
3+
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9\/-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
4+
5+
##@ Development
6+
7+
.PHONY: fmt
8+
fmt: ## Run go fmt and gci against code.
9+
go fmt ./...
10+
$(GCI) write ./ --skip-generated -s standard -s default -s 'prefix(github.com/qdrant)' -s 'prefix(github.com/qdrant/qdrant-cloud-buf-plugins/)'
11+
12+
.PHONY: vet
13+
vet: ## Run go vet (static analysis tool) against code.
14+
go vet ./...
15+
16+
.PHONY: lint
17+
lint: bootstrap ## Run project linters
18+
$(GOLANGCI_LINT) run
19+
20+
.PHONY: test
21+
test: fmt vet test_unit ## Run all tests.
22+
23+
.PHONY: test_unit
24+
test_unit: ## Run unit tests.
25+
echo "Running Go unit tests..."
26+
go test ./... -coverprofile cover.out -v
27+
28+
##@ Dependencies
29+
30+
## Location to install dependencies to
31+
LOCALBIN ?= $(shell pwd)/bin
32+
$(LOCALBIN):
33+
mkdir -p $(LOCALBIN)
34+
35+
## Tool Binaries
36+
GO ?= go
37+
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
38+
GCI = $(LOCALBIN)/gci
39+
40+
## Tool Versions
41+
GOLANGCI_LINT_VERSION ?= v2.0.1
42+
GCI_VERSION ?= v0.13.5
43+
44+
.PHONY: bootstrap
45+
bootstrap: install/gci install/golangci-lint ## Install required dependencies to work with this project
46+
47+
.PHONY: golangci-lint
48+
install/golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
49+
$(GOLANGCI_LINT): $(LOCALBIN)
50+
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
51+
52+
.PHONY: install/gci
53+
install/gci: $(GCI) ## Download gci locally if necessary.
54+
$(GCI): $(LOCALBIN)
55+
$(call go-install-tool,$(GCI),github.com/daixiang0/gci,$(GCI_VERSION))
56+
57+
# copied from kube-builder
58+
# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
59+
# $1 - target path with name of binary
60+
# $2 - package url which can be installed
61+
# $3 - specific version of package
62+
define go-install-tool
63+
@[ -f "$(1)-$(3)" ] || { \
64+
set -e; \
65+
package=$(2)@$(3) ;\
66+
echo "Downloading $${package}" ;\
67+
rm -f $(1) || true ;\
68+
GOBIN=$(LOCALBIN) go install $${package} ;\
69+
mv $(1) $(1)-$(3) ;\
70+
} ;\
71+
ln -sf $(1)-$(3) $(1)
72+
endef

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# qdrant-cloud-buf-plugins
2+
3+
Collection of [Buf plugins](https://buf.build/docs/cli/buf-plugins/overview/) used by Qdrant Cloud APIs.
4+
5+
## Development
6+
7+
This project leverages Make to automate common development tasks. To view all available commands, run:
8+
9+
``` sh
10+
make help
11+
```
12+
13+
### Setup
14+
15+
To work with this project locally, you need to have [Go](https://go.dev/doc/install) installed.
16+
Additionally, there are other required dependencies that you can install running:
17+
18+
``` sh
19+
make bootstrap
20+
```
21+
22+
### Running tests
23+
24+
To run the tests, execute:
25+
26+
``` sh
27+
make test
28+
```
29+
30+
### Formatting & linting code
31+
32+
To format and lint the code of the project, execute:
33+
34+
``` sh
35+
make fmt
36+
make lint
37+
```
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Package main implements a plugin that checks that all rpc methods set the
2+
// required options (permissions, http).
3+
//
4+
// To use this plugin:
5+
//
6+
// # buf.yaml
7+
// version: v2
8+
// lint:
9+
// use:
10+
// - STANDARD # omit if you do not want to use the rules builtin to buf
11+
// - QDRANT_CLOUD_METHOD_OPTIONS
12+
// plugins:
13+
// - plugin: buf-plugin-method-options
14+
package main
15+
16+
import (
17+
"context"
18+
19+
"buf.build/go/bufplugin/check"
20+
"buf.build/go/bufplugin/check/checkutil"
21+
"buf.build/go/bufplugin/info"
22+
googleann "google.golang.org/genproto/googleapis/api/annotations"
23+
"google.golang.org/protobuf/proto"
24+
"google.golang.org/protobuf/reflect/protoreflect"
25+
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
26+
27+
commonv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/common/v1"
28+
)
29+
30+
const (
31+
methodOptionsRuleID = "QDRANT_CLOUD_METHOD_OPTIONS"
32+
)
33+
34+
var (
35+
methodOptionsRuleSpec = &check.RuleSpec{
36+
ID: methodOptionsRuleID,
37+
Default: true,
38+
Purpose: `Checks that all rpc methods define a set of required options.`,
39+
Type: check.RuleTypeLint,
40+
Handler: checkutil.NewMethodRuleHandler(checkMethodOptions, checkutil.WithoutImports()),
41+
}
42+
spec = &check.Spec{
43+
Rules: []*check.RuleSpec{
44+
methodOptionsRuleSpec,
45+
},
46+
Info: &info.Spec{
47+
Documentation: `A plugin that checks that all rpc methods define a set of required options.`,
48+
SPDXLicenseID: "",
49+
LicenseURL: "",
50+
},
51+
}
52+
requiredMethodOptionExtensions = []*protoimpl.ExtensionInfo{
53+
commonv1.E_Permissions,
54+
googleann.E_Http,
55+
}
56+
)
57+
58+
func main() {
59+
check.Main(spec)
60+
}
61+
62+
func checkMethodOptions(ctx context.Context, responseWriter check.ResponseWriter, request check.Request, methodDescriptor protoreflect.MethodDescriptor) error {
63+
options := methodDescriptor.Options()
64+
65+
for _, extension := range requiredMethodOptionExtensions {
66+
if !proto.HasExtension(options, extension) {
67+
responseWriter.AddAnnotation(
68+
check.WithMessagef("Method %q does not define the %q option", methodDescriptor.FullName(), extension.TypeDescriptor().FullName()),
69+
check.WithDescriptor(methodDescriptor),
70+
)
71+
}
72+
}
73+
74+
return nil
75+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"buf.build/go/bufplugin/check/checktest"
7+
)
8+
9+
func TestSpec(t *testing.T) {
10+
t.Parallel()
11+
checktest.SpecTest(t, spec)
12+
}
13+
14+
func TestSimpleSuccess(t *testing.T) {
15+
t.Parallel()
16+
17+
checktest.CheckTest{
18+
Request: &checktest.RequestSpec{
19+
Files: &checktest.ProtoFileSpec{
20+
DirPaths: []string{"testdata/simple_success"},
21+
FilePaths: []string{"simple.proto"},
22+
},
23+
},
24+
Spec: spec,
25+
}.Run(t)
26+
}
27+
28+
func TestSimpleFailure(t *testing.T) {
29+
t.Parallel()
30+
31+
checktest.CheckTest{
32+
Request: &checktest.RequestSpec{
33+
Files: &checktest.ProtoFileSpec{
34+
DirPaths: []string{"testdata/simple_failure"},
35+
FilePaths: []string{"simple.proto"},
36+
},
37+
},
38+
Spec: spec,
39+
ExpectedAnnotations: []checktest.ExpectedAnnotation{
40+
{
41+
RuleID: methodOptionsRuleID,
42+
Message: "Method \"simple.GreeterService.HelloWorld\" does not define the \"google.api.http\" option",
43+
FileLocation: &checktest.ExpectedFileLocation{
44+
FileName: "simple.proto",
45+
StartLine: 9,
46+
StartColumn: 4,
47+
EndLine: 12,
48+
EndColumn: 5,
49+
},
50+
},
51+
{
52+
RuleID: methodOptionsRuleID,
53+
Message: "Method \"simple.GreeterService.HelloWorld\" does not define the \"qdrant.cloud.common.v1.permissions\" option",
54+
FileLocation: &checktest.ExpectedFileLocation{
55+
FileName: "simple.proto",
56+
StartLine: 9,
57+
StartColumn: 4,
58+
EndLine: 12,
59+
EndColumn: 5,
60+
},
61+
},
62+
},
63+
}.Run(t)
64+
}

0 commit comments

Comments
 (0)