tools: Improve proto annotation documentation
This commit is contained in:
@ -66,6 +66,9 @@ func MinimalEtcdVersion(ents []raftpb.Entry) *semver.Version {
|
||||
|
||||
type Visitor func(path protoreflect.FullName, ver *semver.Version) error
|
||||
|
||||
// VisitFileDescriptor calls visitor on each field and enum value with etcd version read from proto definition.
|
||||
// If field/enum value is not annotated, visitor will be called with nil.
|
||||
// Upon encountering invalid annotation, will immediately exit with error.
|
||||
func VisitFileDescriptor(file protoreflect.FileDescriptor, visitor Visitor) error {
|
||||
msgs := file.Messages()
|
||||
for i := 0; i < msgs.Len(); i++ {
|
||||
|
117
tools/proto-annotations/cmd/etcd_version.go
Normal file
117
tools/proto-annotations/cmd/etcd_version.go
Normal file
@ -0,0 +1,117 @@
|
||||
// Copyright 2021 The etcd Authors
|
||||
//
|
||||
// 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.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
"go.etcd.io/etcd/server/v3/storage/wal"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/reflect/protoregistry"
|
||||
)
|
||||
|
||||
// printEtcdVersion writes etcd_version proto annotation to stdout and returns any errors encountered when reading annotation.
|
||||
func printEtcdVersion() []error {
|
||||
var errs []error
|
||||
annotations, err := allEtcdVersionAnnotations()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
return errs
|
||||
}
|
||||
sort.Slice(annotations, func(i, j int) bool {
|
||||
return annotations[i].fullName < annotations[j].fullName
|
||||
})
|
||||
output := &bytes.Buffer{}
|
||||
for _, a := range annotations {
|
||||
newErrs := a.Validate()
|
||||
if len(newErrs) == 0 {
|
||||
err := a.PrintLine(output)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
return errs
|
||||
}
|
||||
}
|
||||
errs = append(errs, newErrs...)
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
fmt.Print(output)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func allEtcdVersionAnnotations() (annotations []etcdVersionAnnotation, err error) {
|
||||
var fileAnnotations []etcdVersionAnnotation
|
||||
protoregistry.GlobalFiles.RangeFiles(func(file protoreflect.FileDescriptor) bool {
|
||||
switch string(file.Package()) {
|
||||
// Skip external packages that are not expected to have etcd version annotation.
|
||||
case "io.prometheus.client", "grpc.binarylog.v1", "google.protobuf", "google.rpc", "google.api":
|
||||
return true
|
||||
}
|
||||
fileAnnotations, err = fileEtcdVersionAnnotations(file)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
annotations = append(annotations, fileAnnotations...)
|
||||
return true
|
||||
})
|
||||
return annotations, err
|
||||
}
|
||||
|
||||
func fileEtcdVersionAnnotations(file protoreflect.FileDescriptor) (annotations []etcdVersionAnnotation, err error) {
|
||||
err = wal.VisitFileDescriptor(file, func(path protoreflect.FullName, ver *semver.Version) error {
|
||||
a := etcdVersionAnnotation{fullName: path, version: ver}
|
||||
annotations = append(annotations, a)
|
||||
return nil
|
||||
})
|
||||
return annotations, err
|
||||
}
|
||||
|
||||
type etcdVersionAnnotation struct {
|
||||
fullName protoreflect.FullName
|
||||
version *semver.Version
|
||||
}
|
||||
|
||||
func (a etcdVersionAnnotation) Validate() (errs []error) {
|
||||
if a.version == nil {
|
||||
return nil
|
||||
}
|
||||
if a.version.Major == 0 {
|
||||
errs = append(errs, fmt.Errorf("%s: etcd_version major version should not be zero", a.fullName))
|
||||
}
|
||||
if a.version.Patch != 0 {
|
||||
errs = append(errs, fmt.Errorf("%s: etcd_version patch version should be zero", a.fullName))
|
||||
}
|
||||
if a.version.PreRelease != "" {
|
||||
errs = append(errs, fmt.Errorf("%s: etcd_version should not be prerelease", a.fullName))
|
||||
}
|
||||
if a.version.Metadata != "" {
|
||||
errs = append(errs, fmt.Errorf("%s: etcd_version should not have metadata", a.fullName))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (a etcdVersionAnnotation) PrintLine(out io.Writer) error {
|
||||
if a.version == nil {
|
||||
_, err := fmt.Fprintf(out, "%s: \"\"\n", a.fullName)
|
||||
return err
|
||||
}
|
||||
_, err := fmt.Fprintf(out, "%s: \"%d.%d\"\n", a.fullName, a.version.Major, a.version.Minor)
|
||||
return err
|
||||
}
|
76
tools/proto-annotations/cmd/root.go
Normal file
76
tools/proto-annotations/cmd/root.go
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright 2021 The etcd Authors
|
||||
//
|
||||
// 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.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
EtcdVersionAnnotation = "etcd_version"
|
||||
)
|
||||
|
||||
func RootCmd() *cobra.Command {
|
||||
var annotation string
|
||||
cmd := &cobra.Command{
|
||||
Use: "proto-annotation",
|
||||
Short: "Proto-annotations prints a dump of annotations used by all protobuf definitions used by Etcd.",
|
||||
Long: `Tool used to extract values of a specific proto annotation used by protobuf definitions used by Etcd.
|
||||
Created to ensure that all newly introduced proto definitions have a etcd_version_* annotation, by analysing diffs between generated by this tool.
|
||||
|
||||
Proto annotations is printed to stdout in format:
|
||||
<Field full name>: "<etcd_version>"
|
||||
|
||||
|
||||
For example:
|
||||
'''
|
||||
etcdserverpb.Member: "3.0"
|
||||
etcdserverpb.Member.ID: ""
|
||||
etcdserverpb.Member.clientURLs: ""
|
||||
etcdserverpb.Member.isLearner: "3.4"
|
||||
etcdserverpb.Member.name: ""
|
||||
etcdserverpb.Member.peerURLs: ""
|
||||
'''
|
||||
|
||||
Any errors in proto will be printed to stderr.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runProtoAnnotation(annotation)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&annotation, "annotation", "", "Specify what proto annotation to read. Options: etcd_version")
|
||||
cmd.MarkFlagRequired("annotation")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runProtoAnnotation(annotation string) error {
|
||||
var errs []error
|
||||
switch annotation {
|
||||
case EtcdVersionAnnotation:
|
||||
errs = printEtcdVersion()
|
||||
default:
|
||||
return fmt.Errorf("unknown annotation %q. Options: %q", annotation, EtcdVersionAnnotation)
|
||||
}
|
||||
if len(errs) != 0 {
|
||||
for _, err := range errs {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
return fmt.Errorf("failed reading anotation")
|
||||
}
|
||||
return nil
|
||||
}
|
@ -15,129 +15,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
"go.etcd.io/etcd/server/v3/storage/wal"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/reflect/protoregistry"
|
||||
)
|
||||
|
||||
const (
|
||||
EtcdVersionAnnotation = "etcd_version"
|
||||
"go.etcd.io/etcd/v3/tools/proto-annotations/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
annotation := flag.String("annotation", "", "Specify what proto annotation to read. Options: etcd_version")
|
||||
flag.Parse()
|
||||
var errs []error
|
||||
switch *annotation {
|
||||
case EtcdVersionAnnotation:
|
||||
errs = handleEtcdVersion()
|
||||
case "":
|
||||
fmt.Fprintf(os.Stderr, "Please provide --annotation flag")
|
||||
os.Exit(1)
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown annotation %q. Options: etcd_version", *annotation)
|
||||
if err := cmd.RootCmd().Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(errs) != 0 {
|
||||
for _, err := range errs {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func handleEtcdVersion() (errs []error) {
|
||||
annotations, err := allEtcdVersionAnnotations()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
return errs
|
||||
}
|
||||
sort.Slice(annotations, func(i, j int) bool {
|
||||
return annotations[i].fullName < annotations[j].fullName
|
||||
})
|
||||
output := &bytes.Buffer{}
|
||||
for _, a := range annotations {
|
||||
newErrs := a.Validate()
|
||||
if len(newErrs) == 0 {
|
||||
err := a.PrintLine(output)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
return errs
|
||||
}
|
||||
}
|
||||
errs = append(errs, newErrs...)
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
fmt.Print(output)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func allEtcdVersionAnnotations() (annotations []etcdVersionAnnotation, err error) {
|
||||
var fileAnnotations []etcdVersionAnnotation
|
||||
protoregistry.GlobalFiles.RangeFiles(func(file protoreflect.FileDescriptor) bool {
|
||||
switch string(file.Package()) {
|
||||
// Skip external packages that are not expected to have etcd version annotation.
|
||||
case "io.prometheus.client", "grpc.binarylog.v1", "google.protobuf", "google.rpc", "google.api":
|
||||
return true
|
||||
}
|
||||
fileAnnotations, err = fileEtcdVersionAnnotations(file)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
annotations = append(annotations, fileAnnotations...)
|
||||
return true
|
||||
})
|
||||
return annotations, err
|
||||
}
|
||||
|
||||
func fileEtcdVersionAnnotations(file protoreflect.FileDescriptor) (annotations []etcdVersionAnnotation, err error) {
|
||||
err = wal.VisitFileDescriptor(file, func(path protoreflect.FullName, ver *semver.Version) error {
|
||||
a := etcdVersionAnnotation{fullName: path, version: ver}
|
||||
annotations = append(annotations, a)
|
||||
return nil
|
||||
})
|
||||
return annotations, err
|
||||
}
|
||||
|
||||
type etcdVersionAnnotation struct {
|
||||
fullName protoreflect.FullName
|
||||
version *semver.Version
|
||||
}
|
||||
|
||||
func (a etcdVersionAnnotation) Validate() (errs []error) {
|
||||
if a.version == nil {
|
||||
return nil
|
||||
}
|
||||
if a.version.Major == 0 {
|
||||
errs = append(errs, fmt.Errorf("%s: etcd_version major version should not be zero", a.fullName))
|
||||
}
|
||||
if a.version.Patch != 0 {
|
||||
errs = append(errs, fmt.Errorf("%s: etcd_version patch version should be zero", a.fullName))
|
||||
}
|
||||
if a.version.PreRelease != "" {
|
||||
errs = append(errs, fmt.Errorf("%s: etcd_version should not be prerelease", a.fullName))
|
||||
}
|
||||
if a.version.Metadata != "" {
|
||||
errs = append(errs, fmt.Errorf("%s: etcd_version should not have metadata", a.fullName))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (a etcdVersionAnnotation) PrintLine(out io.Writer) error {
|
||||
if a.version == nil {
|
||||
_, err := fmt.Fprintf(out, "%s: \"\"\n", a.fullName)
|
||||
return err
|
||||
}
|
||||
_, err := fmt.Fprintf(out, "%s: \"%d.%d\"\n", a.fullName, a.version.Major, a.version.Minor)
|
||||
return err
|
||||
}
|
||||
|
Reference in New Issue
Block a user