go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/internal/resultdb/resultdb_test.go (about)

     1  // Copyright 2021 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 resultdb
    16  
    17  import (
    18  	"context"
    19  	"crypto/sha256"
    20  	"encoding/hex"
    21  	"fmt"
    22  	"testing"
    23  
    24  	"github.com/golang/mock/gomock"
    25  	"go.chromium.org/luci/gae/impl/memory"
    26  	"google.golang.org/grpc"
    27  	"google.golang.org/grpc/codes"
    28  	"google.golang.org/grpc/metadata"
    29  	grpcStatus "google.golang.org/grpc/status"
    30  	"google.golang.org/protobuf/types/known/durationpb"
    31  	"google.golang.org/protobuf/types/known/timestamppb"
    32  
    33  	"go.chromium.org/luci/common/clock/testclock"
    34  	"go.chromium.org/luci/common/proto"
    35  	"go.chromium.org/luci/common/retry/transient"
    36  	"go.chromium.org/luci/gae/service/datastore"
    37  	rdbPb "go.chromium.org/luci/resultdb/proto/v1"
    38  	"go.chromium.org/luci/server/tq"
    39  
    40  	"go.chromium.org/luci/buildbucket/appengine/model"
    41  	pb "go.chromium.org/luci/buildbucket/proto"
    42  
    43  	. "github.com/smartystreets/goconvey/convey"
    44  	. "go.chromium.org/luci/common/testing/assertions"
    45  )
    46  
    47  func TestCreateInvocations(t *testing.T) {
    48  	t.Parallel()
    49  
    50  	Convey("create invocations", t, func() {
    51  		ctl := gomock.NewController(t)
    52  		defer ctl.Finish()
    53  		mockClient := rdbPb.NewMockRecorderClient(ctl)
    54  		ctx := SetMockRecorder(context.Background(), mockClient)
    55  		ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
    56  		ctx = memory.UseInfo(ctx, "cr-buildbucket-dev")
    57  
    58  		bqExports := []*rdbPb.BigQueryExport{}
    59  
    60  		Convey("builds without number", func() {
    61  			builds := []*model.Build{
    62  				{
    63  					ID: 1,
    64  					Proto: &pb.Build{
    65  						Id: 1,
    66  						Builder: &pb.BuilderID{
    67  							Project: "proj1",
    68  							Bucket:  "bucket",
    69  							Builder: "builder",
    70  						},
    71  						Infra: &pb.BuildInfra{
    72  							Resultdb: &pb.BuildInfra_ResultDB{
    73  								Hostname:  "host",
    74  								Enable:    true,
    75  								BqExports: bqExports,
    76  							},
    77  						},
    78  					},
    79  				},
    80  				{
    81  					ID: 2,
    82  					Proto: &pb.Build{
    83  						Id: 2,
    84  						Builder: &pb.BuilderID{
    85  							Project: "proj1",
    86  							Bucket:  "bucket",
    87  							Builder: "builder",
    88  						},
    89  						Infra: &pb.BuildInfra{
    90  							Resultdb: &pb.BuildInfra_ResultDB{
    91  								Enable:    true,
    92  								BqExports: bqExports,
    93  							},
    94  						},
    95  					},
    96  				},
    97  			}
    98  
    99  			mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual(
   100  				&rdbPb.CreateInvocationRequest{
   101  					InvocationId: "build-1",
   102  					Invocation: &rdbPb.Invocation{
   103  						Deadline:         timestamppb.New(testclock.TestRecentTimeUTC),
   104  						BigqueryExports:  bqExports,
   105  						ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/1",
   106  						Realm:            "proj1:bucket",
   107  					},
   108  					RequestId: "build-1",
   109  				}), gomock.Any()).DoAndReturn(func(ctx context.Context, in *rdbPb.CreateInvocationRequest, opt grpc.CallOption) (*rdbPb.Invocation, error) {
   110  				h, _ := opt.(grpc.HeaderCallOption)
   111  				h.HeaderAddr.Set("update-token", "token for build-1")
   112  				return &rdbPb.Invocation{}, nil
   113  			})
   114  			mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual(
   115  				&rdbPb.CreateInvocationRequest{
   116  					InvocationId: "build-2",
   117  					Invocation: &rdbPb.Invocation{
   118  						Deadline:         timestamppb.New(testclock.TestRecentTimeUTC),
   119  						BigqueryExports:  bqExports,
   120  						ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/2",
   121  						Realm:            "proj1:bucket",
   122  					},
   123  					RequestId: "build-2",
   124  				}), gomock.Any()).DoAndReturn(func(ctx context.Context, in *rdbPb.CreateInvocationRequest, opt grpc.CallOption) (*rdbPb.Invocation, error) {
   125  				h, _ := opt.(grpc.HeaderCallOption)
   126  				h.HeaderAddr.Set("update-token", "token for build-2")
   127  				return &rdbPb.Invocation{}, nil
   128  			})
   129  
   130  			err := CreateInvocations(ctx, builds)
   131  			So(err, ShouldBeNil)
   132  			So(builds[0].ResultDBUpdateToken, ShouldEqual, "token for build-1")
   133  			So(builds[0].Proto.Infra.Resultdb.Invocation, ShouldEqual, "invocations/build-1")
   134  			So(builds[1].ResultDBUpdateToken, ShouldEqual, "token for build-2")
   135  			So(builds[1].Proto.Infra.Resultdb.Invocation, ShouldEqual, "invocations/build-2")
   136  		})
   137  
   138  		Convey("build with number and expirations", func() {
   139  			builds := []*model.Build{
   140  				{
   141  					ID: 1,
   142  					Proto: &pb.Build{
   143  						Id:     1,
   144  						Number: 123,
   145  						Builder: &pb.BuilderID{
   146  							Project: "proj1",
   147  							Bucket:  "bucket",
   148  							Builder: "builder",
   149  						},
   150  						Infra: &pb.BuildInfra{
   151  							Resultdb: &pb.BuildInfra_ResultDB{
   152  								Hostname:  "host",
   153  								Enable:    true,
   154  								BqExports: bqExports,
   155  							},
   156  						},
   157  						ExecutionTimeout:  durationpb.New(1000),
   158  						SchedulingTimeout: durationpb.New(1000),
   159  					},
   160  				},
   161  			}
   162  
   163  			deadline := testclock.TestRecentTimeUTC.Add(2000)
   164  			sha256Bldr := sha256.Sum256([]byte("proj1/bucket/builder"))
   165  			mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual(
   166  				&rdbPb.CreateInvocationRequest{
   167  					InvocationId: "build-1",
   168  					Invocation: &rdbPb.Invocation{
   169  						Deadline:         timestamppb.New(deadline),
   170  						BigqueryExports:  bqExports,
   171  						ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/1",
   172  						Realm:            "proj1:bucket",
   173  					},
   174  					RequestId: "build-1",
   175  				}), gomock.Any()).DoAndReturn(func(ctx context.Context, in *rdbPb.CreateInvocationRequest, opt grpc.CallOption) (*rdbPb.Invocation, error) {
   176  				h, _ := opt.(grpc.HeaderCallOption)
   177  				h.HeaderAddr.Set("update-token", "token for build id 1")
   178  				return &rdbPb.Invocation{}, nil
   179  			})
   180  			mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual(
   181  				&rdbPb.CreateInvocationRequest{
   182  					InvocationId: fmt.Sprintf("build-%s-123", hex.EncodeToString(sha256Bldr[:])),
   183  					Invocation: &rdbPb.Invocation{
   184  						IncludedInvocations: []string{"invocations/build-1"},
   185  						ProducerResource:    "//cr-buildbucket-dev.appspot.com/builds/1",
   186  						State:               rdbPb.Invocation_FINALIZING,
   187  						Realm:               "proj1:bucket",
   188  					},
   189  					RequestId: "build-1-123",
   190  				})).Return(&rdbPb.Invocation{}, nil)
   191  
   192  			err := CreateInvocations(ctx, builds)
   193  			So(err, ShouldBeNil)
   194  			So(len(builds), ShouldEqual, 1)
   195  			So(builds[0].ResultDBUpdateToken, ShouldEqual, "token for build id 1")
   196  			So(builds[0].Proto.Infra.Resultdb.Invocation, ShouldEqual, "invocations/build-1")
   197  		})
   198  
   199  		Convey("already exists error", func() {
   200  			builds := []*model.Build{
   201  				{
   202  					ID: 1,
   203  					Proto: &pb.Build{
   204  						Id:     1,
   205  						Number: 123,
   206  						Builder: &pb.BuilderID{
   207  							Project: "proj1",
   208  							Bucket:  "bucket",
   209  							Builder: "builder",
   210  						},
   211  						Infra: &pb.BuildInfra{
   212  							Resultdb: &pb.BuildInfra_ResultDB{
   213  								Hostname:  "host",
   214  								Enable:    true,
   215  								BqExports: bqExports,
   216  							},
   217  						},
   218  					},
   219  				},
   220  			}
   221  
   222  			mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual(
   223  				&rdbPb.CreateInvocationRequest{
   224  					InvocationId: "build-1",
   225  					Invocation: &rdbPb.Invocation{
   226  						Deadline:         timestamppb.New(testclock.TestRecentTimeUTC),
   227  						BigqueryExports:  bqExports,
   228  						ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/1",
   229  						Realm:            "proj1:bucket",
   230  					},
   231  					RequestId: "build-1",
   232  				}), gomock.Any()).Return(nil, grpcStatus.Error(codes.AlreadyExists, "already exists"))
   233  
   234  			err := CreateInvocations(ctx, builds)
   235  			So(err, ShouldErrLike, "failed to create the invocation for build id: 1: rpc error: code = AlreadyExists desc = already exists")
   236  		})
   237  
   238  		Convey("resultDB throws err", func() {
   239  			builds := []*model.Build{
   240  				{
   241  					ID: 1,
   242  					Proto: &pb.Build{
   243  						Id: 1,
   244  						Builder: &pb.BuilderID{
   245  							Project: "proj1",
   246  							Bucket:  "bucket",
   247  							Builder: "builder",
   248  						},
   249  						Infra: &pb.BuildInfra{
   250  							Resultdb: &pb.BuildInfra_ResultDB{
   251  								Hostname:  "host",
   252  								Enable:    true,
   253  								BqExports: bqExports,
   254  							},
   255  						},
   256  					},
   257  				},
   258  			}
   259  
   260  			mockClient.EXPECT().CreateInvocation(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, grpcStatus.Error(codes.DeadlineExceeded, "timeout"))
   261  
   262  			err := CreateInvocations(ctx, builds)
   263  			So(err, ShouldErrLike, "failed to create the invocation for build id: 1: rpc error: code = DeadlineExceeded desc = timeout")
   264  		})
   265  
   266  		Convey("partial success", func() {
   267  			builds := []*model.Build{
   268  				{
   269  					ID: 1,
   270  					Proto: &pb.Build{
   271  						Id: 1,
   272  						Builder: &pb.BuilderID{
   273  							Project: "proj1",
   274  							Bucket:  "bucket",
   275  							Builder: "builder",
   276  						},
   277  						Infra: &pb.BuildInfra{
   278  							Resultdb: &pb.BuildInfra_ResultDB{
   279  								Hostname:  "host",
   280  								Enable:    true,
   281  								BqExports: bqExports,
   282  							},
   283  						},
   284  					},
   285  				},
   286  				{
   287  					ID: 2,
   288  					Proto: &pb.Build{
   289  						Id: 2,
   290  						Builder: &pb.BuilderID{
   291  							Project: "proj1",
   292  							Bucket:  "bucket",
   293  							Builder: "builder",
   294  						},
   295  						Infra: &pb.BuildInfra{
   296  							Resultdb: &pb.BuildInfra_ResultDB{
   297  								Hostname:  "host",
   298  								Enable:    true,
   299  								BqExports: bqExports,
   300  							},
   301  						},
   302  					},
   303  				},
   304  			}
   305  
   306  			mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual(
   307  				&rdbPb.CreateInvocationRequest{
   308  					InvocationId: "build-1",
   309  					Invocation: &rdbPb.Invocation{
   310  						Deadline:         timestamppb.New(testclock.TestRecentTimeUTC),
   311  						BigqueryExports:  bqExports,
   312  						ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/1",
   313  						Realm:            "proj1:bucket",
   314  					},
   315  					RequestId: "build-1",
   316  				}), gomock.Any()).Return(nil, grpcStatus.Error(codes.Internal, "error"))
   317  			mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual(
   318  				&rdbPb.CreateInvocationRequest{
   319  					InvocationId: "build-2",
   320  					Invocation: &rdbPb.Invocation{
   321  						Deadline:         timestamppb.New(testclock.TestRecentTimeUTC),
   322  						BigqueryExports:  bqExports,
   323  						ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/2",
   324  						Realm:            "proj1:bucket",
   325  					},
   326  					RequestId: "build-2",
   327  				}), gomock.Any()).DoAndReturn(func(ctx context.Context, in *rdbPb.CreateInvocationRequest, opt grpc.CallOption) (*rdbPb.Invocation, error) {
   328  				h, _ := opt.(grpc.HeaderCallOption)
   329  				h.HeaderAddr.Set("update-token", "update token")
   330  				return &rdbPb.Invocation{}, nil
   331  			})
   332  
   333  			err := CreateInvocations(ctx, builds)
   334  			So(err[0], ShouldErrLike, "failed to create the invocation for build id: 1: rpc error: code = Internal desc = error")
   335  			So(err[1], ShouldBeNil)
   336  			So(builds[0].ResultDBUpdateToken, ShouldEqual, "")
   337  			So(builds[0].Proto.Infra.Resultdb.Invocation, ShouldEqual, "")
   338  			So(builds[1].ResultDBUpdateToken, ShouldEqual, "update token")
   339  			So(builds[1].Proto.Infra.Resultdb.Invocation, ShouldEqual, "invocations/build-2")
   340  		})
   341  
   342  		Convey("resultDB not enabled", func() {
   343  			builds := []*model.Build{
   344  				{
   345  					ID: 1,
   346  					Proto: &pb.Build{
   347  						Id: 1,
   348  						Builder: &pb.BuilderID{
   349  							Project: "proj1",
   350  							Bucket:  "bucket",
   351  							Builder: "builder",
   352  						},
   353  						Infra: &pb.BuildInfra{Resultdb: &pb.BuildInfra_ResultDB{
   354  							Hostname: "host",
   355  							Enable:   false,
   356  						}},
   357  					},
   358  				},
   359  			}
   360  
   361  			err := CreateInvocations(ctx, builds)
   362  			So(err, ShouldBeNil)
   363  			So(builds[0].Proto.Infra.Resultdb.Invocation, ShouldEqual, "")
   364  		})
   365  	})
   366  }
   367  
   368  func TestFinalizeInvocation(t *testing.T) {
   369  	t.Parallel()
   370  
   371  	Convey("finalize invocations", t, func() {
   372  		ctl := gomock.NewController(t)
   373  		defer ctl.Finish()
   374  		mockClient := rdbPb.NewMockRecorderClient(ctl)
   375  		ctx := memory.Use(context.Background())
   376  		ctx = SetMockRecorder(ctx, mockClient)
   377  		datastore.GetTestable(ctx).AutoIndex(true)
   378  		datastore.GetTestable(ctx).Consistent(true)
   379  
   380  		So(datastore.Put(ctx, &model.Build{
   381  			ID:                  1,
   382  			Project:             "project",
   383  			BucketID:            "bucket",
   384  			BuilderID:           "builder",
   385  			ResultDBUpdateToken: "token",
   386  			Proto: &pb.Build{
   387  				Id: 1,
   388  				Builder: &pb.BuilderID{
   389  					Project: "project",
   390  					Bucket:  "bucket",
   391  					Builder: "builder",
   392  				},
   393  				Status: pb.Status_SUCCESS,
   394  			},
   395  		}), ShouldBeNil)
   396  
   397  		Convey("no exists", func() {
   398  			So(FinalizeInvocation(ctx, 1), ShouldErrLike, "build 1 or buildInfra not found")
   399  		})
   400  
   401  		Convey("no resultdb hostname", func() {
   402  			So(datastore.Put(ctx, &model.BuildInfra{
   403  				ID:    1,
   404  				Build: datastore.KeyForObj(ctx, &model.Build{ID: 1}),
   405  				Proto: &pb.BuildInfra{
   406  					Resultdb: &pb.BuildInfra_ResultDB{
   407  						Invocation: "invocation",
   408  					},
   409  				},
   410  			}), ShouldBeNil)
   411  
   412  			mockClient.EXPECT().FinalizeInvocation(gomock.Any(), gomock.Any()).Times(0)
   413  			So(FinalizeInvocation(ctx, 1), ShouldBeNil)
   414  		})
   415  
   416  		Convey("no invocation", func() {
   417  			So(datastore.Put(ctx, &model.BuildInfra{
   418  				ID:    1,
   419  				Build: datastore.KeyForObj(ctx, &model.Build{ID: 1}),
   420  				Proto: &pb.BuildInfra{
   421  					Resultdb: &pb.BuildInfra_ResultDB{
   422  						Hostname: "hostname",
   423  					},
   424  				},
   425  			}), ShouldBeNil)
   426  
   427  			mockClient.EXPECT().FinalizeInvocation(gomock.Any(), gomock.Any()).Times(0)
   428  			So(FinalizeInvocation(ctx, 1), ShouldBeNil)
   429  		})
   430  
   431  		Convey("success", func() {
   432  			So(datastore.Put(ctx, &model.BuildInfra{
   433  				ID:    1,
   434  				Build: datastore.KeyForObj(ctx, &model.Build{ID: 1}),
   435  				Proto: &pb.BuildInfra{
   436  					Resultdb: &pb.BuildInfra_ResultDB{
   437  						Hostname:   "hostname",
   438  						Invocation: "invocation",
   439  					},
   440  				},
   441  			}), ShouldBeNil)
   442  
   443  			expectedCtx := metadata.AppendToOutgoingContext(ctx, "update-token", "token")
   444  			mockClient.EXPECT().FinalizeInvocation(expectedCtx, proto.MatcherEqual(&rdbPb.FinalizeInvocationRequest{
   445  				Name: "invocation",
   446  			})).Return(&rdbPb.Invocation{}, nil)
   447  
   448  			So(FinalizeInvocation(ctx, 1), ShouldBeNil)
   449  		})
   450  
   451  		Convey("resultDB server fatal err", func() {
   452  			So(datastore.Put(ctx, &model.BuildInfra{
   453  				ID:    1,
   454  				Build: datastore.KeyForObj(ctx, &model.Build{ID: 1}),
   455  				Proto: &pb.BuildInfra{
   456  					Resultdb: &pb.BuildInfra_ResultDB{
   457  						Hostname:   "hostname",
   458  						Invocation: "invocation",
   459  					},
   460  				},
   461  			}), ShouldBeNil)
   462  
   463  			mockClient.EXPECT().FinalizeInvocation(gomock.Any(), proto.MatcherEqual(&rdbPb.FinalizeInvocationRequest{
   464  				Name: "invocation",
   465  			})).Return(nil, grpcStatus.Error(codes.PermissionDenied, "permission denied"))
   466  
   467  			err := FinalizeInvocation(ctx, 1)
   468  			So(err, ShouldNotBeNil)
   469  			So(tq.Fatal.In(err), ShouldBeTrue)
   470  		})
   471  
   472  		Convey("resultDB server retryable err", func() {
   473  			So(datastore.Put(ctx, &model.BuildInfra{
   474  				ID:    1,
   475  				Build: datastore.KeyForObj(ctx, &model.Build{ID: 1}),
   476  				Proto: &pb.BuildInfra{
   477  					Resultdb: &pb.BuildInfra_ResultDB{
   478  						Hostname:   "hostname",
   479  						Invocation: "invocation",
   480  					},
   481  				},
   482  			}), ShouldBeNil)
   483  
   484  			mockClient.EXPECT().FinalizeInvocation(gomock.Any(), proto.MatcherEqual(&rdbPb.FinalizeInvocationRequest{
   485  				Name: "invocation",
   486  			})).Return(nil, grpcStatus.Error(codes.Internal, "internal error"))
   487  
   488  			err := FinalizeInvocation(ctx, 1)
   489  			So(err, ShouldNotBeNil)
   490  			So(transient.Tag.In(err), ShouldBeTrue)
   491  		})
   492  	})
   493  }