go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/grpc/discovery/discovery.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package discovery implements RPC service introspection. 16 package discovery 17 18 import ( 19 "context" 20 "crypto/sha256" 21 "fmt" 22 "reflect" 23 "sync" 24 25 "google.golang.org/grpc/codes" 26 "google.golang.org/grpc/status" 27 "google.golang.org/protobuf/proto" 28 "google.golang.org/protobuf/types/descriptorpb" 29 30 "go.chromium.org/luci/grpc/prpc" 31 ) 32 33 // New creates a discovery server for all given services. 34 // 35 // Service names have form "<pkg>.<service>", where "<pkg>" is name of the proto 36 // package and "<service>" is name of the service in the proto. 37 // 38 // The service descriptions must be registered already using 39 // RegisterDescriptorSetCompressed which is called by init() function 40 // generated by go.chromium.org/luci/grpc/cmd/cproto. 41 func New(serviceNames ...string) DiscoveryServer { 42 return &service{exposed: func() []string { return serviceNames }} 43 } 44 45 // Enable registers a discovery service in the server. 46 // 47 // It makes all services registered in the server (now or later), including the 48 // discovery service itself, discoverable. 49 func Enable(server *prpc.Server) { 50 RegisterDiscoveryServer(server, &service{exposed: server.ServiceNames}) 51 } 52 53 type service struct { 54 exposed func() []string // a dynamic list of services to expose 55 56 m sync.Mutex 57 services []string // services exposed in last Describe 58 description *descriptorpb.FileDescriptorSet // their combined descriptor set 59 } 60 61 func (s *service) Describe(ctx context.Context, _ *Void) (*DescribeResponse, error) { 62 services := s.exposed() 63 64 s.m.Lock() 65 defer s.m.Unlock() 66 67 if !reflect.DeepEqual(services, s.services) { 68 desc, err := combineDescriptors(services) 69 if err != nil { 70 return nil, status.Error(codes.Internal, err.Error()) 71 } 72 s.description = desc 73 s.services = append([]string(nil), services...) 74 } 75 76 return &DescribeResponse{ 77 Description: s.description, 78 Services: s.services, 79 }, nil 80 } 81 82 // combineDescriptors creates one FileDescriptorSet that covers all services 83 // and their dependencies. 84 func combineDescriptors(serviceNames []string) (*descriptorpb.FileDescriptorSet, error) { 85 result := &descriptorpb.FileDescriptorSet{} 86 // seenFiles is a set of descriptor files keyed by SHA256 of their contents. 87 seenFiles := map[[sha256.Size]byte]bool{} 88 89 for _, s := range serviceNames { 90 desc, err := GetDescriptorSet(s) 91 if err != nil { 92 return nil, fmt.Errorf("service %s: %s", s, err) 93 } 94 if desc == nil { 95 return nil, fmt.Errorf( 96 "descriptor for service %q is not found. "+ 97 "Did you compile it with go.chromium.org/luci/grpc/cmd/cproto?", 98 s) 99 } 100 for _, f := range desc.GetFile() { 101 binary, err := proto.Marshal(f) 102 if err != nil { 103 return nil, fmt.Errorf("could not marshal description of %s", f.GetName()) 104 } 105 if hash := sha256.Sum256(binary); !seenFiles[hash] { 106 result.File = append(result.File, f) 107 seenFiles[hash] = true 108 } 109 } 110 } 111 return result, nil 112 }