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  }