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 }