go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/start_build_test.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  	"testing"
    20  
    21  	"google.golang.org/grpc/metadata"
    22  
    23  	"go.chromium.org/luci/gae/filter/txndefer"
    24  	"go.chromium.org/luci/gae/impl/memory"
    25  	"go.chromium.org/luci/gae/service/datastore"
    26  	"go.chromium.org/luci/server/secrets"
    27  	"go.chromium.org/luci/server/secrets/testsecrets"
    28  	"go.chromium.org/luci/server/tq"
    29  	"go.chromium.org/luci/server/tq/tqtesting"
    30  
    31  	"go.chromium.org/luci/buildbucket"
    32  	"go.chromium.org/luci/buildbucket/appengine/common"
    33  	"go.chromium.org/luci/buildbucket/appengine/internal/buildtoken"
    34  	"go.chromium.org/luci/buildbucket/appengine/internal/metrics"
    35  	"go.chromium.org/luci/buildbucket/appengine/model"
    36  	pb "go.chromium.org/luci/buildbucket/proto"
    37  
    38  	. "github.com/smartystreets/goconvey/convey"
    39  	. "go.chromium.org/luci/common/testing/assertions"
    40  )
    41  
    42  func validStartBuildRequest() *pb.StartBuildRequest {
    43  	return &pb.StartBuildRequest{
    44  		RequestId: "random",
    45  		BuildId:   87654321,
    46  		TaskId:    "deadbeef",
    47  	}
    48  }
    49  
    50  func TestValidateStartBuildRequest(t *testing.T) {
    51  	t.Parallel()
    52  	Convey("validateStartBuildRequest", t, func() {
    53  		ctx := context.Background()
    54  
    55  		Convey("empty req", func() {
    56  			err := validateStartBuildRequest(ctx, &pb.StartBuildRequest{})
    57  			So(err, ShouldErrLike, `.request_id: required`)
    58  		})
    59  
    60  		Convey("missing build id", func() {
    61  			req := &pb.StartBuildRequest{
    62  				RequestId: "random",
    63  			}
    64  			err := validateStartBuildRequest(ctx, req)
    65  			So(err, ShouldErrLike, `.build_id: required`)
    66  		})
    67  
    68  		Convey("missing task id", func() {
    69  			req := &pb.StartBuildRequest{
    70  				RequestId: "random",
    71  				BuildId:   87654321,
    72  			}
    73  			err := validateStartBuildRequest(ctx, req)
    74  			So(err, ShouldErrLike, `.task_id: required`)
    75  		})
    76  
    77  		Convey("pass", func() {
    78  			req := validStartBuildRequest()
    79  			err := validateStartBuildRequest(ctx, req)
    80  			So(err, ShouldBeNil)
    81  		})
    82  	})
    83  }
    84  
    85  func TestStartBuild(t *testing.T) {
    86  	srv := &Builds{}
    87  	ctx := memory.Use(context.Background())
    88  	store := &testsecrets.Store{
    89  		Secrets: map[string]secrets.Secret{
    90  			"key": {Active: []byte("stuff")},
    91  		},
    92  	}
    93  	ctx = secrets.Use(ctx, store)
    94  	ctx = secrets.GeneratePrimaryTinkAEADForTest(ctx)
    95  
    96  	req := validStartBuildRequest()
    97  	Convey("validate token", t, func() {
    98  		Convey("token missing", func() {
    99  			_, err := srv.StartBuild(ctx, req)
   100  			So(err, ShouldErrLike, errBadTokenAuth)
   101  		})
   102  
   103  		Convey("wrong purpose", func() {
   104  			tk, err := buildtoken.GenerateToken(ctx, 87654321, pb.TokenBody_TASK)
   105  			So(err, ShouldBeNil)
   106  			ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, tk))
   107  			_, err = srv.StartBuild(ctx, req)
   108  			So(err, ShouldErrLike, buildtoken.ErrBadToken)
   109  		})
   110  
   111  		Convey("wrong build id", func() {
   112  			tk, _ := buildtoken.GenerateToken(ctx, 1, pb.TokenBody_START_BUILD)
   113  			ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, tk))
   114  			_, err := srv.StartBuild(ctx, req)
   115  			So(err, ShouldErrLike, buildtoken.ErrBadToken)
   116  		})
   117  	})
   118  
   119  	Convey("StartBuild", t, func() {
   120  		ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
   121  		ctx = txndefer.FilterRDS(ctx)
   122  		var sch *tqtesting.Scheduler
   123  		ctx, sch = tq.TestingContext(ctx, nil)
   124  
   125  		build := &model.Build{
   126  			ID: 87654321,
   127  			Proto: &pb.Build{
   128  				Id: 87654321,
   129  				Builder: &pb.BuilderID{
   130  					Project: "project",
   131  					Bucket:  "bucket",
   132  					Builder: "builder",
   133  				},
   134  				Status: pb.Status_SCHEDULED,
   135  			},
   136  			Status: pb.Status_SCHEDULED,
   137  		}
   138  		bk := datastore.KeyForObj(ctx, build)
   139  		infra := &model.BuildInfra{
   140  			Build: bk,
   141  			Proto: &pb.BuildInfra{},
   142  		}
   143  		bs := &model.BuildStatus{
   144  			Build:  bk,
   145  			Status: pb.Status_SCHEDULED,
   146  		}
   147  		So(datastore.Put(ctx, build, infra, bs), ShouldBeNil)
   148  
   149  		Convey("build on backend", func() {
   150  			tk, _ := buildtoken.GenerateToken(ctx, 87654321, pb.TokenBody_START_BUILD)
   151  			ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, tk))
   152  
   153  			Convey("build not on backend", func() {
   154  				_, err := srv.StartBuild(ctx, req)
   155  				So(err, ShouldErrLike, `the build 87654321 does not run on task backend`)
   156  			})
   157  
   158  			Convey("first StartBuild", func() {
   159  				Convey("first handshake", func() {
   160  					infra.Proto.Backend = &pb.BuildInfra_Backend{
   161  						Task: &pb.Task{
   162  							Id: &pb.TaskID{
   163  								Target: "swarming://swarming-host",
   164  							},
   165  						},
   166  					}
   167  					So(datastore.Put(ctx, infra), ShouldBeNil)
   168  					res, err := srv.StartBuild(ctx, req)
   169  					So(err, ShouldBeNil)
   170  
   171  					err = datastore.Get(ctx, build, bs)
   172  					So(err, ShouldBeNil)
   173  					So(build.UpdateToken, ShouldEqual, res.UpdateBuildToken)
   174  					So(build.StartBuildRequestID, ShouldEqual, req.RequestId)
   175  					So(build.Status, ShouldEqual, pb.Status_STARTED)
   176  					So(bs.Status, ShouldEqual, pb.Status_STARTED)
   177  
   178  					err = datastore.Get(ctx, infra)
   179  					So(err, ShouldBeNil)
   180  					So(infra.Proto.Backend.Task.Id.Id, ShouldEqual, req.TaskId)
   181  
   182  					// TQ tasks for pubsub-notification.
   183  					tasks := sch.Tasks()
   184  					So(tasks, ShouldHaveLength, 2)
   185  				})
   186  
   187  				Convey("same task", func() {
   188  					infra.Proto.Backend = &pb.BuildInfra_Backend{
   189  						Task: &pb.Task{
   190  							Id: &pb.TaskID{
   191  								Target: "swarming://swarming-host",
   192  								Id:     req.TaskId,
   193  							},
   194  						},
   195  					}
   196  					So(datastore.Put(ctx, infra), ShouldBeNil)
   197  					res, err := srv.StartBuild(ctx, req)
   198  					So(err, ShouldBeNil)
   199  
   200  					build, err = common.GetBuild(ctx, 87654321)
   201  					So(err, ShouldBeNil)
   202  					So(build.UpdateToken, ShouldEqual, res.UpdateBuildToken)
   203  					So(build.StartBuildRequestID, ShouldEqual, req.RequestId)
   204  					So(build.Status, ShouldEqual, pb.Status_STARTED)
   205  
   206  					// TQ tasks for pubsub-notification.
   207  					tasks := sch.Tasks()
   208  					So(tasks, ShouldHaveLength, 2)
   209  				})
   210  
   211  				Convey("after RegisterBuildTask", func() {
   212  					Convey("duplicated task", func() {
   213  						infra.Proto.Backend = &pb.BuildInfra_Backend{
   214  							Task: &pb.Task{
   215  								Id: &pb.TaskID{
   216  									Target: "swarming://swarming-host",
   217  									Id:     "other",
   218  								},
   219  							},
   220  						}
   221  						So(datastore.Put(ctx, infra), ShouldBeNil)
   222  						_, err := srv.StartBuild(ctx, req)
   223  						So(err, ShouldErrLike, `build 87654321 has associated with task "other"`)
   224  						So(buildbucket.DuplicateTask.In(err), ShouldBeTrue)
   225  						build, err = common.GetBuild(ctx, 87654321)
   226  						So(err, ShouldBeNil)
   227  						So(build.UpdateToken, ShouldEqual, "")
   228  						So(build.StartBuildRequestID, ShouldEqual, "")
   229  						So(build.Status, ShouldEqual, pb.Status_SCHEDULED)
   230  
   231  						// TQ tasks for pubsub-notification.
   232  						tasks := sch.Tasks()
   233  						So(tasks, ShouldHaveLength, 0)
   234  					})
   235  
   236  					Convey("same task", func() {
   237  						infra.Proto.Backend = &pb.BuildInfra_Backend{
   238  							Task: &pb.Task{
   239  								Id: &pb.TaskID{
   240  									Target: "swarming://swarming-host",
   241  									Id:     req.TaskId,
   242  								},
   243  							},
   244  						}
   245  						So(datastore.Put(ctx, infra), ShouldBeNil)
   246  						res, err := srv.StartBuild(ctx, req)
   247  						So(err, ShouldBeNil)
   248  
   249  						build, err = common.GetBuild(ctx, 87654321)
   250  						So(err, ShouldBeNil)
   251  						So(build.UpdateToken, ShouldEqual, res.UpdateBuildToken)
   252  						So(build.StartBuildRequestID, ShouldEqual, req.RequestId)
   253  						So(build.Status, ShouldEqual, pb.Status_STARTED)
   254  					})
   255  				})
   256  
   257  				Convey("build has started", func() {
   258  					infra.Proto.Backend = &pb.BuildInfra_Backend{
   259  						Task: &pb.Task{
   260  							Id: &pb.TaskID{
   261  								Target: "swarming://swarming-host",
   262  							},
   263  						},
   264  					}
   265  					build.Proto.Status = pb.Status_STARTED
   266  					So(datastore.Put(ctx, infra, build), ShouldBeNil)
   267  					_, err := srv.StartBuild(ctx, req)
   268  					So(err, ShouldErrLike, `cannot start started build`)
   269  				})
   270  
   271  				Convey("build has ended", func() {
   272  					infra.Proto.Backend = &pb.BuildInfra_Backend{
   273  						Task: &pb.Task{
   274  							Id: &pb.TaskID{
   275  								Target: "swarming://swarming-host",
   276  							},
   277  						},
   278  					}
   279  					build.Proto.Status = pb.Status_FAILURE
   280  					So(datastore.Put(ctx, infra, build), ShouldBeNil)
   281  					_, err := srv.StartBuild(ctx, req)
   282  					So(err, ShouldErrLike, `cannot start ended build`)
   283  				})
   284  			})
   285  
   286  			Convey("subsequent StartBuild", func() {
   287  				Convey("duplicate task", func() {
   288  					build.StartBuildRequestID = "other request"
   289  					infra.Proto.Backend = &pb.BuildInfra_Backend{
   290  						Task: &pb.Task{
   291  							Id: &pb.TaskID{
   292  								Target: "swarming://swarming-host",
   293  								Id:     "another",
   294  							},
   295  						},
   296  					}
   297  					So(datastore.Put(ctx, []any{build, infra}), ShouldBeNil)
   298  
   299  					_, err := srv.StartBuild(ctx, req)
   300  					So(err, ShouldErrLike, `build 87654321 has recorded another StartBuild with request id "other request"`)
   301  					So(buildbucket.DuplicateTask.In(err), ShouldBeTrue)
   302  				})
   303  
   304  				Convey("task with collided request id", func() {
   305  					build.StartBuildRequestID = req.RequestId
   306  					var err error
   307  					tok, err := buildtoken.GenerateToken(ctx, build.ID, pb.TokenBody_BUILD)
   308  					So(err, ShouldBeNil)
   309  					build.UpdateToken = tok
   310  					infra.Proto.Backend = &pb.BuildInfra_Backend{
   311  						Task: &pb.Task{
   312  							Id: &pb.TaskID{
   313  								Target: "swarming://swarming-host",
   314  								Id:     "another",
   315  							},
   316  						},
   317  					}
   318  					So(datastore.Put(ctx, []any{build, infra}), ShouldBeNil)
   319  
   320  					_, err = srv.StartBuild(ctx, req)
   321  					So(err, ShouldErrLike, `build 87654321 has associated with task id "another" with StartBuild request id "random"`)
   322  					So(buildbucket.TaskWithCollidedRequestID.In(err), ShouldBeTrue)
   323  				})
   324  
   325  				Convey("idempotent", func() {
   326  					build.StartBuildRequestID = req.RequestId
   327  					var err error
   328  					tok, err := buildtoken.GenerateToken(ctx, build.ID, pb.TokenBody_BUILD)
   329  					So(err, ShouldBeNil)
   330  					build.UpdateToken = tok
   331  					build.Proto.Status = pb.Status_STARTED
   332  					infra.Proto.Backend = &pb.BuildInfra_Backend{
   333  						Task: &pb.Task{
   334  							Id: &pb.TaskID{
   335  								Target: "swarming://swarming-host",
   336  								Id:     req.TaskId,
   337  							},
   338  						},
   339  					}
   340  					So(datastore.Put(ctx, []any{build, infra}), ShouldBeNil)
   341  
   342  					res, err := srv.StartBuild(ctx, req)
   343  					So(err, ShouldBeNil)
   344  					So(res.UpdateBuildToken, ShouldEqual, tok)
   345  				})
   346  			})
   347  		})
   348  
   349  		Convey("build on swarming", func() {
   350  			Convey("build token missing", func() {
   351  				ctx := metadata.NewIncomingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, "I am a potato"))
   352  				_, err := srv.StartBuild(ctx, req)
   353  				So(err, ShouldErrLike, buildtoken.ErrBadToken)
   354  			})
   355  
   356  			Convey("build token mismatch", func() {
   357  				tk, err := buildtoken.GenerateToken(ctx, 123456, pb.TokenBody_BUILD)
   358  				So(err, ShouldBeNil)
   359  				ctx := metadata.NewIncomingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, tk))
   360  
   361  				_, err = srv.StartBuild(ctx, req)
   362  				So(err, ShouldErrLike, buildtoken.ErrBadToken)
   363  			})
   364  
   365  			Convey("StartBuild", func() {
   366  				tk, err := buildtoken.GenerateToken(ctx, 87654321, pb.TokenBody_BUILD)
   367  				So(err, ShouldBeNil)
   368  				ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, tk))
   369  				build.UpdateToken = tk
   370  				bs := &model.BuildStatus{
   371  					Build:  datastore.KeyForObj(ctx, build),
   372  					Status: pb.Status_SCHEDULED,
   373  				}
   374  				So(datastore.Put(ctx, build, bs), ShouldBeNil)
   375  				Convey("build not on swarming", func() {
   376  					_, err := srv.StartBuild(ctx, req)
   377  					So(err, ShouldErrLike, `the build 87654321 does not run on swarming`)
   378  				})
   379  
   380  				Convey("first StartBuild", func() {
   381  					Convey("first handshake", func() {
   382  						infra.Proto.Swarming = &pb.BuildInfra_Swarming{
   383  							TaskId: req.TaskId,
   384  						}
   385  						So(datastore.Put(ctx, infra), ShouldBeNil)
   386  						res, err := srv.StartBuild(ctx, req)
   387  						So(err, ShouldBeNil)
   388  
   389  						err = datastore.Get(ctx, build, bs)
   390  						So(err, ShouldBeNil)
   391  						So(build.UpdateToken, ShouldEqual, res.UpdateBuildToken)
   392  						So(build.StartBuildRequestID, ShouldEqual, req.RequestId)
   393  						So(build.Status, ShouldEqual, pb.Status_STARTED)
   394  						So(bs.Status, ShouldEqual, pb.Status_STARTED)
   395  
   396  						// TQ tasks for pubsub-notification.
   397  						tasks := sch.Tasks()
   398  						So(tasks, ShouldHaveLength, 2)
   399  					})
   400  
   401  					Convey("first handshake with no task id in datastore", func() {
   402  						infra.Proto.Swarming = &pb.BuildInfra_Swarming{}
   403  						So(datastore.Put(ctx, infra), ShouldBeNil)
   404  						res, err := srv.StartBuild(ctx, req)
   405  						So(err, ShouldBeNil)
   406  
   407  						err = datastore.Get(ctx, build, bs, infra)
   408  						So(err, ShouldBeNil)
   409  						So(build.UpdateToken, ShouldEqual, res.UpdateBuildToken)
   410  						So(build.StartBuildRequestID, ShouldEqual, req.RequestId)
   411  						So(build.Status, ShouldEqual, pb.Status_STARTED)
   412  						So(bs.Status, ShouldEqual, pb.Status_STARTED)
   413  						So(infra.Proto.Swarming.TaskId, ShouldEqual, req.TaskId)
   414  
   415  						// TQ tasks for pubsub-notification.
   416  						tasks := sch.Tasks()
   417  						So(tasks, ShouldHaveLength, 2)
   418  					})
   419  
   420  					Convey("duplicated task", func() {
   421  						infra.Proto.Swarming = &pb.BuildInfra_Swarming{
   422  							TaskId: "another",
   423  						}
   424  						So(datastore.Put(ctx, infra), ShouldBeNil)
   425  						_, err := srv.StartBuild(ctx, req)
   426  						So(err, ShouldErrLike, `build 87654321 has associated with task "another"`)
   427  						So(buildbucket.DuplicateTask.In(err), ShouldBeTrue)
   428  
   429  						// TQ tasks for pubsub-notification.
   430  						tasks := sch.Tasks()
   431  						So(tasks, ShouldHaveLength, 0)
   432  					})
   433  
   434  					Convey("build has started", func() {
   435  						infra.Proto.Swarming = &pb.BuildInfra_Swarming{
   436  							TaskId: req.TaskId,
   437  						}
   438  						build.Proto.Status = pb.Status_STARTED
   439  						So(datastore.Put(ctx, infra, build), ShouldBeNil)
   440  						res, err := srv.StartBuild(ctx, req)
   441  						So(err, ShouldBeNil)
   442  						So(res.UpdateBuildToken, ShouldEqual, build.UpdateToken)
   443  						// TQ tasks for pubsub-notification.
   444  						tasks := sch.Tasks()
   445  						So(tasks, ShouldHaveLength, 0)
   446  					})
   447  
   448  					Convey("build has ended", func() {
   449  						infra.Proto.Swarming = &pb.BuildInfra_Swarming{
   450  							TaskId: req.TaskId,
   451  						}
   452  						build.Proto.Status = pb.Status_FAILURE
   453  						So(datastore.Put(ctx, infra, build), ShouldBeNil)
   454  						_, err := srv.StartBuild(ctx, req)
   455  						So(err, ShouldErrLike, `cannot start ended build`)
   456  					})
   457  				})
   458  
   459  				Convey("subsequent StartBuild", func() {
   460  					Convey("duplicate task", func() {
   461  						build.StartBuildRequestID = "other request"
   462  						infra.Proto.Swarming = &pb.BuildInfra_Swarming{
   463  							TaskId: "another",
   464  						}
   465  						So(datastore.Put(ctx, []any{build, infra}), ShouldBeNil)
   466  
   467  						_, err := srv.StartBuild(ctx, req)
   468  						So(err, ShouldErrLike, `build 87654321 has recorded another StartBuild with request id "other request"`)
   469  						So(buildbucket.DuplicateTask.In(err), ShouldBeTrue)
   470  					})
   471  
   472  					Convey("task with collided request id", func() {
   473  						build.StartBuildRequestID = req.RequestId
   474  						infra.Proto.Swarming = &pb.BuildInfra_Swarming{
   475  							TaskId: "another",
   476  						}
   477  						So(datastore.Put(ctx, []any{build, infra}), ShouldBeNil)
   478  
   479  						_, err := srv.StartBuild(ctx, req)
   480  						So(err, ShouldErrLike, `build 87654321 has associated with task id "another" with StartBuild request id "random"`)
   481  						So(buildbucket.TaskWithCollidedRequestID.In(err), ShouldBeTrue)
   482  					})
   483  
   484  					Convey("idempotent", func() {
   485  						build.StartBuildRequestID = req.RequestId
   486  						build.Proto.Status = pb.Status_STARTED
   487  						infra.Proto.Swarming = &pb.BuildInfra_Swarming{
   488  							TaskId: req.TaskId,
   489  						}
   490  						So(datastore.Put(ctx, []any{build, infra}), ShouldBeNil)
   491  
   492  						res, err := srv.StartBuild(ctx, req)
   493  						So(err, ShouldBeNil)
   494  						So(res.UpdateBuildToken, ShouldEqual, tk)
   495  					})
   496  				})
   497  			})
   498  		})
   499  	})
   500  }