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 }