go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/pbutil/invocation.go (about) 1 // Copyright 2019 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 16 17 import ( 18 "go.chromium.org/luci/common/errors" 19 pb "go.chromium.org/luci/resultdb/proto/v1" 20 ) 21 22 const invocationIDPattern = `[a-z][a-z0-9_\-:.]{0,99}` 23 24 var invocationIDRe = regexpf("^%s$", invocationIDPattern) 25 var invocationNameRe = regexpf("^invocations/(%s)$", invocationIDPattern) 26 27 // ValidateInvocationID returns a non-nil error if id is invalid. 28 func ValidateInvocationID(id string) error { 29 return validateWithRe(invocationIDRe, id) 30 } 31 32 // ValidateInvocationName returns a non-nil error if name is invalid. 33 func ValidateInvocationName(name string) error { 34 _, err := ParseInvocationName(name) 35 return err 36 } 37 38 // ParseInvocationName extracts the invocation id. 39 func ParseInvocationName(name string) (id string, err error) { 40 if name == "" { 41 return "", unspecified() 42 } 43 44 m := invocationNameRe.FindStringSubmatch(name) 45 if m == nil { 46 return "", doesNotMatch(invocationNameRe) 47 } 48 return m[1], nil 49 } 50 51 // InvocationName synthesizes an invocation name from an id. 52 // Does not validate id, use ValidateInvocationID. 53 func InvocationName(id string) string { 54 return "invocations/" + id 55 } 56 57 // NormalizeInvocation converts inv to the canonical form. 58 func NormalizeInvocation(inv *pb.Invocation) { 59 SortStringPairs(inv.Tags) 60 61 changelists := inv.SourceSpec.GetSources().GetChangelists() 62 SortGerritChanges(changelists) 63 } 64 65 // ValidateSourceSpec validates a source specification. 66 func ValidateSourceSpec(sourceSpec *pb.SourceSpec) error { 67 // Treat nil sourceSpec message as empty message. 68 if sourceSpec.GetInherit() && sourceSpec.GetSources() != nil { 69 return errors.Reason("only one of inherit and sources may be set").Err() 70 } 71 if sourceSpec.GetSources() != nil { 72 if err := ValidateSources(sourceSpec.Sources); err != nil { 73 return errors.Annotate(err, "sources").Err() 74 } 75 } 76 return nil 77 } 78 79 // ValidateSources validates a set of sources. 80 func ValidateSources(sources *pb.Sources) error { 81 if sources == nil { 82 return errors.Reason("unspecified").Err() 83 } 84 if err := ValidateGitilesCommit(sources.GetGitilesCommit()); err != nil { 85 return errors.Annotate(err, "gitiles_commit").Err() 86 } 87 88 if len(sources.Changelists) > 10 { 89 return errors.Reason("changelists: exceeds maximum of 10 changelists").Err() 90 } 91 type distinctChangelist struct { 92 host string 93 change int64 94 } 95 clToIndex := make(map[distinctChangelist]int) 96 97 for i, cl := range sources.Changelists { 98 if err := ValidateGerritChange(cl); err != nil { 99 return errors.Annotate(err, "changelists[%v]", i).Err() 100 } 101 cl := distinctChangelist{ 102 host: cl.Host, 103 change: cl.Change, 104 } 105 if duplicateIndex, ok := clToIndex[cl]; ok { 106 return errors.Reason("changelists[%v]: duplicate change modulo patchset number; same change at changelists[%v]", i, duplicateIndex).Err() 107 } 108 clToIndex[cl] = i 109 } 110 return nil 111 }