go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/cmd/bbagent/cipd_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 main 16 17 import ( 18 "context" 19 "encoding/json" 20 "fmt" 21 "os" 22 "os/exec" 23 "path/filepath" 24 "strings" 25 "testing" 26 27 . "github.com/smartystreets/goconvey/convey" 28 29 bbpb "go.chromium.org/luci/buildbucket/proto" 30 "go.chromium.org/luci/common/logging" 31 "go.chromium.org/luci/common/logging/memlogger" 32 . "go.chromium.org/luci/common/testing/assertions" 33 "go.chromium.org/luci/gae/impl/memory" 34 ) 35 36 var successResult = &cipdOut{ 37 Result: map[string][]*cipdPkg{ 38 "path_a": {{Package: "pkg_a", InstanceID: "instance_a"}}, 39 "path_b": {{Package: "pkg_b", InstanceID: "instance_b"}}, 40 kitchenCheckout: {{Package: "package", InstanceID: "instance_k"}}, 41 }, 42 } 43 44 var testCase string 45 46 // fakeExecCommand mocks exec Command. It will trigger TestHelperProcess to 47 // return the right mocked output. 48 func fakeExecCommand(_ context.Context, command string, args ...string) *exec.Cmd { 49 os.Environ() 50 cs := []string{"-test.run=TestHelperProcess", "--", command} 51 cs = append(cs, args...) 52 cmd := exec.Command(os.Args[0], cs...) 53 tc := "TEST_CASE=" + testCase 54 fakeResultsFilePath := "RESULTS_FILE=" + resultsFilePath 55 cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1", tc, fakeResultsFilePath} 56 return cmd 57 } 58 59 // TestHelperProcess produces fake outputs based on the "TEST_CASE" env var when 60 // executed with the env var "GO_WANT_HELPER_PROCESS" set to 1. 61 func TestHelperProcess(t *testing.T) { 62 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { 63 return 64 } 65 defer os.Exit(0) 66 args := os.Args 67 for len(args) > 0 { 68 if args[0] == "--" { 69 args = args[1:] 70 break 71 } 72 args = args[1:] 73 } 74 if len(args) == 0 { 75 fmt.Fprintf(os.Stderr, "No command\n") 76 os.Exit(2) 77 } 78 79 // check if it's a `cipd ensure` command. 80 if !(args[0][len(args[0])-4:] == "cipd" && args[1] == "ensure") { 81 fmt.Fprintf(os.Stderr, "Not a cipd ensure command: %s\n", args) 82 os.Exit(1) 83 } 84 switch os.Getenv("TEST_CASE") { 85 case "success": 86 // Mock the generated json file of `cipd ensure` command. 87 jsonRs, _ := json.Marshal(successResult) 88 if err := os.WriteFile(os.Getenv("RESULTS_FILE"), jsonRs, 0666); err != nil { 89 fmt.Fprintf(os.Stderr, "Errors in preparing data for tests\n") 90 } 91 92 case "failure": 93 os.Exit(1) 94 } 95 } 96 97 func TestPrependPath(t *testing.T) { 98 originalPathEnv := os.Getenv("PATH") 99 Convey("prependPath", t, func() { 100 defer func() { 101 _ = os.Setenv("PATH", originalPathEnv) 102 }() 103 104 build := &bbpb.Build{ 105 Id: 123, 106 Infra: &bbpb.BuildInfra{ 107 Buildbucket: &bbpb.BuildInfra_Buildbucket{ 108 Agent: &bbpb.BuildInfra_Buildbucket_Agent{ 109 Input: &bbpb.BuildInfra_Buildbucket_Agent_Input{ 110 Data: map[string]*bbpb.InputDataRef{ 111 "path_a": { 112 DataType: &bbpb.InputDataRef_Cipd{ 113 Cipd: &bbpb.InputDataRef_CIPD{ 114 Specs: []*bbpb.InputDataRef_CIPD_PkgSpec{{Package: "pkg_a", Version: "latest"}}, 115 }, 116 }, 117 OnPath: []string{"path_a/bin", "path_a"}, 118 }, 119 "path_b": { 120 DataType: &bbpb.InputDataRef_Cas{ 121 Cas: &bbpb.InputDataRef_CAS{ 122 CasInstance: "projects/project/instances/instance", 123 Digest: &bbpb.InputDataRef_CAS_Digest{ 124 Hash: "hash", 125 SizeBytes: 1, 126 }, 127 }, 128 }, 129 OnPath: []string{"path_b/bin", "path_b"}, 130 }, 131 }, 132 CipdSource: map[string]*bbpb.InputDataRef{ 133 "cipd": { 134 OnPath: []string{"cipd"}, 135 }, 136 }, 137 }, 138 Output: &bbpb.BuildInfra_Buildbucket_Agent_Output{}, 139 }, 140 }, 141 }, 142 Input: &bbpb.Build_Input{ 143 Experiments: []string{"luci.buildbucket.agent.cipd_installation"}, 144 }, 145 } 146 147 cwd, err := os.Getwd() 148 So(err, ShouldBeNil) 149 So(prependPath(build, cwd), ShouldBeNil) 150 pathEnv := os.Getenv("PATH") 151 var expectedPath []string 152 for _, p := range []string{"path_a", "path_a/bin", "path_b", "path_b/bin"} { 153 expectedPath = append(expectedPath, filepath.Join(cwd, p)) 154 } 155 So(strings.Contains(pathEnv, strings.Join(expectedPath, string(os.PathListSeparator))), ShouldBeTrue) 156 }) 157 } 158 159 func TestInstallCipdPackages(t *testing.T) { 160 t.Parallel() 161 resultsFilePath = filepath.Join(t.TempDir(), "cipd_ensure_results.json") 162 caseBase := "cache" 163 Convey("installCipdPackages", t, func() { 164 ctx := memory.Use(context.Background()) 165 ctx = memlogger.Use(ctx) 166 logs := logging.Get(ctx).(*memlogger.MemLogger) 167 execCommandContext = fakeExecCommand 168 defer func() { execCommandContext = exec.CommandContext }() 169 170 build := &bbpb.Build{ 171 Id: 123, 172 Infra: &bbpb.BuildInfra{ 173 Buildbucket: &bbpb.BuildInfra_Buildbucket{ 174 Agent: &bbpb.BuildInfra_Buildbucket_Agent{ 175 Input: &bbpb.BuildInfra_Buildbucket_Agent_Input{ 176 Data: map[string]*bbpb.InputDataRef{ 177 "path_a": { 178 DataType: &bbpb.InputDataRef_Cipd{ 179 Cipd: &bbpb.InputDataRef_CIPD{ 180 Specs: []*bbpb.InputDataRef_CIPD_PkgSpec{{Package: "pkg_a", Version: "latest"}}, 181 }, 182 }, 183 OnPath: []string{"path_a/bin", "path_a"}, 184 }, 185 "path_b": { 186 DataType: &bbpb.InputDataRef_Cipd{ 187 Cipd: &bbpb.InputDataRef_CIPD{ 188 Specs: []*bbpb.InputDataRef_CIPD_PkgSpec{{Package: "pkg_b", Version: "latest"}}, 189 }, 190 }, 191 OnPath: []string{"path_b/bin", "path_b"}, 192 }, 193 }, 194 }, 195 Output: &bbpb.BuildInfra_Buildbucket_Agent_Output{}, 196 }, 197 }, 198 }, 199 Input: &bbpb.Build_Input{ 200 Experiments: []string{"luci.buildbucket.agent.cipd_installation"}, 201 }, 202 } 203 204 Convey("without named cache", func() { 205 Convey("success", func() { 206 testCase = "success" 207 cwd, err := os.Getwd() 208 So(err, ShouldBeNil) 209 So(installCipdPackages(ctx, build, cwd, caseBase), ShouldBeNil) 210 So(build.Infra.Buildbucket.Agent.Output.ResolvedData["path_a"], ShouldResembleProto, &bbpb.ResolvedDataRef{ 211 DataType: &bbpb.ResolvedDataRef_Cipd{ 212 Cipd: &bbpb.ResolvedDataRef_CIPD{ 213 Specs: []*bbpb.ResolvedDataRef_CIPD_PkgSpec{{Package: successResult.Result["path_a"][0].Package, Version: successResult.Result["path_a"][0].InstanceID}}, 214 }, 215 }, 216 }) 217 So(build.Infra.Buildbucket.Agent.Output.ResolvedData["path_b"], ShouldResembleProto, &bbpb.ResolvedDataRef{ 218 DataType: &bbpb.ResolvedDataRef_Cipd{ 219 Cipd: &bbpb.ResolvedDataRef_CIPD{ 220 Specs: []*bbpb.ResolvedDataRef_CIPD_PkgSpec{{Package: successResult.Result["path_b"][0].Package, Version: successResult.Result["path_b"][0].InstanceID}}, 221 }, 222 }, 223 }) 224 }) 225 226 Convey("failure", func() { 227 testCase = "failure" 228 err := installCipdPackages(ctx, build, ".", caseBase) 229 So(build.Infra.Buildbucket.Agent.Output.ResolvedData, ShouldBeNil) 230 So(err, ShouldErrLike, "Failed to run cipd ensure command") 231 }) 232 }) 233 234 Convey("with named cache", func() { 235 build.Infra.Buildbucket.Agent.CipdPackagesCache = &bbpb.CacheEntry{ 236 Name: "cipd_cache_hash", 237 Path: "cipd_cache", 238 } 239 testCase = "success" 240 cwd, err := os.Getwd() 241 So(err, ShouldBeNil) 242 So(installCipdPackages(ctx, build, cwd, caseBase), ShouldBeNil) 243 So(build.Infra.Buildbucket.Agent.Output.ResolvedData["path_a"], ShouldResembleProto, &bbpb.ResolvedDataRef{ 244 DataType: &bbpb.ResolvedDataRef_Cipd{ 245 Cipd: &bbpb.ResolvedDataRef_CIPD{ 246 Specs: []*bbpb.ResolvedDataRef_CIPD_PkgSpec{{Package: successResult.Result["path_a"][0].Package, Version: successResult.Result["path_a"][0].InstanceID}}, 247 }, 248 }, 249 }) 250 So(build.Infra.Buildbucket.Agent.Output.ResolvedData["path_b"], ShouldResembleProto, &bbpb.ResolvedDataRef{ 251 DataType: &bbpb.ResolvedDataRef_Cipd{ 252 Cipd: &bbpb.ResolvedDataRef_CIPD{ 253 Specs: []*bbpb.ResolvedDataRef_CIPD_PkgSpec{{Package: successResult.Result["path_b"][0].Package, Version: successResult.Result["path_b"][0].InstanceID}}, 254 }, 255 }, 256 }) 257 So(logs, memlogger.ShouldHaveLog, 258 logging.Info, fmt.Sprintf(`Setting $CIPD_CACHE_DIR to %q`, filepath.Join(cwd, caseBase, "cipd_cache"))) 259 }) 260 261 Convey("handle kitchenCheckout", func() { 262 Convey("kitchenCheckout not in agent input", func() { 263 testCase = "success" 264 build.Exe = &bbpb.Executable{ 265 CipdPackage: "package", 266 CipdVersion: "version", 267 } 268 cwd, err := os.Getwd() 269 So(err, ShouldBeNil) 270 So(installCipdPackages(ctx, build, cwd, "cache"), ShouldBeNil) 271 So(build.Infra.Buildbucket.Agent.Purposes[kitchenCheckout], ShouldEqual, bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD) 272 So(build.Infra.Buildbucket.Agent.Output.ResolvedData[kitchenCheckout], ShouldResembleProto, &bbpb.ResolvedDataRef{ 273 DataType: &bbpb.ResolvedDataRef_Cipd{ 274 Cipd: &bbpb.ResolvedDataRef_CIPD{ 275 Specs: []*bbpb.ResolvedDataRef_CIPD_PkgSpec{{Package: successResult.Result[kitchenCheckout][0].Package, Version: successResult.Result[kitchenCheckout][0].InstanceID}}, 276 }, 277 }, 278 }) 279 }) 280 Convey("kitchenCheckout in agent input", func() { 281 testCase = "success" 282 build.Exe = &bbpb.Executable{ 283 CipdPackage: "package", 284 CipdVersion: "version", 285 } 286 build.Infra.Buildbucket.Agent.Input.Data[kitchenCheckout] = &bbpb.InputDataRef{ 287 DataType: &bbpb.InputDataRef_Cipd{ 288 Cipd: &bbpb.InputDataRef_CIPD{ 289 Specs: []*bbpb.InputDataRef_CIPD_PkgSpec{{Package: "package", Version: "version"}}, 290 }, 291 }, 292 } 293 build.Infra.Buildbucket.Agent.Purposes = map[string]bbpb.BuildInfra_Buildbucket_Agent_Purpose{ 294 kitchenCheckout: bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD, 295 } 296 cwd, err := os.Getwd() 297 So(err, ShouldBeNil) 298 So(installCipdPackages(ctx, build, cwd, "cache"), ShouldBeNil) 299 So(build.Infra.Buildbucket.Agent.Purposes[kitchenCheckout], ShouldEqual, bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD) 300 So(build.Infra.Buildbucket.Agent.Output.ResolvedData[kitchenCheckout], ShouldResembleProto, &bbpb.ResolvedDataRef{ 301 DataType: &bbpb.ResolvedDataRef_Cipd{ 302 Cipd: &bbpb.ResolvedDataRef_CIPD{ 303 Specs: []*bbpb.ResolvedDataRef_CIPD_PkgSpec{{Package: successResult.Result[kitchenCheckout][0].Package, Version: successResult.Result[kitchenCheckout][0].InstanceID}}, 304 }, 305 }, 306 }) 307 }) 308 }) 309 }) 310 } 311 312 func TestInstallCipd(t *testing.T) { 313 t.Parallel() 314 Convey("InstallCipd", t, func() { 315 ctx := context.Background() 316 ctx = memlogger.Use(ctx) 317 logs := logging.Get(ctx).(*memlogger.MemLogger) 318 build := &bbpb.Build{ 319 Id: 123, 320 Infra: &bbpb.BuildInfra{ 321 Buildbucket: &bbpb.BuildInfra_Buildbucket{ 322 Agent: &bbpb.BuildInfra_Buildbucket_Agent{ 323 Input: &bbpb.BuildInfra_Buildbucket_Agent_Input{ 324 CipdSource: map[string]*bbpb.InputDataRef{ 325 "cipd": { 326 DataType: &bbpb.InputDataRef_Cipd{ 327 Cipd: &bbpb.InputDataRef_CIPD{ 328 Server: "chrome-infra-packages.appspot.com", 329 Specs: []*bbpb.InputDataRef_CIPD_PkgSpec{ 330 { 331 Package: "infra/tools/cipd/${platform}", 332 Version: "latest", 333 }, 334 }, 335 }, 336 }, 337 OnPath: []string{"cipd"}, 338 }, 339 }, 340 Data: map[string]*bbpb.InputDataRef{ 341 "path_a": { 342 DataType: &bbpb.InputDataRef_Cipd{ 343 Cipd: &bbpb.InputDataRef_CIPD{ 344 Specs: []*bbpb.InputDataRef_CIPD_PkgSpec{{Package: "pkg_a", Version: "latest"}}, 345 }, 346 }, 347 OnPath: []string{"path_a/bin", "path_a"}, 348 }, 349 "path_b": { 350 DataType: &bbpb.InputDataRef_Cipd{ 351 Cipd: &bbpb.InputDataRef_CIPD{ 352 Specs: []*bbpb.InputDataRef_CIPD_PkgSpec{{Package: "pkg_b", Version: "latest"}}, 353 }, 354 }, 355 OnPath: []string{"path_b/bin", "path_b"}, 356 }, 357 }, 358 }, 359 Output: &bbpb.BuildInfra_Buildbucket_Agent_Output{}, 360 }, 361 }, 362 }, 363 Input: &bbpb.Build_Input{ 364 Experiments: []string{"luci.buildbucket.agent.cipd_installation"}, 365 }, 366 } 367 tempDir := t.TempDir() 368 cacheBase := "cache" 369 cipdURL := "https://chrome-infra-packages.appspot.com/client?platform=linux-amd64&version=latest" 370 Convey("without cache", func() { 371 err := installCipd(ctx, build, tempDir, cacheBase, "linux-amd64") 372 So(err, ShouldBeNil) 373 // check to make sure cipd is correctly saved in the directory 374 cipdDir := filepath.Join(tempDir, "cipd") 375 files, err := os.ReadDir(cipdDir) 376 So(err, ShouldBeNil) 377 for _, file := range files { 378 So(file.Name(), ShouldEqual, "cipd") 379 } 380 // check the cipd path in set in PATH 381 pathEnv := os.Getenv("PATH") 382 So(strings.Contains(pathEnv, "cipd"), ShouldBeTrue) 383 So(logs, memlogger.ShouldHaveLog, 384 logging.Info, fmt.Sprintf("Install CIPD client from URL: %s into %s", cipdURL, cipdDir)) 385 }) 386 387 Convey("with cache", func() { 388 build.Infra.Buildbucket.Agent.CipdClientCache = &bbpb.CacheEntry{ 389 Name: "cipd_client_hash", 390 Path: "cipd_client", 391 } 392 cipdCacheDir := filepath.Join(tempDir, cacheBase, "cipd_client") 393 err := os.MkdirAll(cipdCacheDir, 0750) 394 So(err, ShouldBeNil) 395 396 Convey("hit", func() { 397 // create an empty file as if it's the cipd client. 398 err := os.WriteFile(filepath.Join(cipdCacheDir, "cipd"), []byte(""), 0644) 399 So(err, ShouldBeNil) 400 err = installCipd(ctx, build, tempDir, cacheBase, "linux-amd64") 401 So(err, ShouldBeNil) 402 // check to make sure cipd is correctly saved in the directory 403 files, err := os.ReadDir(cipdCacheDir) 404 So(err, ShouldBeNil) 405 for _, file := range files { 406 So(file.Name(), ShouldEqual, "cipd") 407 } 408 So(logs, memlogger.ShouldNotHaveLog, 409 logging.Info, fmt.Sprintf("Install CIPD client from URL: %s into %s", cipdURL, cipdCacheDir)) 410 411 // check the cipd path in set in PATH 412 pathEnv := os.Getenv("PATH") 413 So(strings.Contains(pathEnv, strings.Join([]string{cipdCacheDir, filepath.Join(cipdCacheDir, "bin")}, string(os.PathListSeparator))), ShouldBeTrue) 414 }) 415 416 Convey("miss", func() { 417 err := installCipd(ctx, build, tempDir, cacheBase, "linux-amd64") 418 So(err, ShouldBeNil) 419 // check to make sure cipd is correctly saved in the directory 420 files, err := os.ReadDir(cipdCacheDir) 421 So(err, ShouldBeNil) 422 for _, file := range files { 423 So(file.Name(), ShouldEqual, "cipd") 424 } 425 So(logs, memlogger.ShouldHaveLog, 426 logging.Info, fmt.Sprintf("Install CIPD client from URL: %s into %s", cipdURL, cipdCacheDir)) 427 428 // check the cipd path in set in PATH 429 pathEnv := os.Getenv("PATH") 430 So(strings.Contains(pathEnv, strings.Join([]string{cipdCacheDir, filepath.Join(cipdCacheDir, "bin")}, string(os.PathListSeparator))), ShouldBeTrue) 431 }) 432 }) 433 }) 434 }