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  }