go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/pbutil/common.go (about)

     1  // Copyright 2022 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 pbutil contains methods for manipulating LUCI Analysis protos.
    16  package pbutil
    17  
    18  import (
    19  	"encoding/json"
    20  	"fmt"
    21  	"regexp"
    22  	"sort"
    23  	"time"
    24  
    25  	"google.golang.org/protobuf/types/known/timestamppb"
    26  
    27  	"go.chromium.org/luci/common/errors"
    28  	cvv0 "go.chromium.org/luci/cv/api/v0"
    29  	"go.chromium.org/luci/resultdb/pbutil"
    30  
    31  	pb "go.chromium.org/luci/analysis/proto/v1"
    32  )
    33  
    34  // EmptyJSON corresponds to a serialized, empty JSON object.
    35  const EmptyJSON = "{}"
    36  const maxStringPairKeyLength = 64
    37  const maxStringPairValueLength = 256
    38  const stringPairKeyPattern = `[a-z][a-z0-9_]*(/[a-z][a-z0-9_]*)*`
    39  
    40  var stringPairKeyRe = regexp.MustCompile(fmt.Sprintf(`^%s$`, stringPairKeyPattern))
    41  var stringPairRe = regexp.MustCompile(fmt.Sprintf("(?s)^(%s):(.*)$", stringPairKeyPattern))
    42  var variantHashRe = regexp.MustCompile("^[0-9a-f]{16}$")
    43  
    44  // ProjectRePattern is the regular expression pattern that matches
    45  // validly formed LUCI Project names.
    46  // From https://source.chromium.org/chromium/infra/infra/+/main:luci/appengine/components/components/config/common.py?q=PROJECT_ID_PATTERN
    47  const ProjectRePattern = `[a-z0-9\-]{1,40}`
    48  
    49  // projectRe matches validly formed LUCI Project names.
    50  var projectRe = regexp.MustCompile(`^` + ProjectRePattern + `$`)
    51  
    52  // MustTimestampProto converts a time.Time to a *timestamppb.Timestamp and panics
    53  // on failure.
    54  func MustTimestampProto(t time.Time) *timestamppb.Timestamp {
    55  	ts := timestamppb.New(t)
    56  	if err := ts.CheckValid(); err != nil {
    57  		panic(err)
    58  	}
    59  	return ts
    60  }
    61  
    62  // AsTime converts a *timestamppb.Timestamp to a time.Time.
    63  func AsTime(ts *timestamppb.Timestamp) (time.Time, error) {
    64  	if ts == nil {
    65  		return time.Time{}, errors.Reason("unspecified").Err()
    66  	}
    67  	if err := ts.CheckValid(); err != nil {
    68  		return time.Time{}, err
    69  	}
    70  	return ts.AsTime(), nil
    71  }
    72  
    73  func doesNotMatch(r *regexp.Regexp) error {
    74  	return errors.Reason("does not match %s", r).Err()
    75  }
    76  
    77  // StringPair creates a pb.StringPair with the given strings as key/value field values.
    78  func StringPair(k, v string) *pb.StringPair {
    79  	return &pb.StringPair{Key: k, Value: v}
    80  }
    81  
    82  // StringPairs creates a slice of pb.StringPair from a list of strings alternating key/value.
    83  //
    84  // Panics if an odd number of tokens is passed.
    85  func StringPairs(pairs ...string) []*pb.StringPair {
    86  	if len(pairs)%2 != 0 {
    87  		panic(fmt.Sprintf("odd number of tokens in %q", pairs))
    88  	}
    89  
    90  	strpairs := make([]*pb.StringPair, len(pairs)/2)
    91  	for i := range strpairs {
    92  		strpairs[i] = StringPair(pairs[2*i], pairs[2*i+1])
    93  	}
    94  	return strpairs
    95  }
    96  
    97  // StringPairFromString creates a pb.StringPair from the given key:val string.
    98  func StringPairFromString(s string) (*pb.StringPair, error) {
    99  	m := stringPairRe.FindStringSubmatch(s)
   100  	if m == nil {
   101  		return nil, doesNotMatch(stringPairRe)
   102  	}
   103  	return StringPair(m[1], m[3]), nil
   104  }
   105  
   106  // StringPairToString converts a StringPair to a key:val string.
   107  func StringPairToString(pair *pb.StringPair) string {
   108  	return fmt.Sprintf("%s:%s", pair.Key, pair.Value)
   109  }
   110  
   111  // StringPairsToStrings converts pairs to a slice of "{key}:{value}" strings
   112  // in the same order.
   113  func StringPairsToStrings(pairs ...*pb.StringPair) []string {
   114  	ret := make([]string, len(pairs))
   115  	for i, p := range pairs {
   116  		ret[i] = StringPairToString(p)
   117  	}
   118  	return ret
   119  }
   120  
   121  // Variant creates a pb.Variant from a list of strings alternating
   122  // key/value. Does not validate pairs.
   123  // See also VariantFromStrings.
   124  //
   125  // Panics if an odd number of tokens is passed.
   126  func Variant(pairs ...string) *pb.Variant {
   127  	if len(pairs)%2 != 0 {
   128  		panic(fmt.Sprintf("odd number of tokens in %q", pairs))
   129  	}
   130  
   131  	vr := &pb.Variant{Def: make(map[string]string, len(pairs)/2)}
   132  	for i := 0; i < len(pairs); i += 2 {
   133  		vr.Def[pairs[i]] = pairs[i+1]
   134  	}
   135  	return vr
   136  }
   137  
   138  // VariantFromStrings returns a Variant proto given the key:val string slice of its contents.
   139  //
   140  // If a key appears multiple times, the last pair wins.
   141  func VariantFromStrings(pairs []string) (*pb.Variant, error) {
   142  	if len(pairs) == 0 {
   143  		return nil, nil
   144  	}
   145  
   146  	def := make(map[string]string, len(pairs))
   147  	for _, p := range pairs {
   148  		pair, err := StringPairFromString(p)
   149  		if err != nil {
   150  			return nil, errors.Annotate(err, "pair %q", p).Err()
   151  		}
   152  		def[pair.Key] = pair.Value
   153  	}
   154  	return &pb.Variant{Def: def}, nil
   155  }
   156  
   157  // SortedVariantKeys returns the keys in the variant as a sorted slice.
   158  func SortedVariantKeys(vr *pb.Variant) []string {
   159  	keys := make([]string, 0, len(vr.GetDef()))
   160  	for k := range vr.GetDef() {
   161  		keys = append(keys, k)
   162  	}
   163  	sort.Strings(keys)
   164  	return keys
   165  }
   166  
   167  var nonNilEmptyStringSlice = []string{}
   168  
   169  // VariantToStrings returns a key:val string slice representation of the Variant.
   170  // Never returns nil.
   171  func VariantToStrings(vr *pb.Variant) []string {
   172  	if len(vr.GetDef()) == 0 {
   173  		return nonNilEmptyStringSlice
   174  	}
   175  
   176  	keys := SortedVariantKeys(vr)
   177  	pairs := make([]string, len(keys))
   178  	defMap := vr.GetDef()
   179  	for i, k := range keys {
   180  		pairs[i] = (k + ":" + defMap[k])
   181  	}
   182  	return pairs
   183  }
   184  
   185  // VariantToStringPairs returns a slice of StringPair derived from *pb.Variant.
   186  func VariantToStringPairs(vr *pb.Variant) []*pb.StringPair {
   187  	defMap := vr.GetDef()
   188  	if len(defMap) == 0 {
   189  		return nil
   190  	}
   191  
   192  	keys := SortedVariantKeys(vr)
   193  	sp := make([]*pb.StringPair, len(keys))
   194  	for i, k := range keys {
   195  		sp[i] = StringPair(k, defMap[k])
   196  	}
   197  	return sp
   198  }
   199  
   200  // VariantToJSON returns the JSON equivalent for a variant.
   201  // Each key in the variant is mapped to a top-level key in the
   202  // JSON object.
   203  // e.g. `{"builder":"linux-rel","os":"Ubuntu-18.04"}`
   204  func VariantToJSON(variant *pb.Variant) (string, error) {
   205  	if variant == nil {
   206  		// There is no string value we can send to BigQuery that
   207  		// BigQuery will interpret as a NULL value for a JSON column:
   208  		// - "" (empty string) is rejected as invalid JSON.
   209  		// - "null" is interpreted as the JSON value null, not the
   210  		//   absence of a value.
   211  		// Consequently, the next best thing is to return an empty
   212  		// JSON object.
   213  		return EmptyJSON, nil
   214  	}
   215  	m := make(map[string]string)
   216  	for key, value := range variant.Def {
   217  		m[key] = value
   218  	}
   219  	b, err := json.Marshal(m)
   220  	if err != nil {
   221  		return "", err
   222  	}
   223  	return string(b), nil
   224  }
   225  
   226  // VariantFromJSON convert json string representation of the variant into protocol buffer.
   227  func VariantFromJSON(variant string) (*pb.Variant, error) {
   228  	v := map[string]string{}
   229  	if err := json.Unmarshal([]byte(variant), &v); err != nil {
   230  		return nil, err
   231  	}
   232  	return &pb.Variant{Def: v}, nil
   233  }
   234  
   235  // PresubmitRunModeFromString returns a pb.PresubmitRunMode corresponding
   236  // to a CV Run mode string.
   237  func PresubmitRunModeFromString(mode string) (pb.PresubmitRunMode, error) {
   238  	switch mode {
   239  	case "FULL_RUN":
   240  		return pb.PresubmitRunMode_FULL_RUN, nil
   241  	case "DRY_RUN":
   242  		return pb.PresubmitRunMode_DRY_RUN, nil
   243  	case "QUICK_DRY_RUN":
   244  		return pb.PresubmitRunMode_QUICK_DRY_RUN, nil
   245  	case "NEW_PATCHSET_RUN":
   246  		return pb.PresubmitRunMode_NEW_PATCHSET_RUN, nil
   247  	case "CQ_MODE_MEGA_DRY_RUN":
   248  		// Report as DRY_RUN for now.
   249  		return pb.PresubmitRunMode_DRY_RUN, nil
   250  	}
   251  	return pb.PresubmitRunMode_PRESUBMIT_RUN_MODE_UNSPECIFIED, fmt.Errorf("unknown run mode %q", mode)
   252  }
   253  
   254  // PresubmitRunStatusFromLUCICV returns a pb.PresubmitRunStatus corresponding
   255  // to a LUCI CV Run status. Only statuses corresponding to an ended run
   256  // are supported.
   257  func PresubmitRunStatusFromLUCICV(status cvv0.Run_Status) (pb.PresubmitRunStatus, error) {
   258  	switch status {
   259  	case cvv0.Run_SUCCEEDED:
   260  		return pb.PresubmitRunStatus_PRESUBMIT_RUN_STATUS_SUCCEEDED, nil
   261  	case cvv0.Run_FAILED:
   262  		return pb.PresubmitRunStatus_PRESUBMIT_RUN_STATUS_FAILED, nil
   263  	case cvv0.Run_CANCELLED:
   264  		return pb.PresubmitRunStatus_PRESUBMIT_RUN_STATUS_CANCELED, nil
   265  	}
   266  	return pb.PresubmitRunStatus_PRESUBMIT_RUN_STATUS_UNSPECIFIED, fmt.Errorf("unknown run status %q", status)
   267  }
   268  
   269  func ValidateProject(project string) error {
   270  	if project == "" {
   271  		return errors.Reason("unspecified").Err()
   272  	}
   273  	if !projectRe.MatchString(project) {
   274  		return errors.Reason("must match %s", projectRe).Err()
   275  	}
   276  	return nil
   277  }
   278  
   279  // ValidateSources validates a set of sources.
   280  func ValidateSources(sources *pb.Sources) error {
   281  	return pbutil.ValidateSources(SourcesToResultDB(sources))
   282  }