diff --git a/server/storage/wal/version.go b/server/storage/wal/version.go index 8fe6725e8cd..e4c3c250057 100644 --- a/server/storage/wal/version.go +++ b/server/storage/wal/version.go @@ -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++ { diff --git a/tools/proto-annotations/cmd/etcd_version.go b/tools/proto-annotations/cmd/etcd_version.go new file mode 100644 index 00000000000..9d6f8658a40 --- /dev/null +++ b/tools/proto-annotations/cmd/etcd_version.go @@ -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 +} diff --git a/tools/proto-annotations/cmd/root.go b/tools/proto-annotations/cmd/root.go new file mode 100644 index 00000000000..2b4ff9530a0 --- /dev/null +++ b/tools/proto-annotations/cmd/root.go @@ -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: +: "" + + +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 +} diff --git a/tools/proto-annotations/main.go b/tools/proto-annotations/main.go index 105f829fb2d..a350ef82b92 100644 --- a/tools/proto-annotations/main.go +++ b/tools/proto-annotations/main.go @@ -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") + if err := cmd.RootCmd().Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) os.Exit(1) - default: - fmt.Fprintf(os.Stderr, "Unknown annotation %q. Options: etcd_version", *annotation) - 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 }