go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/tryjob/requirement/util.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 requirement 16 17 import ( 18 "crypto" 19 "encoding/binary" 20 "fmt" 21 "math/rand" 22 "regexp" 23 "sort" 24 "strings" 25 26 "go.chromium.org/luci/common/data/stringset" 27 "go.chromium.org/luci/cv/internal/run" 28 ) 29 30 var ( 31 // Reference: https://chromium.googlesource.com/infra/luci/luci-py/+/a6655aa3/appengine/components/components/config/proto/service_config.proto#87 32 projectRE = `[a-z0-9\-]+` 33 // Reference: https://chromium.googlesource.com/infra/luci/luci-go/+/6b8fdd66/buildbucket/proto/project_config.proto#482 34 bucketRE = `[a-z0-9\-_.]+` 35 // Reference: https://chromium.googlesource.com/infra/luci/luci-go/+/6b8fdd66/buildbucket/proto/project_config.proto#220 36 builderRE = `[a-zA-Z0-9\-_.\(\) ]+` 37 modernProjBucketRe = fmt.Sprintf(`%s/%s`, projectRE, bucketRE) 38 legacyProjBucketRe = fmt.Sprintf(`luci\.%s\.%s`, projectRE, bucketRE) 39 buildersRE = fmt.Sprintf(`((%s)|(%s))\s*:\s*%s(\s*,\s*%s)*`, modernProjBucketRe, legacyProjBucketRe, builderRE, builderRE) 40 tryjobDirectiveLineRegexp = regexp.MustCompile(fmt.Sprintf(`^\s*%s(\s*;\s*%s)*\s*$`, buildersRE, buildersRE)) 41 ) 42 43 // parseTryjobDirectives parses a list of builders from the Tryjob directives 44 // lines provided via git footers like `Cq-Include-Trybots` and 45 // `Override-Tryjobs-For-Automation`. 46 // 47 // Return a list of builders. 48 func parseTryjobDirectives(directives []string) (stringset.Set, ComputationFailure) { 49 ret := make(stringset.Set) 50 for _, d := range directives { 51 builderStrings, compFail := parseBuilderStrings(d) 52 if compFail != nil { 53 return nil, compFail 54 } 55 for _, builderString := range builderStrings { 56 ret.Add(builderString) 57 } 58 } 59 return ret, nil 60 } 61 62 // TODO(robertocn): Consider moving the parsing of the Tryjob directives 63 // like `Cq-Include-Trybots` to the place where the footer values are 64 // extracted, and refactor the corresponding field in RunOptions accordingly 65 // (e.g. to be a list of builder ids). 66 func parseBuilderStrings(line string) ([]string, ComputationFailure) { 67 if !tryjobDirectiveLineRegexp.MatchString(line) { 68 return nil, &invalidTryjobDirectives{line} 69 } 70 var ret []string 71 for _, bucketSegment := range strings.Split(strings.TrimSpace(line), ";") { 72 parts := strings.Split(strings.TrimSpace(bucketSegment), ":") 73 if len(parts) != 2 { 74 panic(fmt.Errorf("impossible; expected %q separated by exactly one \":\", got %d", bucketSegment, len(parts)-1)) 75 } 76 projectBucket, builders := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) 77 var project, bucket string 78 if strings.HasPrefix(projectBucket, "luci.") { 79 // Legacy style. Example: luci.chromium.try: builder_a 80 parts := strings.SplitN(projectBucket, ".", 3) 81 project, bucket = parts[1], parts[2] 82 } else { 83 // Modern style. Example: chromium/try: builder_a 84 parts := strings.SplitN(projectBucket, "/", 2) 85 project, bucket = parts[0], parts[1] 86 } 87 for _, builderName := range strings.Split(builders, ",") { 88 ret = append(ret, fmt.Sprintf("%s/%s/%s", strings.TrimSpace(project), strings.TrimSpace(bucket), strings.TrimSpace(builderName))) 89 } 90 } 91 return ret, nil 92 } 93 94 // makeRands makes `n` new pseudo-random generators to be used for the Tryjob 95 // Requirement computation's random selections (e.g. equivalentBuilder) 96 // 97 // The generators are seeded deterministically based on the set of CLs (their 98 // IDs, specifically) and their trigger times. 99 // 100 // We do it this way so that recomputing the Requirement for the same Run yields 101 // the same result, but triggering the same set of CLs subsequent times has a 102 // chance of generating a different set of random selections. 103 func makeRands(in Input, n int) []*rand.Rand { 104 // Though MD5 is cryptographically broken, it's not being used here for 105 // security purposes, and it's faster than SHA. 106 h := crypto.MD5.New() 107 buf := make([]byte, 8) 108 cls := make([]*run.RunCL, len(in.CLs)) 109 copy(cls, in.CLs) 110 sort.Slice(cls, func(i, j int) bool { return cls[i].ID < cls[j].ID }) 111 var err error 112 for _, cl := range cls { 113 binary.LittleEndian.PutUint64(buf, uint64(cl.ID)) 114 _, err = h.Write(buf) 115 if err == nil && cl.Trigger != nil { 116 binary.LittleEndian.PutUint64(buf, uint64(cl.Trigger.Time.AsTime().UTC().Unix())) 117 _, err = h.Write(buf) 118 } 119 if err != nil { 120 panic(err) 121 } 122 } 123 digest := h.Sum(nil) 124 // Use the first eight bytes of the digest to seed a new rand, and use such 125 // rand's generated values to seed the requested number of generators. 126 baseRand := rand.New(rand.NewSource(int64(binary.LittleEndian.Uint64(digest[:8])))) 127 ret := make([]*rand.Rand, n) 128 for i := range ret { 129 ret[i] = rand.New(rand.NewSource(baseRand.Int63())) 130 } 131 return ret 132 }