go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/synthesize_build_test.go (about) 1 // Copyright 2022 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 "math/rand" 20 "testing" 21 22 "google.golang.org/protobuf/types/known/durationpb" 23 "google.golang.org/protobuf/types/known/structpb" 24 25 "go.chromium.org/luci/auth/identity" 26 "go.chromium.org/luci/common/clock/testclock" 27 "go.chromium.org/luci/common/data/rand/mathrand" 28 "go.chromium.org/luci/gae/filter/txndefer" 29 "go.chromium.org/luci/gae/impl/memory" 30 "go.chromium.org/luci/gae/service/datastore" 31 "go.chromium.org/luci/server/auth" 32 "go.chromium.org/luci/server/auth/authtest" 33 34 "go.chromium.org/luci/buildbucket/appengine/internal/config" 35 "go.chromium.org/luci/buildbucket/appengine/model" 36 "go.chromium.org/luci/buildbucket/bbperms" 37 pb "go.chromium.org/luci/buildbucket/proto" 38 39 . "github.com/smartystreets/goconvey/convey" 40 . "go.chromium.org/luci/common/testing/assertions" 41 ) 42 43 func TestValidateSynthesize(t *testing.T) { 44 t.Parallel() 45 46 Convey("validateSynthesize", t, func() { 47 Convey("nil", func() { 48 So(validateSynthesize(&pb.SynthesizeBuildRequest{}), ShouldErrLike, "builder or template_build_id is required") 49 }) 50 51 Convey("invalid Builder", func() { 52 req := &pb.SynthesizeBuildRequest{ 53 Builder: &pb.BuilderID{ 54 Project: "project", 55 Builder: "builder", 56 }, 57 } 58 So(validateSynthesize(req), ShouldErrLike, "builder:") 59 }) 60 }) 61 62 } 63 64 func TestSynthesizeBuild(t *testing.T) { 65 const userID = identity.Identity("user:user@example.com") 66 67 Convey("SynthesizeBuild", t, func() { 68 srv := &Builds{} 69 ctx := txndefer.FilterRDS(memory.Use(context.Background())) 70 ctx = mathrand.Set(ctx, rand.New(rand.NewSource(0))) 71 ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC) 72 datastore.GetTestable(ctx).AutoIndex(true) 73 datastore.GetTestable(ctx).Consistent(true) 74 75 So(config.SetTestSettingsCfg(ctx, &pb.SettingsCfg{ 76 Resultdb: &pb.ResultDBSettings{ 77 Hostname: "rdbHost", 78 }, 79 Swarming: &pb.SwarmingSettings{ 80 BbagentPackage: &pb.SwarmingSettings_Package{ 81 PackageName: "bbagent", 82 Version: "bbagent-version", 83 }, 84 KitchenPackage: &pb.SwarmingSettings_Package{ 85 PackageName: "kitchen", 86 Version: "kitchen-version", 87 }, 88 }, 89 }), ShouldBeNil) 90 91 ctx = auth.WithState(ctx, &authtest.FakeState{ 92 Identity: userID, 93 FakeDB: authtest.NewFakeDB( 94 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet), 95 ), 96 }) 97 98 Convey("fail", func() { 99 Convey("entities missing", func() { 100 Convey("bucket missing", func() { 101 req := &pb.SynthesizeBuildRequest{ 102 Builder: &pb.BuilderID{ 103 Project: "project", 104 Bucket: "bucket", 105 Builder: "builder", 106 }, 107 } 108 _, err := srv.SynthesizeBuild(ctx, req) 109 So(err, ShouldErrLike, "not found") 110 }) 111 Convey("shadow bucket", func() { 112 So(datastore.Put(ctx, &model.Bucket{ 113 ID: "bucket.shadow", 114 Parent: model.ProjectKey(ctx, "project"), 115 Proto: &pb.Bucket{}, 116 Shadows: []string{"bucket"}, 117 }), ShouldBeNil) 118 ctx = auth.WithState(ctx, &authtest.FakeState{ 119 Identity: userID, 120 FakeDB: authtest.NewFakeDB( 121 authtest.MockPermission(userID, "project:bucket.shadow", bbperms.BuildersGet), 122 ), 123 }) 124 req := &pb.SynthesizeBuildRequest{ 125 Builder: &pb.BuilderID{ 126 Project: "project", 127 Bucket: "bucket.shadow", 128 Builder: "builder", 129 }, 130 } 131 _, err := srv.SynthesizeBuild(ctx, req) 132 So(err, ShouldErrLike, "Synthesizing a build from a shadow bucket is not supported") 133 }) 134 Convey("builder missing", func() { 135 So(datastore.Put(ctx, &model.Bucket{ 136 ID: "bucket", 137 Parent: model.ProjectKey(ctx, "project"), 138 Proto: &pb.Bucket{}, 139 }), ShouldBeNil) 140 req := &pb.SynthesizeBuildRequest{ 141 Builder: &pb.BuilderID{ 142 Project: "project", 143 Bucket: "bucket", 144 Builder: "builder", 145 }, 146 } 147 _, err := srv.SynthesizeBuild(ctx, req) 148 So(err, ShouldErrLike, "not found") 149 }) 150 }) 151 Convey("permissions denied", func() { 152 So(datastore.Put(ctx, &model.Bucket{ 153 ID: "bucket", 154 Parent: model.ProjectKey(ctx, "project"), 155 Proto: &pb.Bucket{}, 156 }), ShouldBeNil) 157 Convey("permission denied for getting builder", func() { 158 ctx = auth.WithState(ctx, &authtest.FakeState{ 159 Identity: "user:unauthorized@example.com", 160 }) 161 req := &pb.SynthesizeBuildRequest{ 162 Builder: &pb.BuilderID{ 163 Project: "project", 164 Bucket: "bucket", 165 Builder: "builder", 166 }, 167 } 168 _, err := srv.SynthesizeBuild(ctx, req) 169 So(err, ShouldErrLike, "not found") 170 }) 171 Convey("permission denied for getting template build", func() { 172 ctx = auth.WithState(ctx, &authtest.FakeState{ 173 Identity: "user:unauthorized@example.com", 174 }) 175 So(datastore.Put(ctx, &model.Build{ 176 Proto: &pb.Build{ 177 Id: 1, 178 Builder: &pb.BuilderID{ 179 Project: "project", 180 Bucket: "bucket", 181 Builder: "builder", 182 }, 183 }, 184 }), ShouldBeNil) 185 186 req := &pb.SynthesizeBuildRequest{ 187 TemplateBuildId: 1, 188 } 189 _, err := srv.SynthesizeBuild(ctx, req) 190 So(err, ShouldErrLike, "not found") 191 }) 192 }) 193 }) 194 195 Convey("pass", func() { 196 So(datastore.Put(ctx, &model.Bucket{ 197 ID: "bucket", 198 Parent: model.ProjectKey(ctx, "project"), 199 Proto: &pb.Bucket{}, 200 }), ShouldBeNil) 201 So(datastore.Put(ctx, &model.Builder{ 202 Parent: model.BucketKey(ctx, "project", "bucket"), 203 ID: "builder", 204 Config: &pb.BuilderConfig{ 205 Name: "builder", 206 ServiceAccount: "sa@chops-service-accounts.iam.gserviceaccount.com", 207 Dimensions: []string{"pool:pool1"}, 208 Properties: `{"a":"b","b":"b"}`, 209 ShadowBuilderAdjustments: &pb.BuilderConfig_ShadowBuilderAdjustments{ 210 ServiceAccount: "shadow@chops-service-accounts.iam.gserviceaccount.com", 211 Pool: "pool2", 212 Properties: `{"a":"b2","c":"c"}`, 213 Dimensions: []string{ 214 "pool:pool2", 215 }, 216 }, 217 }, 218 }), ShouldBeNil) 219 220 ctx = auth.WithState(ctx, &authtest.FakeState{ 221 Identity: userID, 222 FakeDB: authtest.NewFakeDB( 223 authtest.MockPermission(userID, "project:bucket", bbperms.BuildersGet), 224 authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet), 225 ), 226 }) 227 228 Convey("template build", func() { 229 So(datastore.Put(ctx, &model.Build{ 230 Proto: &pb.Build{ 231 Id: 1, 232 Builder: &pb.BuilderID{ 233 Project: "project", 234 Bucket: "bucket", 235 Builder: "builder", 236 }, 237 Input: &pb.Build_Input{ 238 GerritChanges: []*pb.GerritChange{ 239 { 240 Host: "host", 241 Patchset: 1, 242 Project: "project", 243 }, 244 }, 245 }, 246 // Non-retriable build can still be synthesized. 247 Retriable: pb.Trinary_NO, 248 }, 249 }), ShouldBeNil) 250 251 req := &pb.SynthesizeBuildRequest{ 252 TemplateBuildId: 1, 253 } 254 b, err := srv.SynthesizeBuild(ctx, req) 255 So(err, ShouldBeNil) 256 257 expected := &pb.Build{ 258 Builder: &pb.BuilderID{ 259 Project: "project", 260 Bucket: "bucket", 261 Builder: "builder", 262 }, 263 Exe: &pb.Executable{ 264 Cmd: []string{"recipes"}, 265 }, 266 ExecutionTimeout: &durationpb.Duration{ 267 Seconds: 10800, 268 }, 269 GracePeriod: &durationpb.Duration{ 270 Seconds: 30, 271 }, 272 Infra: &pb.BuildInfra{ 273 Bbagent: &pb.BuildInfra_BBAgent{ 274 CacheDir: "cache", 275 PayloadPath: "kitchen-checkout", 276 }, 277 Buildbucket: &pb.BuildInfra_Buildbucket{ 278 Hostname: "app.appspot.com", 279 Agent: &pb.BuildInfra_Buildbucket_Agent{ 280 Input: &pb.BuildInfra_Buildbucket_Agent_Input{}, 281 Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{ 282 "kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD, 283 }, 284 }, 285 }, 286 Logdog: &pb.BuildInfra_LogDog{ 287 Project: "project", 288 }, 289 Resultdb: &pb.BuildInfra_ResultDB{ 290 Hostname: "rdbHost", 291 }, 292 Swarming: &pb.BuildInfra_Swarming{ 293 Caches: []*pb.BuildInfra_Swarming_CacheEntry{ 294 { 295 Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2", 296 Path: "builder", 297 WaitForWarmCache: &durationpb.Duration{ 298 Seconds: 240, 299 }, 300 }, 301 }, 302 Priority: 30, 303 TaskServiceAccount: "sa@chops-service-accounts.iam.gserviceaccount.com", 304 TaskDimensions: []*pb.RequestedDimension{ 305 { 306 Key: "pool", 307 Value: "pool1", 308 }, 309 }, 310 }, 311 }, 312 Input: &pb.Build_Input{ 313 Properties: &structpb.Struct{ 314 Fields: map[string]*structpb.Value{ 315 "a": { 316 Kind: &structpb.Value_StringValue{ 317 StringValue: "b", 318 }, 319 }, 320 "b": { 321 Kind: &structpb.Value_StringValue{ 322 StringValue: "b", 323 }, 324 }, 325 }, 326 }, 327 GerritChanges: []*pb.GerritChange{ 328 { 329 Host: "host", 330 Patchset: 1, 331 Project: "project", 332 }, 333 }, 334 }, 335 SchedulingTimeout: &durationpb.Duration{ 336 Seconds: 21600, 337 }, 338 Tags: []*pb.StringPair{ 339 { 340 Key: "builder", 341 Value: "builder", 342 }, 343 { 344 Key: "buildset", 345 Value: "patch/gerrit/host/0/1", 346 }, 347 }, 348 } 349 So(b, ShouldResembleProto, expected) 350 }) 351 352 Convey("builder", func() { 353 So(datastore.Put(ctx, &model.Bucket{ 354 ID: "bucket", 355 Parent: model.ProjectKey(ctx, "project"), 356 Proto: &pb.Bucket{ 357 Acls: []*pb.Acl{ 358 { 359 Identity: "user:caller@example.com", 360 Role: pb.Acl_READER, 361 }, 362 }, 363 Shadow: "bucket.shadow", 364 }, 365 }), ShouldBeNil) 366 expected := &pb.Build{ 367 Builder: &pb.BuilderID{ 368 Project: "project", 369 Bucket: "bucket.shadow", 370 Builder: "builder", 371 }, 372 Exe: &pb.Executable{ 373 Cmd: []string{"recipes"}, 374 }, 375 ExecutionTimeout: &durationpb.Duration{ 376 Seconds: 10800, 377 }, 378 GracePeriod: &durationpb.Duration{ 379 Seconds: 30, 380 }, 381 Infra: &pb.BuildInfra{ 382 Bbagent: &pb.BuildInfra_BBAgent{ 383 CacheDir: "cache", 384 PayloadPath: "kitchen-checkout", 385 }, 386 Buildbucket: &pb.BuildInfra_Buildbucket{ 387 Hostname: "app.appspot.com", 388 Agent: &pb.BuildInfra_Buildbucket_Agent{ 389 Input: &pb.BuildInfra_Buildbucket_Agent_Input{}, 390 Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{ 391 "kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD, 392 }, 393 }, 394 }, 395 Logdog: &pb.BuildInfra_LogDog{ 396 Project: "project", 397 }, 398 Resultdb: &pb.BuildInfra_ResultDB{ 399 Hostname: "rdbHost", 400 }, 401 Swarming: &pb.BuildInfra_Swarming{ 402 Caches: []*pb.BuildInfra_Swarming_CacheEntry{ 403 { 404 Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2", 405 Path: "builder", 406 WaitForWarmCache: &durationpb.Duration{ 407 Seconds: 240, 408 }, 409 }, 410 }, 411 Priority: 30, 412 TaskServiceAccount: "shadow@chops-service-accounts.iam.gserviceaccount.com", 413 TaskDimensions: []*pb.RequestedDimension{ 414 { 415 Key: "pool", 416 Value: "pool2", 417 }, 418 }, 419 }, 420 Led: &pb.BuildInfra_Led{ 421 ShadowedBucket: "bucket", 422 }, 423 }, 424 Input: &pb.Build_Input{ 425 Properties: &structpb.Struct{ 426 Fields: map[string]*structpb.Value{ 427 "$recipe_engine/led": { 428 Kind: &structpb.Value_StructValue{ 429 StructValue: &structpb.Struct{ 430 Fields: map[string]*structpb.Value{ 431 "shadowed_bucket": { 432 Kind: &structpb.Value_StringValue{ 433 StringValue: "bucket", 434 }, 435 }, 436 }, 437 }, 438 }, 439 }, 440 "a": { 441 Kind: &structpb.Value_StringValue{ 442 StringValue: "b2", 443 }, 444 }, 445 "b": { 446 Kind: &structpb.Value_StringValue{ 447 StringValue: "b", 448 }, 449 }, 450 "c": { 451 Kind: &structpb.Value_StringValue{ 452 StringValue: "c", 453 }, 454 }, 455 }, 456 }, 457 }, 458 SchedulingTimeout: &durationpb.Duration{ 459 Seconds: 21600, 460 }, 461 Tags: []*pb.StringPair{ 462 { 463 Key: "builder", 464 Value: "builder", 465 }, 466 }, 467 } 468 req := &pb.SynthesizeBuildRequest{ 469 Builder: &pb.BuilderID{ 470 Project: "project", 471 Bucket: "bucket", 472 Builder: "builder", 473 }, 474 } 475 b, err := srv.SynthesizeBuild(ctx, req) 476 So(err, ShouldBeNil) 477 478 So(b, ShouldResembleProto, expected) 479 }) 480 481 Convey("set experiments", func() { 482 So(datastore.Put(ctx, &model.Bucket{ 483 ID: "bucket", 484 Parent: model.ProjectKey(ctx, "project"), 485 Proto: &pb.Bucket{ 486 Acls: []*pb.Acl{ 487 { 488 Identity: "user:caller@example.com", 489 Role: pb.Acl_READER, 490 }, 491 }, 492 Shadow: "bucket.shadow", 493 }, 494 }), ShouldBeNil) 495 expected := &pb.Build{ 496 Builder: &pb.BuilderID{ 497 Project: "project", 498 Bucket: "bucket.shadow", 499 Builder: "builder", 500 }, 501 Exe: &pb.Executable{ 502 Cmd: []string{"recipes"}, 503 }, 504 ExecutionTimeout: &durationpb.Duration{ 505 Seconds: 10800, 506 }, 507 GracePeriod: &durationpb.Duration{ 508 Seconds: 30, 509 }, 510 Infra: &pb.BuildInfra{ 511 Bbagent: &pb.BuildInfra_BBAgent{ 512 CacheDir: "cache", 513 PayloadPath: "kitchen-checkout", 514 }, 515 Buildbucket: &pb.BuildInfra_Buildbucket{ 516 Hostname: "app.appspot.com", 517 ExperimentReasons: map[string]pb.BuildInfra_Buildbucket_ExperimentReason{ 518 "cool.experiment_thing": pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED, 519 "disabled.experiment_thing": pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED, 520 }, 521 Agent: &pb.BuildInfra_Buildbucket_Agent{ 522 Input: &pb.BuildInfra_Buildbucket_Agent_Input{}, 523 Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{ 524 "kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD, 525 }, 526 }, 527 }, 528 Logdog: &pb.BuildInfra_LogDog{ 529 Project: "project", 530 }, 531 Resultdb: &pb.BuildInfra_ResultDB{ 532 Hostname: "rdbHost", 533 }, 534 Swarming: &pb.BuildInfra_Swarming{ 535 Caches: []*pb.BuildInfra_Swarming_CacheEntry{ 536 { 537 Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2", 538 Path: "builder", 539 WaitForWarmCache: &durationpb.Duration{ 540 Seconds: 240, 541 }, 542 }, 543 }, 544 Priority: 30, 545 TaskServiceAccount: "shadow@chops-service-accounts.iam.gserviceaccount.com", 546 TaskDimensions: []*pb.RequestedDimension{ 547 { 548 Key: "pool", 549 Value: "pool2", 550 }, 551 }, 552 }, 553 Led: &pb.BuildInfra_Led{ 554 ShadowedBucket: "bucket", 555 }, 556 }, 557 Input: &pb.Build_Input{ 558 Properties: &structpb.Struct{ 559 Fields: map[string]*structpb.Value{ 560 "$recipe_engine/led": { 561 Kind: &structpb.Value_StructValue{ 562 StructValue: &structpb.Struct{ 563 Fields: map[string]*structpb.Value{ 564 "shadowed_bucket": { 565 Kind: &structpb.Value_StringValue{ 566 StringValue: "bucket", 567 }, 568 }, 569 }, 570 }, 571 }, 572 }, 573 "a": { 574 Kind: &structpb.Value_StringValue{ 575 StringValue: "b2", 576 }, 577 }, 578 "b": { 579 Kind: &structpb.Value_StringValue{ 580 StringValue: "b", 581 }, 582 }, 583 "c": { 584 Kind: &structpb.Value_StringValue{ 585 StringValue: "c", 586 }, 587 }, 588 }, 589 }, 590 Experiments: []string{ 591 "cool.experiment_thing", 592 }, 593 }, 594 SchedulingTimeout: &durationpb.Duration{ 595 Seconds: 21600, 596 }, 597 Tags: []*pb.StringPair{ 598 { 599 Key: "builder", 600 Value: "builder", 601 }, 602 }, 603 } 604 req := &pb.SynthesizeBuildRequest{ 605 Builder: &pb.BuilderID{ 606 Project: "project", 607 Bucket: "bucket", 608 Builder: "builder", 609 }, 610 Experiments: map[string]bool{ 611 "cool.experiment_thing": true, 612 "disabled.experiment_thing": false, 613 }, 614 } 615 b, err := srv.SynthesizeBuild(ctx, req) 616 So(err, ShouldBeNil) 617 618 So(b, ShouldResembleProto, expected) 619 }) 620 621 }) 622 }) 623 }