go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/invocation.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 "time" 20 21 "cloud.google.com/go/spanner" 22 "google.golang.org/grpc/codes" 23 "google.golang.org/grpc/metadata" 24 25 "go.chromium.org/luci/common/clock" 26 "go.chromium.org/luci/common/data/rand/mathrand" 27 "go.chromium.org/luci/grpc/appstatus" 28 "go.chromium.org/luci/server/span" 29 "go.chromium.org/luci/server/tokens" 30 31 "go.chromium.org/luci/resultdb/internal/invocations" 32 "go.chromium.org/luci/resultdb/internal/spanutil" 33 "go.chromium.org/luci/resultdb/pbutil" 34 pb "go.chromium.org/luci/resultdb/proto/v1" 35 ) 36 37 const ( 38 day = 24 * time.Hour 39 40 // Delete Invocations row after this duration since invocation creation. 41 invocationExpirationDuration = 2 * 365 * day // 2 y 42 43 // By default, finalize the invocation 2d after creation if it is still 44 // incomplete. 45 defaultInvocationDeadlineDuration = 2 * day 46 47 // The maximum amount of time for an invocation. 48 // This is the same as the BUILD_TIMEOUT 49 // https://source.chromium.org/chromium/infra/infra/+/main:appengine/cr-buildbucket/model.py;l=28;drc=e6d97dc362dd4a412fc7b07da0c4df53f2940a80 50 // https://source.chromium.org/chromium/infra/infra/+/main:go/src/go.chromium.org/luci/buildbucket/appengine/model/build.go;l=53 51 maxInvocationDeadlineDuration = 5 * day 52 ) 53 54 // invocationTokenKind generates and validates tokens issued to authorize 55 // updating a given invocation. 56 var invocationTokenKind = tokens.TokenKind{ 57 Algo: tokens.TokenAlgoHmacSHA256, 58 SecretKey: "invocation_tokens_secret", 59 Version: 1, 60 } 61 62 // generateInvocationToken generates an update token for a given invocation. 63 func generateInvocationToken(ctx context.Context, invID invocations.ID) (string, error) { 64 // The token should last as long as a build is allowed to run. 65 // Buildbucket has a max of 2 days, so one week should be enough even 66 // for other use cases. 67 return invocationTokenKind.Generate(ctx, []byte(invID), nil, 7*day) // One week. 68 } 69 70 // validateInvocationToken validates an update token for a given invocation, 71 // returning an error if the token is invalid, nil otherwise. 72 func validateInvocationToken(ctx context.Context, token string, invID invocations.ID) error { 73 _, err := invocationTokenKind.Validate(ctx, token, []byte(invID)) 74 return err 75 } 76 77 // mutateInvocation checks if the invocation can be mutated and also 78 // finalizes the invocation if it's deadline is exceeded. 79 // If the invocation is active, continue with the other mutation(s) in f. 80 func mutateInvocation(ctx context.Context, id invocations.ID, f func(context.Context) error) error { 81 var retErr error 82 83 token, err := extractUpdateToken(ctx) 84 if err != nil { 85 return err 86 } 87 if err := validateInvocationToken(ctx, token, id); err != nil { 88 return appstatus.Errorf(codes.PermissionDenied, "invalid update token") 89 } 90 _, err = span.ReadWriteTransaction(ctx, func(ctx context.Context) error { 91 state, err := invocations.ReadState(ctx, id) 92 switch { 93 case err != nil: 94 return err 95 96 case state != pb.Invocation_ACTIVE: 97 return appstatus.Errorf(codes.FailedPrecondition, "%s is not active", id.Name()) 98 } 99 100 return f(ctx) 101 }) 102 103 if err != nil { 104 retErr = err 105 } 106 return retErr 107 } 108 109 func extractUpdateToken(ctx context.Context) (string, error) { 110 md, _ := metadata.FromIncomingContext(ctx) 111 token := md.Get(pb.UpdateTokenMetadataKey) 112 switch { 113 case len(token) == 0: 114 return "", appstatus.Errorf(codes.Unauthenticated, "missing %s metadata value in the request", pb.UpdateTokenMetadataKey) 115 116 case len(token) > 1: 117 return "", appstatus.Errorf(codes.InvalidArgument, "expected exactly one %s metadata value, got %d", pb.UpdateTokenMetadataKey, len(token)) 118 119 default: 120 return token[0], nil 121 } 122 } 123 124 // rowOfInvocation returns Invocation row values to be inserted to create the 125 // invocation. 126 // inv.CreateTime is ignored in favor of spanner.CommitTime. 127 func (s *recorderServer) rowOfInvocation(ctx context.Context, inv *pb.Invocation, createRequestID string) map[string]any { 128 now := clock.Now(ctx).UTC() 129 row := map[string]any{ 130 "InvocationId": invocations.MustParseName(inv.Name), 131 "ShardId": mathrand.Intn(ctx, invocations.Shards), 132 "State": inv.State, 133 "Realm": inv.Realm, 134 "CreatedBy": inv.CreatedBy, 135 136 "InvocationExpirationTime": now.Add(invocationExpirationDuration), 137 "ExpectedTestResultsExpirationTime": now.Add(s.ExpectedResultsExpiration), 138 139 "CreateTime": spanner.CommitTimestamp, 140 "Deadline": inv.Deadline, 141 142 "Tags": inv.Tags, 143 "ProducerResource": inv.ProducerResource, 144 "BigQueryExports": inv.BigqueryExports, 145 "Properties": spanutil.Compressed(pbutil.MustMarshal(inv.Properties)), 146 "InheritSources": spanner.NullBool{Valid: inv.SourceSpec != nil, Bool: inv.SourceSpec.GetInherit()}, 147 "Sources": spanutil.Compressed(pbutil.MustMarshal(inv.SourceSpec.GetSources())), 148 "BaselineId": inv.BaselineId, 149 } 150 151 if inv.State == pb.Invocation_FINALIZED { 152 // We are ignoring the provided inv.FinalizeTime because it would not 153 // make sense to have an invocation finalized before it was created, 154 // yet attempting to set this in the future would fail the sql schema 155 // restriction for columns that allow commit timestamp. 156 // Note this function is only used for setting FinalizeTime by derive 157 // invocation, which is planned to be superseded by other mechanisms. 158 row["FinalizeTime"] = spanner.CommitTimestamp 159 } 160 161 if createRequestID != "" { 162 row["CreateRequestId"] = createRequestID 163 } 164 165 return row 166 }