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

     1  // Copyright 2023 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 rpc
    16  
    17  import (
    18  	"context"
    19  
    20  	"google.golang.org/grpc/codes"
    21  
    22  	bbutil "go.chromium.org/luci/buildbucket/protoutil"
    23  	"go.chromium.org/luci/common/logging"
    24  	"go.chromium.org/luci/grpc/appstatus"
    25  
    26  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    27  	apiv0pb "go.chromium.org/luci/cv/api/v0"
    28  	"go.chromium.org/luci/cv/internal/configs/prjcfg"
    29  	"go.chromium.org/luci/cv/internal/rpc/versioning"
    30  	"go.chromium.org/luci/cv/internal/run"
    31  	"go.chromium.org/luci/cv/internal/tryjob"
    32  )
    33  
    34  func makeTryjobInvocations(ctx context.Context, r *run.Run) ([]*apiv0pb.TryjobInvocation, error) {
    35  	if r.Tryjobs.GetState() == nil {
    36  		return nil, nil // early return if there is no tryjob
    37  	}
    38  	// TODO(yiwzhang): store the config group ID in the
    39  	// r.Tryjobs.State.Requirement. Using r.ConfigGroupID has a slight chance
    40  	// causing unexpected behavior. For example:
    41  	//  T1: config is at revision X and builder A is triggered
    42  	//  T2: config gets a new revision X+1 and builder A is removed and the
    43  	//      run is cancelled due to config change.
    44  	//  T3: user loads the cancelled run. r.Tryjobs may contain information about
    45  	//      triggering builder A. However, the config associated with the run
    46  	//      no longer has builder A definition. Causing error in a later stage.
    47  	// Saving the config group ID (i.e. pinned at the revision) that is used to
    48  	// compute the requirement inside the requirement would solve the problem.
    49  	cg, err := prjcfg.GetConfigGroup(ctx, r.ID.LUCIProject(), r.ConfigGroupID)
    50  	if err != nil {
    51  		return nil, appstatus.Attachf(err, codes.Internal, "failed to load config group for the run")
    52  	}
    53  	builderByName := computeBuilderByName(cg)
    54  	ret := make([]*apiv0pb.TryjobInvocation, 0, len(r.Tryjobs.GetState().GetExecutions()))
    55  	for i, execution := range r.Tryjobs.GetState().GetExecutions() {
    56  		ti := &apiv0pb.TryjobInvocation{}
    57  		definition := r.Tryjobs.GetState().GetRequirement().GetDefinitions()[i]
    58  		switch definition.GetBackend().(type) {
    59  		case *tryjob.Definition_Buildbucket_:
    60  			builderName := bbutil.FormatBuilderID(definition.GetBuildbucket().GetBuilder())
    61  			builderConfig, ok := builderByName[builderName]
    62  			if !ok {
    63  				// Error out once the TODO above is implemented
    64  				logging.Warningf(ctx, "builder %q is triggered by LUCI CV but can NOT be found in the config. Skip including the builder in the response", builderName)
    65  				continue
    66  			}
    67  			ti.BuilderConfig = builderConfig
    68  		default:
    69  			return nil, appstatus.Errorf(codes.Unimplemented, "unknown tryjob backend %T", definition.GetBackend())
    70  		}
    71  		ti.Critical = definition.GetCritical()
    72  		if len(execution.GetAttempts()) == 0 {
    73  			logging.Errorf(ctx, "NEEDS INVESTIGATION: tryjob execution has empty attempts. tryjob definition: %s", definition)
    74  			continue
    75  		}
    76  		ti.Attempts = make([]*apiv0pb.TryjobInvocation_Attempt, len(execution.GetAttempts()))
    77  		for i, attempt := range execution.GetAttempts() {
    78  			// attempts in execution are sorted with earliest first and the response
    79  			// need to be sorted with latest first.
    80  			var err error
    81  			ti.Attempts[len(execution.GetAttempts())-1-i], err = makeTryjobAttempt(attempt, definition)
    82  			if err != nil {
    83  				return nil, err
    84  			}
    85  		}
    86  		ti.Status = ti.Attempts[0].GetStatus()
    87  		ret = append(ret, ti)
    88  	}
    89  	return ret, nil
    90  }
    91  
    92  func computeBuilderByName(cg *prjcfg.ConfigGroup) map[string]*cfgpb.Verifiers_Tryjob_Builder {
    93  	ret := make(map[string]*cfgpb.Verifiers_Tryjob_Builder, len(cg.Content.GetVerifiers().GetTryjob().GetBuilders()))
    94  	for _, b := range cg.Content.GetVerifiers().GetTryjob().GetBuilders() {
    95  		ret[b.GetName()] = b
    96  		if equiName := b.GetEquivalentTo().GetName(); equiName != "" {
    97  			ret[equiName] = b // map equivalent builder name to this builder too.
    98  		}
    99  	}
   100  	return ret
   101  }
   102  
   103  func makeTryjobAttempt(attempt *tryjob.ExecutionState_Execution_Attempt, def *tryjob.Definition) (*apiv0pb.TryjobInvocation_Attempt, error) {
   104  	ret := &apiv0pb.TryjobInvocation_Attempt{}
   105  	if attempt.GetResult() != nil {
   106  		switch attempt.GetResult().GetBackend().(type) {
   107  		case *tryjob.Result_Buildbucket_:
   108  			bbResult := attempt.GetResult().GetBuildbucket()
   109  			ret.Result = &apiv0pb.TryjobResult{
   110  				Backend: &apiv0pb.TryjobResult_Buildbucket_{
   111  					Buildbucket: &apiv0pb.TryjobResult_Buildbucket{
   112  						Host:    def.GetBuildbucket().GetHost(),
   113  						Id:      bbResult.GetId(),
   114  						Builder: bbResult.GetBuilder(),
   115  					},
   116  				},
   117  			}
   118  		default:
   119  			return nil, appstatus.Errorf(codes.Unimplemented, "unknown tryjob backend %T", attempt.GetResult().GetBackend())
   120  		}
   121  	}
   122  	ret.Reuse = attempt.GetReused()
   123  	ret.Status = versioning.TryjobStatusV0(attempt.GetStatus(), attempt.Result.GetStatus())
   124  	return ret, nil
   125  }