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  }