emahiro/b.log

日々の勉強の記録とか育児の記録とか。

Go で protoc のカスタムプラグインを書く

Overview

protoc のプラグインを自分で書いてみたのでその備忘録です。

なぜ protoc プラグインを自前で用意したくなったのか

ちょうど業務で RBAC 制御の実装を考えていたときに、過去何度もあたってきた「操作に対する Role、Permission をアタッチするマッピング」の管理方法をいくつか検討していて、これを proto で管理する拡張を作ることを考えました。

ただし、protoc にそういったカスタムフォーマットを parse する仕様はないので、自前で用意した、という経緯です。

カスタムプラグインの作成

今回作成したカスタム protoc プラグインでやってることは以下です。

  • protoc のプラグインとしての Go ツールを自前で用意。
  • 操作に対する permission を定義をするカスタム書式を proto に定義。
  • buf generate 実行時にカスタムプラグインコマンドを実行し、カスタム書式を解析 -> 操作:Permission のマップを作成。

ざっくりとしたディレクトリ構成は以下です。

.
├── cmd/
│   └── customPluginCmd/
│       ├── main.go
│       └── parser.go
├── proto/
│   ├── option.proto
│   └── operation.proto
├── gen/
│   └── rbac_map.go
├── buf.yaml
└── buf.gen.yaml

option.proto に今回の対象となる拡張設定を定義してそれを operation.proto で import して利用します。

option.proto は以下のように設定します。

syntax="proto3";

package proto;

import "google/protobuf/descriptor.proto";

extend google.protobuf.MethodOptions {
  Authz authz = 50001;
}

message Authz {
  repeated string permissions = 1;
}

"google/protobuf/descriptor.proto" は proto の service や message にカスタムメタデータを組み込むライブラリで、今回は MethodOptions を利用してメソッドにメタデータを付与してるので、 rpc $Method を拡張しています。MessageOptions では message (リクエストパラメータなど)にメタデータを付与できます。
またメタデータを付与するときはデフォルトの番号と競合しない拡張番号を付与する必要があります(ものすごい大きい数にとかにするとまぁコンフリクトは発生しないと思います)

このカスタムオプションを利用したい proto の定義で import すれば proto 自体はメタデータを読み込めるようになります。

syntax="proto3";

package proto;

import "proto/option.proto";

service Service {
  rpc Method (...) ... {
    option (proto.authz) = {
        permissions: [...]
    };
  }

このとき options の引数に渡す proto への path ですが (proto は path を A.B.C....のように表現します)、root ディレクトリからの options.proto までの path 定義 + メタデータの field 名になります。
今回のケースでは path が proto/option.protoメタデータの field 名が option.proto 内の Authz (authz) なので、option に渡す proto の定義までの path は proto.authz と表現されます。メタデータが定義されている proto までの path でこの option に渡す引数は変化します。

この状態で buf generate できれば proto の定義自体は通っています。あとは plugin の本体である Go のコマンドを buf generate 時に実行するように buf.gen.yaml に hook する処理を書きます。
今回のケースでは以下のようになります。

version: v2
clean: true
managed:
  enabled: true
  disable:
    - file_option: go_package_prefix
plugins:
  - local: [go, run, ./cmd/customPluginCmd]
    out: server/gen
    opt:
      - paths=source_relative
    strategy: all

これで buf generate したときに customPluginCmd が実行されます。

Plugin の実装自体は今回は割愛しますが、Go のコマンドが実行されるだけなので、proto から operation と permission を取り出して mapping する処理を書けば終わりです。自分はこの部分は LLM に書いてもらいました。

まとめ

  • 無いものは作る。
  • buf は hook 処理を書くのも楽。
  • hook 処理を登録するのも直感的。

ということで簡単に protoc のプラグインを書いて処理を拡張することができました。