go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/state/evalcl.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 state
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"go.chromium.org/luci/common/data/stringset"
    23  	"go.chromium.org/luci/common/errors"
    24  	"go.chromium.org/luci/common/logging"
    25  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    26  	"go.chromium.org/luci/common/retry/transient"
    27  	"go.chromium.org/luci/gae/service/datastore"
    28  	"google.golang.org/protobuf/proto"
    29  
    30  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    31  	"go.chromium.org/luci/cv/internal/changelist"
    32  	"go.chromium.org/luci/cv/internal/common"
    33  	"go.chromium.org/luci/cv/internal/configs/prjcfg"
    34  	"go.chromium.org/luci/cv/internal/gerrit/cfgmatcher"
    35  	"go.chromium.org/luci/cv/internal/gerrit/trigger"
    36  	"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
    37  	"go.chromium.org/luci/cv/internal/run"
    38  )
    39  
    40  // reevalPCLs re-evaluates PCLs after a project config change.
    41  //
    42  // If any are changed, marks PB.RepartitionRequired.
    43  func (s *State) reevalPCLs(ctx context.Context) error {
    44  	cls, errs, err := s.loadCLsForPCLs(ctx)
    45  	if err != nil {
    46  		return err
    47  	}
    48  	newPCLs := make([]*prjpb.PCL, len(cls))
    49  	mutated := false
    50  	for i, cl := range cls {
    51  		old := s.PB.GetPcls()[i]
    52  		switch pcl, err := s.makePCLFromDS(ctx, cl, errs[i], old); {
    53  		case err != nil:
    54  			return err
    55  		case pcl == nil:
    56  			panic("makePCLFromDS is wrong")
    57  		case pcl != old:
    58  			mutated = true
    59  			fallthrough
    60  		default:
    61  			newPCLs[i] = pcl
    62  		}
    63  	}
    64  	if mutated {
    65  		s.PB.Pcls = newPCLs
    66  		s.PB.RepartitionRequired = true
    67  		s.pclIndex = nil
    68  	}
    69  	return nil
    70  }
    71  
    72  // evalUpdatedCLs updates/inserts PCLs, if the PCL doesn't exist or with
    73  // an older eversion than the given eversion.
    74  func (s *State) evalUpdatedCLs(ctx context.Context, clEVersions map[int64]int64) error {
    75  	cls := make([]*changelist.CL, 0, len(clEVersions))
    76  	// Avoid doing anything in cases where all CL updates sent due to recent full
    77  	// poll iff we already know about each CL based on its EVersion.
    78  	for clid, ev := range clEVersions {
    79  		switch pcl := s.PB.GetPCL(clid); {
    80  		case pcl != nil && ev <= pcl.GetEversion():
    81  		default:
    82  			cls = append(cls, &changelist.CL{ID: common.CLID(clid)})
    83  		}
    84  	}
    85  	if len(cls) == 0 {
    86  		return nil
    87  	}
    88  	return s.evalCLsFromDS(ctx, cls)
    89  }
    90  
    91  func (s *State) evalCLs(ctx context.Context, clids []int64) error {
    92  	cls := make([]*changelist.CL, len(clids))
    93  	for i, clid := range clids {
    94  		cls[i] = &changelist.CL{ID: common.CLID(clid)}
    95  	}
    96  	return s.evalCLsFromDS(ctx, cls)
    97  }
    98  
    99  // evalCLsFromDS adds, updates, and marks for deletion PCLs based on CLs in
   100  // Datastore.
   101  //
   102  // Sorts passed cls slice and updates it with loaded from DS info.
   103  func (s *State) evalCLsFromDS(ctx context.Context, cls []*changelist.CL) error {
   104  	if s.cfgMatcher == nil {
   105  		meta, err := prjcfg.GetHashMeta(ctx, s.PB.GetLuciProject(), s.PB.GetConfigHash())
   106  		if err != nil {
   107  			return err
   108  		}
   109  		if s.configGroups, err = meta.GetConfigGroups(ctx); err != nil {
   110  			return err
   111  		}
   112  		s.cfgMatcher = cfgmatcher.LoadMatcherFromConfigGroups(ctx, s.configGroups, &meta)
   113  	}
   114  
   115  	// Sort new/updated CLs in the way as PCLs already are, namely by CL ID. Do it
   116  	// before loading from Datastore because `errs` must correspond to `cls`.
   117  	changelist.Sort(cls)
   118  	cls, errs, err := loadCLs(ctx, cls)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	// Now, while copying old PCLs slice into new PCLs slice,
   124  	// insert new PCL objects for new CLs and replace PCL objects for updated CLs.
   125  	// This preserves the property of PCLs being sorted.
   126  	// Since we have to re-create PCLs slice anyways, it's fastest to merge two
   127  	// sorted in the same way PCLs and CLs slices in  O(len(PCLs) + len(cls)).
   128  	oldPCLs := s.PB.GetPcls()
   129  	newPCLs := make([]*prjpb.PCL, 0, len(oldPCLs)+len(cls))
   130  	changed := common.CLIDsSet{}
   131  	for i, cl := range cls {
   132  		// Copy all old PCLs before this CL.
   133  		for len(oldPCLs) > 0 && common.CLID(oldPCLs[0].GetClid()) < cl.ID {
   134  			newPCLs = append(newPCLs, oldPCLs[0])
   135  			oldPCLs = oldPCLs[1:]
   136  		}
   137  		// If CL is updated, pop old.
   138  		var old *prjpb.PCL
   139  		if len(oldPCLs) > 0 && common.CLID(oldPCLs[0].GetClid()) == cl.ID {
   140  			old = oldPCLs[0]
   141  			oldPCLs = oldPCLs[1:]
   142  		}
   143  		// Compute new PCL.
   144  		switch pcl, err := s.makePCLFromDS(ctx, cl, errs[i], old); {
   145  		case err != nil:
   146  			return err
   147  		case pcl == nil && old != nil:
   148  			panic(fmt.Errorf("makePCLFromDS is wrong"))
   149  		case pcl == nil:
   150  			// New CL, but not in datastore. Don't add anything to newPCLs.
   151  			// This weird case was logged by makePCLFromDS already.
   152  		case pcl != old:
   153  			changed.Add(cl.ID)
   154  			fallthrough
   155  		default:
   156  			newPCLs = append(newPCLs, pcl)
   157  		}
   158  	}
   159  	if len(changed) == 0 {
   160  		return nil
   161  	}
   162  	// Copy remaining oldPCLs.
   163  	for len(oldPCLs) > 0 {
   164  		newPCLs = append(newPCLs, oldPCLs[0])
   165  		oldPCLs = oldPCLs[1:]
   166  	}
   167  	s.PB.Pcls = newPCLs
   168  	s.PB.RepartitionRequired = true
   169  	s.PB.Components = markForTriageOnChangedPCLs(s.PB.GetComponents(), s.PB.GetPcls(), changed)
   170  	s.pclIndex = nil
   171  	return nil
   172  }
   173  
   174  func (s *State) makePCLFromDS(ctx context.Context, cl *changelist.CL, err error, old *prjpb.PCL) (*prjpb.PCL, error) {
   175  	switch {
   176  	case err == datastore.ErrNoSuchEntity:
   177  		oldEversion := int64(0)
   178  		if old == nil {
   179  			logging.Errorf(ctx, "New CL %d not in Datastore", cl.ID)
   180  		} else {
   181  			// Must not happen outside of extremely rare un-deletion of a project
   182  			// whose PM state references long ago wiped out CLs.
   183  			logging.Errorf(ctx, "Old CL %d no longer in Datastore", cl.ID)
   184  			oldEversion = old.GetEversion()
   185  		}
   186  		return &prjpb.PCL{
   187  			Clid:     int64(cl.ID),
   188  			Eversion: oldEversion,
   189  			Status:   prjpb.PCL_DELETED,
   190  		}, nil
   191  	case err != nil:
   192  		return nil, errors.Annotate(err, "failed to load CL %d", cl.ID).Tag(transient.Tag).Err()
   193  	default:
   194  		pcl := s.makePCL(ctx, cl)
   195  		if proto.Equal(pcl, old) {
   196  			return old, nil
   197  		}
   198  		return pcl, nil
   199  	}
   200  }
   201  
   202  // makePCL creates new PCL based on Datastore CL entity and current config.
   203  func (s *State) makePCL(ctx context.Context, cl *changelist.CL) *prjpb.PCL {
   204  	if s.cfgMatcher == nil {
   205  		panic("cfgMather must be initialized")
   206  	}
   207  	pcl := &prjpb.PCL{
   208  		Clid:     int64(cl.ID),
   209  		Eversion: int64(cl.EVersion),
   210  		Status:   prjpb.PCL_UNKNOWN,
   211  	}
   212  
   213  	var ap *changelist.ApplicableConfig_Project
   214  	switch kind, reason := cl.AccessKindWithReason(ctx, s.PB.GetLuciProject()); kind {
   215  	case changelist.AccessUnknown:
   216  		// Need more time to fetch this.
   217  		logging.Debugf(ctx, "CL %d %s %s", cl.ID, cl.ExternalID, reason)
   218  		return pcl
   219  	case changelist.AccessDeniedProbably:
   220  		// PM should not create a new Run in such cases, but PM won't terminate
   221  		// existing Run when Run Manager can and should do it on its own.
   222  		fallthrough
   223  	case changelist.AccessDenied:
   224  		logging.Infof(ctx, "This project has no access to CL(%d %s): %s", cl.ID, cl.ExternalID, reason)
   225  		var watchedByMultiple bool
   226  		ap, watchedByMultiple = cl.IsWatchedByThisAndOtherProjects(s.PB.GetLuciProject())
   227  		if !watchedByMultiple {
   228  			pcl.Status = prjpb.PCL_UNWATCHED
   229  			return pcl
   230  		}
   231  		// Special case if the CL is watched by more than one project.
   232  		err := newMultiProjectWatchError(cl)
   233  		pcl.PurgeReasons = append(pcl.PurgeReasons,
   234  			&prjpb.PurgeReason{
   235  				ClError: err,
   236  				ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true},
   237  			})
   238  		pcl.Errors = append(pcl.Errors, err)
   239  		pcl.Status = prjpb.PCL_OK
   240  	case changelist.AccessGranted:
   241  		switch {
   242  		case cl.Snapshot.GetOutdated() != nil:
   243  			// Need more time to fetch this.
   244  			logging.Debugf(ctx, "CL %d %s Snapshot is outdated", cl.ID, cl.ExternalID)
   245  			pcl.Outdated = cl.Snapshot.GetOutdated()
   246  			return pcl
   247  		case cl.Snapshot.GetGerrit() == nil:
   248  			panic(fmt.Errorf("only Gerrit CLs supported for now"))
   249  		case len(cl.ApplicableConfig.GetProjects()) != 1:
   250  			panic(fmt.Errorf("AccessGranted but %d projects in ApplicableConfig", len(cl.ApplicableConfig.GetProjects())))
   251  		}
   252  		ap = cl.ApplicableConfig.GetProjects()[0]
   253  		pcl.Status = prjpb.PCL_OK
   254  	default:
   255  		panic(fmt.Errorf("unknown access kind %d", kind))
   256  	}
   257  
   258  	s.setApplicableConfigGroups(ap, cl.Snapshot, pcl)
   259  	if errs := cl.Snapshot.GetErrors(); len(errs) > 0 {
   260  		for _, err := range errs {
   261  			pcl.PurgeReasons = append(pcl.PurgeReasons, &prjpb.PurgeReason{
   262  				ClError: err,
   263  				ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true},
   264  			})
   265  			pcl.Errors = append(pcl.Errors, err)
   266  		}
   267  	}
   268  
   269  	pcl.Outdated = cl.Snapshot.GetOutdated()
   270  	pcl.Deps = cl.Snapshot.GetDeps()
   271  	for _, d := range pcl.GetDeps() {
   272  		if d.GetClid() == pcl.GetClid() {
   273  			if d.GetKind() != changelist.DepKind_SOFT {
   274  				logging.Errorf(ctx, "BUG: self-referential %s dep: CL %d with Snapshot\n%s", d.GetKind(), cl.ID, cl.Snapshot)
   275  				// If this actually happens, better to proceed with bad error message to
   276  				// the user than crash later while processing the CL.
   277  			}
   278  			// TODO(robertocn): allow new patchset runs to continue even in the
   279  			// case of this error, since new patchset runs should not be subject
   280  			// to dependencies.
   281  			err := &changelist.CLError{
   282  				Kind: &changelist.CLError_SelfCqDepend{SelfCqDepend: true},
   283  			}
   284  			pcl.PurgeReasons = append(pcl.PurgeReasons, &prjpb.PurgeReason{
   285  				ClError: err,
   286  				ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true},
   287  			})
   288  			pcl.Errors = append(pcl.Errors, err)
   289  		}
   290  	}
   291  
   292  	ci := cl.Snapshot.GetGerrit().GetInfo()
   293  	if ci.GetStatus() == gerritpb.ChangeStatus_MERGED {
   294  		pcl.Submitted = true
   295  		return pcl
   296  	}
   297  
   298  	if ci.GetOwner().GetEmail() == "" {
   299  		err := &changelist.CLError{
   300  			Kind: &changelist.CLError_OwnerLacksEmail{OwnerLacksEmail: true},
   301  		}
   302  		pcl.PurgeReasons = append(pcl.PurgeReasons, &prjpb.PurgeReason{
   303  			ClError: err,
   304  			ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true},
   305  		})
   306  		pcl.Errors = append(pcl.Errors, err)
   307  	}
   308  
   309  	s.setTriggers(ci, pcl, cl.TriggerNewPatchsetRunAfterPS)
   310  
   311  	// Check for "Commit: false" footer after setting Trigger, because this should
   312  	// only have an effect in the case of an attempted full run.
   313  	if hasCommitFalseFlag(cl.Snapshot.GetMetadata()) && pcl.GetTriggers().GetCqVoteTrigger().GetMode() == string(run.FullRun) {
   314  		err := &changelist.CLError{
   315  			Kind: &changelist.CLError_CommitBlocked{CommitBlocked: true},
   316  		}
   317  		pcl.PurgeReasons = append(pcl.PurgeReasons, &prjpb.PurgeReason{
   318  			ClError: err,
   319  			ApplyTo: &prjpb.PurgeReason_Triggers{
   320  				Triggers: &run.Triggers{
   321  					CqVoteTrigger: pcl.GetTriggers().GetCqVoteTrigger(),
   322  				},
   323  			},
   324  		})
   325  		pcl.Errors = append(pcl.Errors, err)
   326  	}
   327  	return pcl
   328  }
   329  
   330  // setApplicableConfigGroups sets ConfigGroup indexes of PCL.
   331  //
   332  // If provided ApplicableConfig_Project is up to date, uses config groups from
   333  // it. Otherwise, matches against project config directly using s.cfgMatcher.
   334  //
   335  // Expects s.cfgMatcher to be set.
   336  //
   337  // Modifies the passed PCL.
   338  func (s *State) setApplicableConfigGroups(ap *changelist.ApplicableConfig_Project, snapshot *changelist.Snapshot, pcl *prjpb.PCL) {
   339  	// Most likely, ApplicableConfig stored in a CL entity is still up-to-date.
   340  	if upToDate := s.tryUsingApplicableConfigGroups(ap, pcl); upToDate {
   341  		return
   342  	}
   343  	// Project's config has been updated after CL snapshot was made.
   344  	g := snapshot.GetGerrit()
   345  	ci := g.GetInfo()
   346  	for _, id := range s.cfgMatcher.Match(g.GetHost(), ci.GetProject(), ci.GetRef()) {
   347  		index := s.indexOfConfigGroup(prjcfg.ConfigGroupID(id))
   348  		pcl.ConfigGroupIndexes = append(pcl.ConfigGroupIndexes, index)
   349  	}
   350  }
   351  
   352  // tryUsingApplicableConfigGroups sets ConfigGroup indexes of a PCL based on
   353  // ApplicableConfig_Project if ApplicableConfig_Project references the State's
   354  // config hash.
   355  //
   356  // Modifies the passed PCL.
   357  // Returns whether config hash matched.
   358  func (s *State) tryUsingApplicableConfigGroups(ap *changelist.ApplicableConfig_Project, pcl *prjpb.PCL) bool {
   359  	expectedConfigHash := s.PB.GetConfigHash()
   360  	// At least 1 ID is guaranteed in ApplicableConfig_Project by gerrit.gobmap.
   361  	for _, id := range ap.GetConfigGroupIds() {
   362  		if prjcfg.ConfigGroupID(id).Hash() != expectedConfigHash {
   363  			return false
   364  		}
   365  	}
   366  	for _, id := range ap.GetConfigGroupIds() {
   367  		index := s.indexOfConfigGroup(prjcfg.ConfigGroupID(id))
   368  		pcl.ConfigGroupIndexes = append(pcl.ConfigGroupIndexes, index)
   369  	}
   370  	return true
   371  }
   372  
   373  // setTriggers populates a PCL's .Triggers field with the triggers present in
   374  // the given ChangeInfo.
   375  //
   376  // It also validates that the trigger mode is allowed, and strips the
   377  // information from the triggerer.
   378  func (s *State) setTriggers(ci *gerritpb.ChangeInfo, pcl *prjpb.PCL, latestPSRun int32) {
   379  	// Triggers are a function of a CL and applicable ConfigGroup, which may
   380  	// define additional modes.
   381  	// In case of misconfiguration, there may be 0 or 2+ applicable
   382  	// ConfigGroups, in which case we use empty ConfigGroup{}.
   383  	// This doesn't matter much, since such CLs will be soon purged.
   384  	// In the very worst case, CL purger will remove just the CQ vote and not
   385  	// the additional label's vote defined in actually intended ConfigGroup,
   386  	// which isn't a big deal.
   387  	var cg *cfgpb.ConfigGroup
   388  	if idxs := pcl.GetConfigGroupIndexes(); len(idxs) == 1 {
   389  		cg = s.configGroups[idxs[0]].Content
   390  	} else {
   391  		cg = &cfgpb.ConfigGroup{}
   392  	}
   393  	ts := trigger.Find(&trigger.FindInput{ChangeInfo: ci, ConfigGroup: cg, TriggerNewPatchsetRunAfterPS: latestPSRun})
   394  	if ts == nil {
   395  		return
   396  	}
   397  
   398  	allowedRunModes := stringset.NewFromSlice(
   399  		"",
   400  		string(run.DryRun),
   401  		string(run.FullRun),
   402  		string(run.NewPatchsetRun),
   403  	)
   404  	for _, am := range cg.GetAdditionalModes() {
   405  		allowedRunModes.Add(am.GetName())
   406  	}
   407  
   408  	for _, modeString := range []string{ts.GetCqVoteTrigger().GetMode(), ts.GetNewPatchsetRunTrigger().GetMode()} {
   409  		if !allowedRunModes.Has(modeString) {
   410  			err := &changelist.CLError{
   411  				Kind: &changelist.CLError_UnsupportedMode{UnsupportedMode: string(modeString)},
   412  			}
   413  			pcl.PurgeReasons = append(pcl.PurgeReasons, &prjpb.PurgeReason{
   414  				ClError: err,
   415  				ApplyTo: &prjpb.PurgeReason_Triggers{
   416  					Triggers: ts,
   417  				},
   418  			})
   419  			pcl.Errors = append(pcl.Errors, err)
   420  			return
   421  		}
   422  	}
   423  
   424  	pcl.Triggers = ts
   425  }
   426  
   427  // loadCLsForPCLs loads CLs from Datastore corresponding to PCLs.
   428  //
   429  // Returns slice of CLs, error.MultiError slice corresponding to
   430  // per-CL errors *(always exists and has same length as CLs)*, and a
   431  // top level error if it can't be attributed to any CL.
   432  //
   433  // *each error in per-CL errors* is not annotated and is nil if CL was loaded
   434  // successfully.
   435  func (s *State) loadCLsForPCLs(ctx context.Context) ([]*changelist.CL, errors.MultiError, error) {
   436  	cls := make([]*changelist.CL, len(s.PB.GetPcls()))
   437  	for i, pcl := range s.PB.GetPcls() {
   438  		cls[i] = &changelist.CL{ID: common.CLID(pcl.GetClid())}
   439  	}
   440  	return loadCLs(ctx, cls)
   441  }
   442  
   443  func loadCLs(ctx context.Context, cls []*changelist.CL) ([]*changelist.CL, errors.MultiError, error) {
   444  	// At 0.007 KiB (serialized) per CL as of Jan 2021, this should scale 2000 CLs
   445  	// with reasonable RAM footprint and well within 10s because
   446  	// datastore.GetMulti splits it into concurrently queried batches.
   447  	// To support more, CLs would need to be loaded and processed in batches,
   448  	// or CL snapshot size reduced.
   449  	err := datastore.Get(ctx, cls)
   450  	switch merr, ok := err.(errors.MultiError); {
   451  	case err == nil:
   452  		return cls, make(errors.MultiError, len(cls)), nil
   453  	case ok:
   454  		return cls, merr, nil
   455  	default:
   456  		return nil, nil, errors.Annotate(err, "failed to load %d CLs", len(cls)).Tag(transient.Tag).Err()
   457  	}
   458  }
   459  
   460  func newMultiProjectWatchError(cl *changelist.CL) *changelist.CLError {
   461  	projects := make([]string, len(cl.ApplicableConfig.GetProjects()))
   462  	for i, p := range cl.ApplicableConfig.GetProjects() {
   463  		projects[i] = p.GetName()
   464  	}
   465  	return &changelist.CLError{
   466  		Kind: &changelist.CLError_WatchedByManyProjects_{
   467  			WatchedByManyProjects: &changelist.CLError_WatchedByManyProjects{
   468  				Projects: projects,
   469  			},
   470  		},
   471  	}
   472  }
   473  
   474  func hasCommitFalseFlag(metadata []*changelist.StringPair) bool {
   475  	for _, p := range metadata {
   476  		// The values stored in the CL Snapshot Metadata are not necessarily normalized,
   477  		// and could have come from "Commit: false", "COMMIT=FALSE" or some other style.
   478  		// Other possible values like "Commit: no" are not recognized.
   479  		if strings.ToLower(p.Key) == "commit" && strings.ToLower(p.Value) == "false" {
   480  			return true
   481  		}
   482  	}
   483  	return false
   484  }