go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/create_test_exoneration.go (about)

     1  // Copyright 2019 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 recorder
    16  
    17  import (
    18  	"context"
    19  	"crypto/sha512"
    20  	"encoding/hex"
    21  	"fmt"
    22  
    23  	"cloud.google.com/go/spanner"
    24  	"github.com/google/uuid"
    25  
    26  	"go.chromium.org/luci/common/errors"
    27  	"go.chromium.org/luci/grpc/appstatus"
    28  	"go.chromium.org/luci/resultdb/internal/invocations"
    29  	"go.chromium.org/luci/resultdb/internal/spanutil"
    30  	"go.chromium.org/luci/resultdb/pbutil"
    31  	pb "go.chromium.org/luci/resultdb/proto/v1"
    32  	"go.chromium.org/luci/server/auth"
    33  	"go.chromium.org/luci/server/span"
    34  )
    35  
    36  // validateCreateTestExonerationRequest returns a non-nil error if req is invalid.
    37  func validateCreateTestExonerationRequest(req *pb.CreateTestExonerationRequest, requireInvocation bool) error {
    38  	if requireInvocation || req.Invocation != "" {
    39  		if err := pbutil.ValidateInvocationName(req.Invocation); err != nil {
    40  			return errors.Annotate(err, "invocation").Err()
    41  		}
    42  	}
    43  
    44  	ex := req.GetTestExoneration()
    45  	if err := pbutil.ValidateTestID(ex.GetTestId()); err != nil {
    46  		return errors.Annotate(err, "test_exoneration: test_id").Err()
    47  	}
    48  	if err := pbutil.ValidateVariant(ex.GetVariant()); err != nil {
    49  		return errors.Annotate(err, "test_exoneration: variant").Err()
    50  	}
    51  
    52  	hasVariant := len(ex.GetVariant().GetDef()) != 0
    53  	hasVariantHash := ex.VariantHash != ""
    54  	if hasVariant && hasVariantHash {
    55  		computedHash := pbutil.VariantHash(ex.GetVariant())
    56  		if computedHash != ex.VariantHash {
    57  			return errors.Reason("computed and supplied variant hash don't match").Err()
    58  		}
    59  	}
    60  
    61  	if err := pbutil.ValidateRequestID(req.RequestId); err != nil {
    62  		return errors.Annotate(err, "request_id").Err()
    63  	}
    64  
    65  	if ex.ExplanationHtml == "" {
    66  		return errors.Reason("test_exoneration: explanation_html: unspecified").Err()
    67  	}
    68  	if ex.Reason == pb.ExonerationReason_EXONERATION_REASON_UNSPECIFIED {
    69  		return errors.Reason("test_exoneration: reason: unspecified").Err()
    70  	}
    71  	return nil
    72  }
    73  
    74  // CreateTestExoneration implements pb.RecorderServer.
    75  func (s *recorderServer) CreateTestExoneration(ctx context.Context, in *pb.CreateTestExonerationRequest) (*pb.TestExoneration, error) {
    76  	if err := validateCreateTestExonerationRequest(in, true); err != nil {
    77  		return nil, appstatus.BadRequest(err)
    78  	}
    79  	invID := invocations.MustParseName(in.Invocation)
    80  
    81  	ret, mutation := insertTestExoneration(ctx, invID, in.RequestId, 0, in.TestExoneration)
    82  	err := mutateInvocation(ctx, invID, func(ctx context.Context) error {
    83  		span.BufferWrite(ctx, mutation)
    84  		return nil
    85  	})
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	return ret, nil
    90  }
    91  
    92  func insertTestExoneration(ctx context.Context, invID invocations.ID, requestID string, ordinal int, body *pb.TestExoneration) (ret *pb.TestExoneration, mutation *spanner.Mutation) {
    93  	// Compute exoneration ID and choose Insert vs InsertOrUpdate.
    94  	var exonerationIDSuffix string
    95  	mutFn := spanner.InsertMap
    96  	if requestID == "" {
    97  		// Use a random id.
    98  		exonerationIDSuffix = "r:" + uuid.New().String()
    99  	} else {
   100  		// Use a deterministic id.
   101  		exonerationIDSuffix = "d:" + deterministicExonerationIDSuffix(ctx, requestID, ordinal)
   102  		mutFn = spanner.InsertOrUpdateMap
   103  	}
   104  
   105  	// Use the given variant hash, or the hash of the given variant, whichever
   106  	// is present. If both are present then validation guarantees they'll
   107  	// match, so we can just use whichever.
   108  	variantHash := body.VariantHash
   109  	if variantHash == "" {
   110  		variantHash = pbutil.VariantHash(body.Variant)
   111  	}
   112  
   113  	exonerationID := fmt.Sprintf("%s:%s", variantHash, exonerationIDSuffix)
   114  	ret = &pb.TestExoneration{
   115  		Name:            pbutil.TestExonerationName(string(invID), body.TestId, exonerationID),
   116  		TestId:          body.TestId,
   117  		Variant:         body.Variant,
   118  		VariantHash:     variantHash,
   119  		ExonerationId:   exonerationID,
   120  		ExplanationHtml: body.ExplanationHtml,
   121  		Reason:          body.Reason,
   122  	}
   123  
   124  	mutation = mutFn("TestExonerations", spanutil.ToSpannerMap(map[string]any{
   125  		"InvocationId":    invID,
   126  		"TestId":          ret.TestId,
   127  		"ExonerationId":   exonerationID,
   128  		"Variant":         ret.Variant,
   129  		"VariantHash":     ret.VariantHash,
   130  		"ExplanationHTML": spanutil.Compressed(ret.ExplanationHtml),
   131  		"Reason":          ret.Reason,
   132  	}))
   133  	return
   134  }
   135  
   136  func deterministicExonerationIDSuffix(ctx context.Context, requestID string, ordinal int) string {
   137  	h := sha512.New()
   138  	// Include current identity, so that two separate clients
   139  	// do not override each other's test exonerations even if
   140  	// they happened to produce identical request ids.
   141  	// The alternative is to use remote IP address, but it is not
   142  	// implemented in pRPC.
   143  	fmt.Fprintln(h, auth.CurrentIdentity(ctx))
   144  	fmt.Fprintln(h, requestID)
   145  	fmt.Fprintln(h, ordinal)
   146  	return hex.EncodeToString(h.Sum(nil))
   147  }