go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/common/run.go (about)

     1  // Copyright 2020 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 common
    16  
    17  import (
    18  	"context"
    19  	"encoding/hex"
    20  	"fmt"
    21  	"sort"
    22  	"strings"
    23  	"time"
    24  
    25  	"go.chromium.org/luci/auth/identity"
    26  	"go.chromium.org/luci/common/errors"
    27  	"go.chromium.org/luci/common/logging"
    28  	"go.chromium.org/luci/config"
    29  	"go.chromium.org/luci/server/auth"
    30  )
    31  
    32  // RunKind is the Datastore entity kind for Run.
    33  const RunKind = "Run"
    34  
    35  // MaxRunTotalDuration is the max total duration of the Run.
    36  //
    37  // Total duration means end time - create time. Run will be cancelled after
    38  // the total duration is reached.
    39  const MaxRunTotalDuration = 10 * 24 * time.Hour // 10 days
    40  
    41  // RunID is an unique RunID to identify a Run in CV.
    42  //
    43  // RunID is string like `luciProject/inverseTS-1-hexHashDigest` consisting of
    44  // 7 parts:
    45  //  1. The LUCI Project that this Run belongs to.
    46  //     Purpose: separates load on Datastore from different projects.
    47  //  2. `/` separator.
    48  //  3. InverseTS, defined as (`endOfTheWorld` - CreateTime) in ms precision,
    49  //     left-padded with zeros to 13 digits. See `Run.CreateTime` Doc.
    50  //     Purpose: ensures queries by default orders runs of the same project by
    51  //     most recent first.
    52  //  4. `-` separator.
    53  //  5. Digest version (see part 7).
    54  //  6. `-` separator.
    55  //  7. A hex digest string uniquely identifying the set of CLs involved in
    56  //     this Run.
    57  //     Purpose: ensures two simultaneously started Runs in the same project
    58  //     won't have the same RunID.
    59  type RunID string
    60  
    61  // CV will be dead on ~292.3 years after first LUCI design doc was created.
    62  //
    63  // Computed as https://play.golang.com/p/hDQ-EhlSLu5
    64  //
    65  //	luci := time.Date(2014, time.May, 9, 1, 26, 0, 0, time.UTC)
    66  //	endOfTheWorld := luci.Add(time.Duration(1<<63 - 1))
    67  var endOfTheWorld = time.Date(2306, time.August, 19, 1, 13, 16, 854775807, time.UTC)
    68  
    69  func MakeRunID(luciProject string, createTime time.Time, digestVersion int, clsDigest []byte) RunID {
    70  	if endOfTheWorld.Sub(createTime) == 1<<63-1 {
    71  		panic(fmt.Errorf("overflow"))
    72  	}
    73  	ms := endOfTheWorld.Sub(createTime).Milliseconds()
    74  	if ms < 0 {
    75  		panic(fmt.Errorf("Can't create run at %s which is after endOfTheWorld %s", createTime, endOfTheWorld))
    76  	}
    77  	id := fmt.Sprintf("%s/%013d-%d-%s", luciProject, ms, digestVersion, hex.EncodeToString(clsDigest))
    78  	return RunID(id)
    79  }
    80  
    81  // Validate returns an error if Run ID is not valid.
    82  //
    83  // If validate returns nil,
    84  //   - it means all other methods on RunID will work fine instead of panicking,
    85  //   - it doesn't mean Run ID is possible to generate using the MakeRunID.
    86  //     This is especially relevant in CV tests, where specifying short Run IDs is
    87  //     useful.
    88  func (id RunID) Validate() (err error) {
    89  	defer func() {
    90  		if err != nil {
    91  			err = errors.Annotate(err, "malformed RunID %q", id).Err()
    92  		}
    93  	}()
    94  
    95  	allDigits := func(digits string) bool {
    96  		for _, r := range digits {
    97  			if r < '0' || r > '9' {
    98  				return false
    99  			}
   100  		}
   101  		return true
   102  	}
   103  
   104  	s := string(id)
   105  	i := strings.IndexRune(s, '/')
   106  	if i < 1 {
   107  		return fmt.Errorf("lacks LUCI project")
   108  	}
   109  	if err := config.ValidateProjectName(s[:i]); err != nil {
   110  		return fmt.Errorf("invalid LUCI project part: %s", err)
   111  	}
   112  	s = s[i+1:]
   113  
   114  	i = strings.IndexRune(s, '-')
   115  	if i < 1 {
   116  		return fmt.Errorf("lacks InverseTS part")
   117  	}
   118  	if !allDigits(s[:i]) {
   119  		return fmt.Errorf("invalid InverseTS")
   120  	}
   121  
   122  	s = s[i+1:]
   123  	i = strings.IndexRune(s, '-')
   124  	if i < 1 {
   125  		return fmt.Errorf("lacks version")
   126  	}
   127  	if !allDigits(s[:i]) {
   128  		return fmt.Errorf("invalid version")
   129  	}
   130  
   131  	s = s[i+1:]
   132  	if len(s) == 0 {
   133  		return fmt.Errorf("lacks digest")
   134  	}
   135  	return nil
   136  }
   137  
   138  // LUCIProject this Run belongs to.
   139  func (id RunID) LUCIProject() string {
   140  	pos := strings.IndexRune(string(id), '/')
   141  	if pos == -1 {
   142  		panic(fmt.Errorf("invalid run ID %q", id))
   143  	}
   144  	return string(id[:pos])
   145  }
   146  
   147  // Inner is the part after "<LUCIProject>/" for use in UI.
   148  func (id RunID) Inner() string {
   149  	pos := strings.IndexRune(string(id), '/')
   150  	if pos == -1 {
   151  		panic(fmt.Errorf("invalid run ID %q", id))
   152  	}
   153  	return string(id[pos+1:])
   154  }
   155  
   156  // InverseTS of this Run. See RunID doc.
   157  func (id RunID) InverseTS() string {
   158  	s := string(id)
   159  	posSlash := strings.IndexRune(s, '/')
   160  	if posSlash == -1 {
   161  		panic(fmt.Errorf("invalid run ID %q", id))
   162  	}
   163  	s = s[posSlash+1:]
   164  	posDash := strings.IndexRune(s, '-')
   165  	return s[:posDash]
   166  }
   167  
   168  // PublicID returns the public representation of the RunID.
   169  //
   170  // The format of a public ID is `projects/$luci-project/runs/$id`, where
   171  // - luci-project is the name of the LUCI project the Run belongs to
   172  // - id is an opaque key unique in the LUCI project.
   173  func (id RunID) PublicID() string {
   174  	prj := id.LUCIProject()
   175  	return fmt.Sprintf("projects/%s/runs/%s", prj, string(id[len(prj)+1:]))
   176  }
   177  
   178  // FromPublicRunID is the inverse of RunID.PublicID().
   179  func FromPublicRunID(id string) (RunID, error) {
   180  	parts := strings.Split(id, "/")
   181  	if len(parts) == 4 && parts[0] == "projects" && parts[2] == "runs" {
   182  		return RunID(parts[1] + "/" + parts[3]), nil
   183  	}
   184  	return "", errors.Reason(`Run ID must be in the form "projects/$luci-project/runs/$id", but %q given"`, id).Err()
   185  }
   186  
   187  // AttemptKey returns CQDaemon attempt key.
   188  func (id RunID) AttemptKey() string {
   189  	i := strings.LastIndexByte(string(id), '-')
   190  	if i == -1 || i == len(id)-1 {
   191  		panic(fmt.Errorf("invalid run ID %q", id))
   192  	}
   193  	return string(id[i+1:])
   194  }
   195  
   196  // RunIDs is a convenience type to facilitate handling of run RunIDs.
   197  type RunIDs []RunID
   198  
   199  // sort.Interface copy-pasta.
   200  func (ids RunIDs) Less(i, j int) bool { return ids[i] < ids[j] }
   201  func (ids RunIDs) Len() int           { return len(ids) }
   202  func (ids RunIDs) Swap(i, j int)      { ids[i], ids[j] = ids[j], ids[i] }
   203  
   204  // WithoutSorted returns a subsequence of IDs without excluded IDs.
   205  //
   206  // Both this and the excluded slices must be sorted.
   207  //
   208  // If this and excluded IDs are disjoint, return this slice.
   209  // Otherwise, returns a copy without excluded IDs.
   210  func (ids RunIDs) WithoutSorted(exclude RunIDs) RunIDs {
   211  	remaining := ids
   212  	ret := ids
   213  	mutated := false
   214  	for {
   215  		switch {
   216  		case len(remaining) == 0:
   217  			return ret
   218  		case len(exclude) == 0:
   219  			if mutated {
   220  				ret = append(ret, remaining...)
   221  			}
   222  			return ret
   223  		case remaining[0] < exclude[0]:
   224  			if mutated {
   225  				ret = append(ret, remaining[0])
   226  			}
   227  			remaining = remaining[1:]
   228  		case remaining[0] > exclude[0]:
   229  			exclude = exclude[1:]
   230  		default:
   231  			if !mutated {
   232  				// Must copy all IDs that were skipped.
   233  				mutated = true
   234  				n := len(ids) - len(remaining)
   235  				ret = make(RunIDs, n, len(ids)-1)
   236  				copy(ret, ids) // copies len(ret) == n elements.
   237  			}
   238  			remaining = remaining[1:]
   239  			exclude = exclude[1:]
   240  		}
   241  	}
   242  }
   243  
   244  // InsertSorted adds given ID if not yet exists to the list keeping list sorted.
   245  //
   246  // InsertSorted is a pointer receiver method, because it modifies slice itself.
   247  func (p *RunIDs) InsertSorted(id RunID) {
   248  	ids := *p
   249  	switch i := sort.Search(len(ids), func(i int) bool { return ids[i] >= id }); {
   250  	case i == len(ids):
   251  		*p = append(ids, id)
   252  	case ids[i] > id:
   253  		// Insert new ID at position i and shift the rest of slice to the right.
   254  		toInsert := id
   255  		for ; i < len(ids); i++ {
   256  			ids[i], toInsert = toInsert, ids[i]
   257  		}
   258  		*p = append(ids, toInsert)
   259  	}
   260  }
   261  
   262  // DelSorted removes the given ID if it exists.
   263  //
   264  // DelSorted is a pointer receiver method, because it modifies slice itself.
   265  func (p *RunIDs) DelSorted(id RunID) bool {
   266  	ids := *p
   267  	i := sort.Search(len(ids), func(i int) bool { return ids[i] >= id })
   268  	if i == len(ids) || ids[i] != id {
   269  		return false
   270  	}
   271  
   272  	copy(ids[i:], ids[i+1:])
   273  	ids[len(ids)-1] = ""
   274  	*p = ids[:len(ids)-1]
   275  	return true
   276  }
   277  
   278  // ContainsSorted returns true if ids contain the given one.
   279  func (ids RunIDs) ContainsSorted(id RunID) bool {
   280  	i := sort.Search(len(ids), func(i int) bool { return ids[i] >= id })
   281  	return i < len(ids) && ids[i] == id
   282  }
   283  
   284  // DifferenceSorted returns all IDs in this slice and not the other one.
   285  //
   286  // Both slices must be sorted. Doesn't modify input slices.
   287  func (a RunIDs) DifferenceSorted(b RunIDs) RunIDs {
   288  	var diff RunIDs
   289  	for {
   290  		if len(b) == 0 {
   291  			return append(diff, a...)
   292  		}
   293  		if len(a) == 0 {
   294  			return diff
   295  		}
   296  		x, y := a[0], b[0]
   297  		switch {
   298  		case x == y:
   299  			a, b = a[1:], b[1:]
   300  		case x < y:
   301  			diff = append(diff, x)
   302  			a = a[1:]
   303  		default:
   304  			b = b[1:]
   305  		}
   306  	}
   307  }
   308  
   309  // Index returns the index of the first instance of the provided id.
   310  //
   311  // Returns -1 if the provided id isn't present.
   312  func (ids RunIDs) Index(target RunID) int {
   313  	for i, id := range ids {
   314  		if id == target {
   315  			return i
   316  		}
   317  	}
   318  	return -1
   319  }
   320  
   321  // Equal checks if two ids are equal.
   322  func (ids RunIDs) Equal(other RunIDs) bool {
   323  	if len(ids) != len(other) {
   324  		return false
   325  	}
   326  	for i, id := range ids {
   327  		if id != other[i] {
   328  			return false
   329  		}
   330  	}
   331  	return true
   332  }
   333  
   334  // Set returns a new set of run IDs.
   335  func (ids RunIDs) Set() map[RunID]struct{} {
   336  	r := make(map[RunID]struct{}, len(ids))
   337  	for _, id := range ids {
   338  		r[id] = struct{}{}
   339  	}
   340  	return r
   341  }
   342  
   343  // MakeRunIDs returns RunIDs from list of strings.
   344  func MakeRunIDs(ids ...string) RunIDs {
   345  	ret := make(RunIDs, len(ids))
   346  	for i, id := range ids {
   347  		ret[i] = RunID(id)
   348  	}
   349  	return ret
   350  }
   351  
   352  // MCEDogfooderGroup is a CrIA group who signed up for dogfooding MCE.
   353  const MCEDogfooderGroup = "luci-cv-mce-dogfooders"
   354  
   355  // IsMCEDogfooder returns true if the user is an MCE dogfooder.
   356  //
   357  // TODO(ddoman): remove this function, once MCE dogfood is done.
   358  func IsMCEDogfooder(ctx context.Context, id identity.Identity) bool {
   359  	// if it fails to retrieve the authDB, then log the error and return false.
   360  	// this function will be removed, anyways.
   361  	ret, err := auth.GetState(ctx).DB().IsMember(ctx, id, []string{MCEDogfooderGroup})
   362  	if err != nil {
   363  		logging.Errorf(ctx, "IsMCEDogfooder: auth.IsMember: %s", err)
   364  	}
   365  	return ret
   366  }
   367  
   368  // InstantTriggerDogfooderGroup is the CrIA group who signed up for dogfooding
   369  // cros instant trigger.
   370  const InstantTriggerDogfooderGroup = "luci-cv-instant-trigger-dogfooders"
   371  
   372  // IsInstantTriggerDogfooder returns true if the given user participate in
   373  // the cros instant trigger dogfood.
   374  //
   375  // TODO(yiwzhang): remove this function, once cros instant trigger dogfood is
   376  // done.
   377  func IsInstantTriggerDogfooder(ctx context.Context, id identity.Identity) bool {
   378  	// if it fails to retrieve the authDB, then log the error and return false.
   379  	// this function will be removed, anyways.
   380  	ret, err := auth.GetState(ctx).DB().IsMember(ctx, id, []string{InstantTriggerDogfooderGroup})
   381  	if err != nil {
   382  		logging.Errorf(ctx, "IsInstantTriggerDogfooder: auth.IsMember: %s", err)
   383  		return false
   384  	}
   385  	return ret
   386  }