go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/protos.go (about) 1 // Copyright 2018 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 lucicfg 16 17 import ( 18 "fmt" 19 "strings" 20 21 "go.starlark.net/starlark" 22 "google.golang.org/protobuf/proto" 23 "google.golang.org/protobuf/reflect/protodesc" 24 "google.golang.org/protobuf/reflect/protoregistry" 25 "google.golang.org/protobuf/types/descriptorpb" 26 27 "go.chromium.org/luci/common/data/stringset" 28 "go.chromium.org/luci/common/logging" 29 luciproto "go.chromium.org/luci/common/proto" 30 "go.chromium.org/luci/starlark/interpreter" 31 "go.chromium.org/luci/starlark/starlarkproto" 32 33 _ "google.golang.org/protobuf/types/known/anypb" 34 _ "google.golang.org/protobuf/types/known/durationpb" 35 _ "google.golang.org/protobuf/types/known/emptypb" 36 _ "google.golang.org/protobuf/types/known/structpb" 37 _ "google.golang.org/protobuf/types/known/timestamppb" 38 _ "google.golang.org/protobuf/types/known/wrapperspb" 39 40 _ "google.golang.org/genproto/googleapis/type/calendarperiod" 41 _ "google.golang.org/genproto/googleapis/type/color" 42 _ "google.golang.org/genproto/googleapis/type/date" 43 _ "google.golang.org/genproto/googleapis/type/dayofweek" 44 _ "google.golang.org/genproto/googleapis/type/expr" 45 _ "google.golang.org/genproto/googleapis/type/fraction" 46 _ "google.golang.org/genproto/googleapis/type/latlng" 47 _ "google.golang.org/genproto/googleapis/type/money" 48 _ "google.golang.org/genproto/googleapis/type/postaladdress" 49 _ "google.golang.org/genproto/googleapis/type/quaternion" 50 _ "google.golang.org/genproto/googleapis/type/timeofday" 51 52 // This covers all of google/api/*.proto 53 _ "google.golang.org/genproto/googleapis/api/annotations" 54 55 // Dependency of some LUCI protos. 56 _ "github.com/envoyproxy/protoc-gen-validate/validate" 57 58 _ "go.chromium.org/luci/buildbucket/proto" 59 _ "go.chromium.org/luci/common/proto/config" 60 _ "go.chromium.org/luci/common/proto/realms" 61 _ "go.chromium.org/luci/cv/api/config/legacy" 62 _ "go.chromium.org/luci/cv/api/config/v2" 63 _ "go.chromium.org/luci/logdog/api/config/svcconfig" 64 _ "go.chromium.org/luci/luci_notify/api/config" 65 _ "go.chromium.org/luci/milo/proto/projectconfig" 66 _ "go.chromium.org/luci/resultdb/proto/v1" 67 _ "go.chromium.org/luci/scheduler/appengine/messages" 68 ) 69 70 // Collection of built-in descriptor sets built from the protobuf registry 71 // embedded into the lucicfg binary. 72 var ( 73 wellKnownDescSet *starlarkproto.DescriptorSet 74 googTypesDescSet *starlarkproto.DescriptorSet 75 annotationsDescSet *starlarkproto.DescriptorSet 76 validateDescSet *starlarkproto.DescriptorSet 77 luciTypesDescSet *starlarkproto.DescriptorSet 78 ) 79 80 // init initializes DescSet global vars. 81 // 82 // Uses the protobuf registry embedded into the binary. It visits imports in 83 // topological order, to make sure all cross-file references are correctly 84 // resolved. We assume there are no circular dependencies (if there are, they'll 85 // be caught by hanging unit tests). 86 func init() { 87 visited := stringset.New(0) 88 89 // Various well-known proto types (see also starlark/internal/descpb.star). 90 wellKnownDescSet = builtinDescriptorSet( 91 visited, "google/protobuf", "google/protobuf/", 92 []string{ 93 "google/protobuf/any.proto", 94 "google/protobuf/descriptor.proto", 95 "google/protobuf/duration.proto", 96 "google/protobuf/empty.proto", 97 "google/protobuf/field_mask.proto", 98 "google/protobuf/struct.proto", 99 "google/protobuf/timestamp.proto", 100 "google/protobuf/wrappers.proto", 101 }) 102 103 // Google API types (see also starlark/internal/descpb.star). 104 googTypesDescSet = builtinDescriptorSet( 105 visited, "google/type", "google/type/", 106 []string{ 107 "google/type/calendar_period.proto", 108 "google/type/color.proto", 109 "google/type/date.proto", 110 "google/type/dayofweek.proto", 111 "google/type/expr.proto", 112 "google/type/fraction.proto", 113 "google/type/latlng.proto", 114 "google/type/money.proto", 115 "google/type/postal_address.proto", 116 "google/type/quaternion.proto", 117 "google/type/timeofday.proto", 118 }, wellKnownDescSet) 119 120 // Google API annotations (see also starlark/internal/descpb.star). 121 annotationsDescSet = builtinDescriptorSet( 122 visited, "google/api", "google/api/", 123 []string{ 124 "google/api/annotations.proto", 125 "google/api/client.proto", 126 "google/api/field_behavior.proto", 127 "google/api/http.proto", 128 "google/api/resource.proto", 129 }, wellKnownDescSet) 130 131 // github.com/envoyproxy/protoc-gen-validate protos, since they are needed by 132 // some LUCI protos (see also starlark/internal/descpb.star). 133 validateDescSet = builtinDescriptorSet( 134 visited, "protoc-gen-validate", "validate/", 135 []string{ 136 "validate/validate.proto", 137 }, wellKnownDescSet) 138 139 // LUCI protos used by stdlib (see also starlark/internal/luci/descpb.star). 140 luciTypesDescSet = builtinDescriptorSet( 141 visited, "lucicfg/stdlib", "go.chromium.org/luci/", 142 []string{ 143 "go.chromium.org/luci/buildbucket/proto/common.proto", 144 "go.chromium.org/luci/buildbucket/proto/project_config.proto", 145 "go.chromium.org/luci/common/proto/config/project_config.proto", 146 "go.chromium.org/luci/common/proto/realms/realms_config.proto", 147 "go.chromium.org/luci/cv/api/config/legacy/tricium.proto", 148 "go.chromium.org/luci/cv/api/config/v2/config.proto", 149 "go.chromium.org/luci/logdog/api/config/svcconfig/project.proto", 150 "go.chromium.org/luci/luci_notify/api/config/notify.proto", 151 "go.chromium.org/luci/milo/proto/projectconfig/project.proto", 152 "go.chromium.org/luci/resultdb/proto/v1/invocation.proto", 153 "go.chromium.org/luci/resultdb/proto/v1/predicate.proto", 154 "go.chromium.org/luci/scheduler/appengine/messages/config.proto", 155 }, wellKnownDescSet, googTypesDescSet, annotationsDescSet, validateDescSet) 156 } 157 158 // builtinDescriptorSet assembles a *DescriptorSet from descriptors embedded 159 // into the binary in the protobuf registry. 160 // 161 // Visits 'files' and all their dependencies (not already visited per 'visited' 162 // set), adding them in topological order to the new DescriptorSet, updating 163 // 'visited' along the way. 164 // 165 // 'name' and 'deps' are passed verbatim to NewDescriptorSet(...). 166 // 167 // For each proto file added to the new descriptor set verifies its filename 168 // starts with the given 'prefix' to detect if we accidentally pick up 169 // dependencies that should logically belong to a different descriptor set. 170 // 171 // Panics on errors. Built-in descriptors can't be invalid. 172 func builtinDescriptorSet(visited stringset.Set, name, prefix string, files []string, deps ...*starlarkproto.DescriptorSet) *starlarkproto.DescriptorSet { 173 var descs []*descriptorpb.FileDescriptorProto 174 for _, f := range files { 175 var err error 176 if descs, err = visitRegistry(descs, f, visited); err != nil { 177 panic(fmt.Errorf("%s: %s", f, err)) 178 } 179 } 180 181 var misplacedFiles []string 182 for _, desc := range descs { 183 if !strings.HasPrefix(desc.GetName(), prefix) { 184 misplacedFiles = append(misplacedFiles, desc.GetName()) 185 } 186 } 187 if len(misplacedFiles) > 0 { 188 panic(fmt.Errorf("%s: proto dependencies are not under %q: %v", name, prefix, misplacedFiles)) 189 } 190 191 ds, err := starlarkproto.NewDescriptorSet(name, descs, deps) 192 if err != nil { 193 panic(err) 194 } 195 return ds 196 } 197 198 // visitRegistry visits dependencies of 'path', and then 'path' itself. 199 // 200 // Appends discovered file descriptors to fds and returns it. 201 func visitRegistry(fds []*descriptorpb.FileDescriptorProto, path string, visited stringset.Set) ([]*descriptorpb.FileDescriptorProto, error) { 202 if !visited.Add(path) { 203 return fds, nil // visited it already 204 } 205 fd, err := protoregistry.GlobalFiles.FindFileByPath(path) 206 if err != nil { 207 return fds, err 208 } 209 fdp := protodesc.ToFileDescriptorProto(fd) 210 for _, d := range fdp.GetDependency() { 211 if fds, err = visitRegistry(fds, d, visited); err != nil { 212 return fds, fmt.Errorf("%s: %s", d, err) 213 } 214 } 215 return append(fds, fdp), nil 216 } 217 218 // protoMessageDoc returns the message name and a link to its schema doc. 219 // 220 // Extracts it from `option (lucicfg.file_metadata) = {...}` embedded 221 // into the file descriptor proto. 222 // 223 // If there's no documentation, returns two empty strings. 224 func protoMessageDoc(msg *starlarkproto.Message) (name, doc string) { 225 fd := msg.MessageType().Descriptor().ParentFile() 226 if fd == nil { 227 return "", "" 228 } 229 opts := fd.Options().(*descriptorpb.FileOptions) 230 if opts != nil && proto.HasExtension(opts, luciproto.E_FileMetadata) { 231 meta := proto.GetExtension(opts, luciproto.E_FileMetadata).(*luciproto.Metadata) 232 if meta.GetDocUrl() != "" { 233 return string(msg.MessageType().Descriptor().Name()), meta.GetDocUrl() 234 } 235 } 236 return "", "" // not a public proto 237 } 238 239 // protoCache holds a frozen copy of deserialized proto messages. 240 // 241 // Implements starlarkproto.MessageCache. 242 type protoCache struct { 243 interner stringInterner 244 cache map[protoCacheKey]*starlarkproto.Message 245 total int64 246 warned bool 247 } 248 249 type protoCacheKey struct { 250 cache string 251 body string 252 typ *starlarkproto.MessageType 253 } 254 255 func newProtoCache(interner stringInterner) protoCache { 256 return protoCache{ 257 interner: interner, 258 cache: map[protoCacheKey]*starlarkproto.Message{}, 259 } 260 } 261 262 // Fetch returns a previously stored message or (nil, nil) if missing. 263 func (pc *protoCache) Fetch(th *starlark.Thread, cache, body string, typ *starlarkproto.MessageType) (*starlarkproto.Message, error) { 264 return pc.cache[protoCacheKey{cache: cache, body: body, typ: typ}], nil 265 } 266 267 // Store stores a deserialized message. 268 func (pc *protoCache) Store(th *starlark.Thread, cache, body string, msg *starlarkproto.Message) error { 269 if !msg.IsFrozen() { 270 panic("can store only frozen messages") 271 } 272 273 key := protoCacheKey{ 274 cache: cache, 275 body: pc.interner.internString(body), 276 typ: msg.MessageType(), 277 } 278 if _, ok := pc.cache[key]; ok { 279 return nil 280 } 281 282 pc.cache[key] = msg 283 284 pc.total += int64(len(body)) 285 if pc.total > 500*1000*1000 && !pc.warned { 286 logging.Warningf(interpreter.Context(th), 287 "lucicfg internals: proto cache is too large, see https://crbug.com/1382916 if this causes issues like OOMs") 288 pc.warned = true 289 } 290 291 return nil 292 }