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  }