go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/led/job/edit_buildbucket.go (about)

     1  // Copyright 2020 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 job
    16  
    17  import (
    18  	"encoding/json"
    19  	"sort"
    20  	"time"
    21  
    22  	"github.com/golang/protobuf/proto"
    23  	"google.golang.org/protobuf/types/known/durationpb"
    24  	"google.golang.org/protobuf/types/known/structpb"
    25  
    26  	"go.chromium.org/luci/buildbucket"
    27  	bbpb "go.chromium.org/luci/buildbucket/proto"
    28  	"go.chromium.org/luci/common/data/stringset"
    29  	"go.chromium.org/luci/common/data/strpair"
    30  	"go.chromium.org/luci/common/errors"
    31  	swarmingpb "go.chromium.org/luci/swarming/proto/api_v2"
    32  )
    33  
    34  // RecipeDirectory is a very unfortunate constant which is here for
    35  // a combination of reasons:
    36  //  1. swarming doesn't allow you to 'checkout' an isolate relative to any
    37  //     path in the task (other than the task root). This means that
    38  //     whatever value we pick for EditRecipeBundle must be used EVERYWHERE
    39  //     the isolated hash is used.
    40  //  2. Currently the 'recipe_engine/led' module will blindly take the
    41  //     isolated input and 'inject' it into further uses of led. This module
    42  //     currently doesn't specify the checkout dir, relying on kitchen's
    43  //     default value of (you guessed it) "kitchen-checkout".
    44  //
    45  // In order to fix this (and it will need to be fixed for bbagent support):
    46  //   - The 'recipe_engine/led' module needs to accept 'checkout-dir' as
    47  //     a parameter in its input properties.
    48  //   - led needs to start passing the checkout dir to the led module's input
    49  //     properties.
    50  //   - `led edit` needs a way to manipulate the checkout directory in a job
    51  //   - The 'recipe_engine/led' module needs to set this in the job
    52  //     alongside the isolate hash when it's doing the injection.
    53  //
    54  // For now, we just hard-code it.
    55  //
    56  // TODO(crbug.com/1072117): Fix this, it's weird.
    57  const RecipeDirectory = "kitchen-checkout"
    58  
    59  // LEDBuilderIsBootstrappedProperty should be set to a boolean value. If true,
    60  // edit-recipe-bundle will set the "led_cas_recipe_bundle" property
    61  // instead of overwriting the build's payload.
    62  const LEDBuilderIsBootstrappedProperty = "led_builder_is_bootstrapped"
    63  
    64  type buildbucketEditor struct {
    65  	jd *Definition
    66  	bb *Buildbucket
    67  
    68  	err error
    69  }
    70  
    71  var _ HighLevelEditor = (*buildbucketEditor)(nil)
    72  
    73  func newBuildbucketEditor(jd *Definition) *buildbucketEditor {
    74  	bb := jd.GetBuildbucket()
    75  	if bb == nil {
    76  		panic(errors.New("impossible: only supported for Buildbucket builds"))
    77  	}
    78  	bb.EnsureBasics()
    79  
    80  	return &buildbucketEditor{jd, bb, nil}
    81  }
    82  
    83  func (bbe *buildbucketEditor) Close() error {
    84  	return bbe.err
    85  }
    86  
    87  func (bbe *buildbucketEditor) tweak(fn func() error) {
    88  	if bbe.err == nil {
    89  		bbe.err = fn()
    90  	}
    91  }
    92  
    93  func (bbe *buildbucketEditor) Tags(values []string) {
    94  	if len(values) == 0 {
    95  		return
    96  	}
    97  
    98  	bbe.tweak(func() (err error) {
    99  		if err = validateTags(values); err == nil {
   100  			tags := bbe.bb.BbagentArgs.Build.Tags
   101  			for _, tag := range values {
   102  				k, v := strpair.Parse(tag)
   103  				tags = append(tags, &bbpb.StringPair{
   104  					Key:   k,
   105  					Value: v,
   106  				})
   107  			}
   108  			sort.Slice(tags, func(i, j int) bool { return tags[i].Key < tags[j].Key })
   109  			bbe.bb.BbagentArgs.Build.Tags = tags
   110  		}
   111  		return nil
   112  	})
   113  }
   114  
   115  func (bbe *buildbucketEditor) TaskPayloadSource(cipdPkg, cipdVers string) {
   116  	bbe.tweak(func() error {
   117  		usedCipdVers := cipdVers
   118  		if cipdVers == "" {
   119  			usedCipdVers = "latest"
   120  		}
   121  		// Update exe.
   122  		exe := bbe.bb.BbagentArgs.Build.Exe
   123  		if cipdPkg != "" {
   124  			exe.CipdPackage = cipdPkg
   125  			exe.CipdVersion = usedCipdVers
   126  		} else if cipdPkg == "" && cipdVers == "" {
   127  			exe.CipdPackage = ""
   128  			exe.CipdVersion = ""
   129  		} else {
   130  			return errors.Reason(
   131  				"cipdPkg and cipdVers must both be set or both be empty: cipdPkg=%q cipdVers=%q",
   132  				cipdPkg, cipdVers).Err()
   133  		}
   134  
   135  		// Update infra.Buildbucket.Agent.Input
   136  		if cipdPkg == "" && cipdVers == "" {
   137  			return nil
   138  		}
   139  		bbe.TaskPayloadPath(RecipeDirectory)
   140  		input := bbe.bb.BbagentArgs.Build.Infra.Buildbucket.Agent.Input
   141  		if input == nil {
   142  			input = &bbpb.BuildInfra_Buildbucket_Agent_Input{}
   143  			bbe.bb.BbagentArgs.Build.Infra.Buildbucket.Agent.Input = input
   144  		}
   145  		inputData := input.GetData()
   146  		if len(inputData) == 0 {
   147  			inputData = make(map[string]*bbpb.InputDataRef)
   148  			input.Data = inputData
   149  		}
   150  		if ref, ok := inputData[RecipeDirectory]; ok && ref.GetCipd() != nil {
   151  			if len(ref.GetCipd().Specs) > 1 {
   152  				return errors.Reason("can only have one user payload under %s", RecipeDirectory).Err()
   153  			}
   154  			ref.GetCipd().Specs[0] = &bbpb.InputDataRef_CIPD_PkgSpec{
   155  				Package: cipdPkg,
   156  				Version: usedCipdVers,
   157  			}
   158  			return nil
   159  		}
   160  		inputData[RecipeDirectory] = &bbpb.InputDataRef{
   161  			DataType: &bbpb.InputDataRef_Cipd{
   162  				Cipd: &bbpb.InputDataRef_CIPD{
   163  					Specs: []*bbpb.InputDataRef_CIPD_PkgSpec{
   164  						&bbpb.InputDataRef_CIPD_PkgSpec{
   165  							Package: cipdPkg,
   166  							Version: usedCipdVers,
   167  						},
   168  					},
   169  				},
   170  			},
   171  		}
   172  
   173  		return nil
   174  	})
   175  }
   176  
   177  func (bbe *buildbucketEditor) TaskPayloadPath(path string) {
   178  	bbe.tweak(func() error {
   179  		bbe.bb.UpdatePayloadPath(path)
   180  		return nil
   181  	})
   182  }
   183  
   184  func (bbe *buildbucketEditor) CASTaskPayload(path string, casRef *swarmingpb.CASReference) {
   185  	bbe.tweak(func() error {
   186  		if path != "" {
   187  			bbe.TaskPayloadPath(path)
   188  		} else {
   189  			purposes := bbe.bb.BbagentArgs.Build.Infra.Buildbucket.GetAgent().GetPurposes()
   190  			for dir, pur := range purposes {
   191  				if pur == bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD {
   192  					path = dir
   193  					break
   194  				}
   195  			}
   196  		}
   197  
   198  		if path == "" {
   199  			return errors.Reason("failed to get exe payload path").Err()
   200  		}
   201  
   202  		input := bbe.bb.BbagentArgs.Build.Infra.Buildbucket.Agent.Input
   203  		if input == nil {
   204  			input = &bbpb.BuildInfra_Buildbucket_Agent_Input{}
   205  			bbe.bb.BbagentArgs.Build.Infra.Buildbucket.Agent.Input = input
   206  		}
   207  		inputData := input.GetData()
   208  		if len(inputData) == 0 {
   209  			inputData = make(map[string]*bbpb.InputDataRef)
   210  			input.Data = inputData
   211  		}
   212  
   213  		if ref, ok := inputData[path]; ok && ref.GetCas() != nil {
   214  			if casRef.CasInstance != "" {
   215  				ref.GetCas().CasInstance = casRef.CasInstance
   216  			}
   217  			ref.GetCas().Digest = &bbpb.InputDataRef_CAS_Digest{
   218  				Hash:      casRef.GetDigest().GetHash(),
   219  				SizeBytes: casRef.GetDigest().GetSizeBytes(),
   220  			}
   221  		} else {
   222  			casInstance := casRef.CasInstance
   223  			if casInstance == "" {
   224  				var err error
   225  				casInstance, err = bbe.jd.CasInstance()
   226  				if err != nil {
   227  					return err
   228  				}
   229  			}
   230  			inputData[path] = &bbpb.InputDataRef{
   231  				DataType: &bbpb.InputDataRef_Cas{
   232  					Cas: &bbpb.InputDataRef_CAS{
   233  						CasInstance: casInstance,
   234  						Digest: &bbpb.InputDataRef_CAS_Digest{
   235  							Hash:      casRef.GetDigest().GetHash(),
   236  							SizeBytes: casRef.GetDigest().GetSizeBytes(),
   237  						},
   238  					},
   239  				},
   240  			}
   241  		}
   242  		return nil
   243  	})
   244  }
   245  
   246  func (bbe *buildbucketEditor) TaskPayloadCmd(args []string) {
   247  	bbe.tweak(func() error {
   248  		if len(args) == 0 {
   249  			args = []string{"luciexe"}
   250  		}
   251  		bbe.bb.BbagentArgs.Build.Exe.Cmd = args
   252  		return nil
   253  	})
   254  }
   255  
   256  func (bbe *buildbucketEditor) ClearCurrentIsolated() {
   257  	bbe.tweak(func() error {
   258  		agent := bbe.bb.BbagentArgs.Build.GetInfra().GetBuildbucket().GetAgent()
   259  		if agent == nil {
   260  			return nil
   261  		}
   262  
   263  		payloadPath := ""
   264  		for p, purpose := range agent.GetPurposes() {
   265  			if purpose == bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD {
   266  				payloadPath = p
   267  				break
   268  			}
   269  		}
   270  		if payloadPath == "" {
   271  			return nil
   272  		}
   273  		inputData := agent.GetInput().GetData()
   274  		if ref, ok := inputData[payloadPath]; ok {
   275  			if ref.GetCas() != nil {
   276  				delete(inputData, payloadPath)
   277  			}
   278  		}
   279  		return nil
   280  	})
   281  }
   282  
   283  func (bbe *buildbucketEditor) ClearDimensions() {
   284  	bbe.tweak(func() error {
   285  		infra := bbe.bb.BbagentArgs.Build.Infra
   286  		if infra.Swarming != nil {
   287  			bbe.bb.BbagentArgs.Build.Infra.Swarming.TaskDimensions = nil
   288  		} else {
   289  			bbe.bb.BbagentArgs.Build.Infra.Backend.TaskDimensions = nil
   290  		}
   291  
   292  		return nil
   293  	})
   294  }
   295  
   296  func (bbe *buildbucketEditor) SetDimensions(dims ExpiringDimensions) {
   297  	bbe.ClearDimensions()
   298  	dec := DimensionEditCommands{}
   299  	for key, vals := range dims {
   300  		dec[key] = &DimensionEditCommand{SetValues: vals}
   301  	}
   302  	bbe.EditDimensions(dec)
   303  }
   304  
   305  func (bbe *buildbucketEditor) EditDimensions(dimEdits DimensionEditCommands) {
   306  	if len(dimEdits) == 0 {
   307  		return
   308  	}
   309  
   310  	bbe.tweak(func() error {
   311  		dims, err := bbe.jd.Info().Dimensions()
   312  		if err != nil {
   313  			return err
   314  		}
   315  
   316  		dimMap := dims.toLogical()
   317  		dimEdits.apply(dimMap, 0)
   318  
   319  		build := bbe.bb.BbagentArgs.Build
   320  		var curTimeout time.Duration
   321  		if build.SchedulingTimeout != nil {
   322  			if err := build.SchedulingTimeout.CheckValid(); err != nil {
   323  				return err
   324  			}
   325  			curTimeout = build.SchedulingTimeout.AsDuration()
   326  		}
   327  		var maxExp time.Duration
   328  		var newDimLen int
   329  		if build.Infra.Swarming != nil {
   330  			newDimLen = len(build.Infra.Swarming.TaskDimensions) + len(dimEdits)
   331  		} else {
   332  			newDimLen = len(build.Infra.Backend.TaskDimensions) + len(dimEdits)
   333  		}
   334  		newDims := make([]*bbpb.RequestedDimension, 0, newDimLen)
   335  		for _, key := range keysOf(dimMap) {
   336  			valueExp := dimMap[key]
   337  			for _, value := range keysOf(valueExp) {
   338  				exp := valueExp[value]
   339  				if exp > maxExp {
   340  					maxExp = exp
   341  				}
   342  
   343  				toAdd := &bbpb.RequestedDimension{
   344  					Key:   key,
   345  					Value: value,
   346  				}
   347  				if exp > 0 && exp != curTimeout {
   348  					toAdd.Expiration = durationpb.New(exp)
   349  				}
   350  				newDims = append(newDims, toAdd)
   351  			}
   352  		}
   353  		if build.Infra.Swarming != nil {
   354  			build.Infra.Swarming.TaskDimensions = newDims
   355  		} else {
   356  			build.Infra.Backend.TaskDimensions = newDims
   357  		}
   358  
   359  		if maxExp > curTimeout {
   360  			build.SchedulingTimeout = durationpb.New(maxExp)
   361  		}
   362  		return nil
   363  	})
   364  }
   365  
   366  func (bbe *buildbucketEditor) Env(env map[string]string) {
   367  	if len(env) == 0 {
   368  		return
   369  	}
   370  
   371  	bbe.tweak(func() error {
   372  		updateStringPairList(&bbe.bb.EnvVars, env)
   373  		return nil
   374  	})
   375  }
   376  
   377  func (bbe *buildbucketEditor) Priority(priority int32) {
   378  	bbe.tweak(func() error {
   379  		if priority < 0 {
   380  			return errors.Reason("negative Priority argument: %d", priority).Err()
   381  		}
   382  
   383  		infra := bbe.bb.BbagentArgs.Build.Infra
   384  		if infra.Swarming != nil {
   385  			infra.Swarming.Priority = priority
   386  		} else {
   387  			infra.Backend.Config.Fields["priority"] = structpb.NewNumberValue(float64(priority))
   388  		}
   389  		return nil
   390  	})
   391  }
   392  
   393  func (bbe *buildbucketEditor) Properties(props map[string]string, auto bool) {
   394  	if len(props) == 0 {
   395  		return
   396  	}
   397  	bbe.tweak(func() error {
   398  		toWrite := map[string]any{}
   399  		removed := make([]string, 0, len(props))
   400  
   401  		for k, v := range props {
   402  			if v == "" {
   403  				toWrite[k] = nil
   404  				removed = append(removed, k)
   405  			} else {
   406  				var obj any
   407  				if err := json.Unmarshal([]byte(v), &obj); err != nil {
   408  					if !auto {
   409  						return err
   410  					}
   411  					obj = v
   412  				}
   413  				toWrite[k] = obj
   414  			}
   415  		}
   416  
   417  		if bbe.bb.BbagentArgs.Build.Input.Properties.GetFields()[LEDBuilderIsBootstrappedProperty].GetBoolValue() {
   418  			propCopy := map[string]any{}
   419  			for k, v := range toWrite {
   420  				if v == nil {
   421  					// removed properties are tracked by `removed`.
   422  					continue
   423  				}
   424  				propCopy[k] = v
   425  			}
   426  			toWrite["led_edited_properties"] = propCopy
   427  			toWrite["led_removed_properties"] = removed
   428  		}
   429  
   430  		bbe.bb.WriteProperties(toWrite)
   431  		return nil
   432  	})
   433  }
   434  
   435  func (bbe *buildbucketEditor) CIPDPkgs(cipdPkgs CIPDPkgs) {
   436  	if len(cipdPkgs) == 0 {
   437  		return
   438  	}
   439  
   440  	bbe.tweak(func() error {
   441  		if !bbe.bb.BbagentDownloadCIPDPkgs() {
   442  			cipdPkgs.updateCipdPkgs(&bbe.bb.CipdPackages)
   443  			return nil
   444  		}
   445  		return errors.Reason("not supported for Buildbucket v2 builds").Err()
   446  	})
   447  }
   448  
   449  func (bbe *buildbucketEditor) SwarmingHostname(host string) {
   450  	bbe.tweak(func() (err error) {
   451  		if host == "" {
   452  			return errors.New("empty SwarmingHostname")
   453  		}
   454  
   455  		infra := bbe.bb.BbagentArgs.Build.Infra
   456  		if infra.Swarming != nil {
   457  			infra.Swarming.Hostname = host
   458  		} else {
   459  			return errors.New("the build does not run on swarming directly.")
   460  		}
   461  		return
   462  	})
   463  }
   464  
   465  func (bbe *buildbucketEditor) TaskName(name string) {
   466  	bbe.tweak(func() (err error) {
   467  		bbe.bb.Name = name
   468  		return
   469  	})
   470  }
   471  
   472  func (bbe *buildbucketEditor) Experimental(isExperimental bool) {
   473  	bbe.tweak(func() error {
   474  		bbe.Experiments(map[string]bool{buildbucket.ExperimentNonProduction: isExperimental})
   475  		return nil
   476  	})
   477  }
   478  
   479  func (bbe *buildbucketEditor) Experiments(exps map[string]bool) {
   480  	bbe.tweak(func() error {
   481  		if len(exps) == 0 {
   482  			return nil
   483  		}
   484  
   485  		er := bbe.bb.BbagentArgs.Build.Infra.Buildbucket.ExperimentReasons
   486  		if er == nil {
   487  			er = make(map[string]bbpb.BuildInfra_Buildbucket_ExperimentReason)
   488  			bbe.bb.BbagentArgs.Build.Infra.Buildbucket.ExperimentReasons = er
   489  		}
   490  		enabled := stringset.NewFromSlice(bbe.bb.BbagentArgs.Build.Input.Experiments...)
   491  		for k, v := range exps {
   492  			if k == buildbucket.ExperimentNonProduction {
   493  				bbe.bb.BbagentArgs.Build.Input.Experimental = v
   494  			}
   495  			if v {
   496  				enabled.Add(k)
   497  				er[k] = bbpb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
   498  			} else {
   499  				enabled.Del(k)
   500  				delete(er, k)
   501  			}
   502  		}
   503  		bbe.bb.BbagentArgs.Build.Input.Experiments = enabled.ToSortedSlice()
   504  		return nil
   505  	})
   506  }
   507  
   508  func (bbe *buildbucketEditor) PrefixPathEnv(values []string) {
   509  	if len(values) == 0 {
   510  		return
   511  	}
   512  
   513  	bbe.tweak(func() error {
   514  		updatePrefixPathEnv(values, &bbe.bb.EnvPrefixes)
   515  		return nil
   516  	})
   517  }
   518  
   519  func (bbe *buildbucketEditor) ClearGerritChanges() {
   520  	bbe.tweak(func() error {
   521  		bbe.bb.BbagentArgs.Build.Input.GerritChanges = nil
   522  		return nil
   523  	})
   524  }
   525  
   526  func (bbe *buildbucketEditor) AddGerritChange(cl *bbpb.GerritChange) {
   527  	if cl == nil {
   528  		return
   529  	}
   530  
   531  	bbe.tweak(func() error {
   532  		gc := &bbe.bb.BbagentArgs.Build.Input.GerritChanges
   533  		for _, change := range *gc {
   534  			if proto.Equal(change, cl) {
   535  				return nil
   536  			}
   537  		}
   538  		*gc = append(*gc, cl)
   539  		return nil
   540  	})
   541  }
   542  
   543  func (bbe *buildbucketEditor) RemoveGerritChange(cl *bbpb.GerritChange) {
   544  	if cl == nil {
   545  		return
   546  	}
   547  
   548  	bbe.tweak(func() error {
   549  		gc := &bbe.bb.BbagentArgs.Build.Input.GerritChanges
   550  		for idx, change := range *gc {
   551  			if proto.Equal(change, cl) {
   552  				*gc = append((*gc)[:idx], (*gc)[idx+1:]...)
   553  				return nil
   554  			}
   555  		}
   556  		return nil
   557  	})
   558  }
   559  
   560  func (bbe *buildbucketEditor) GitilesCommit(commit *bbpb.GitilesCommit) {
   561  	bbe.tweak(func() error {
   562  		bbe.bb.BbagentArgs.Build.Input.GitilesCommit = commit
   563  		return nil
   564  	})
   565  }