go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/led/job/edit_swarming.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  	"sort"
    19  	"strings"
    20  	"time"
    21  
    22  	"go.chromium.org/luci/common/errors"
    23  	swarmingpb "go.chromium.org/luci/swarming/proto/api_v2"
    24  )
    25  
    26  type swarmingEditor struct {
    27  	jd *Definition
    28  	sw *Swarming
    29  
    30  	err error
    31  }
    32  
    33  var _ Editor = (*swarmingEditor)(nil)
    34  
    35  func newSwarmingEditor(jd *Definition) *swarmingEditor {
    36  	sw := jd.GetSwarming()
    37  	if sw == nil {
    38  		panic(errors.New("impossible: only supported for Swarming builds"))
    39  	}
    40  	if sw.Task == nil {
    41  		sw.Task = &swarmingpb.NewTaskRequest{}
    42  	}
    43  
    44  	return &swarmingEditor{jd, sw, nil}
    45  }
    46  
    47  func (swe *swarmingEditor) Close() error {
    48  	return swe.err
    49  }
    50  
    51  func (swe *swarmingEditor) tweak(fn func() error) {
    52  	if swe.err == nil {
    53  		swe.err = fn()
    54  	}
    55  }
    56  
    57  func (swe *swarmingEditor) tweakSlices(fn func(*swarmingpb.TaskSlice) error) {
    58  	swe.tweak(func() error {
    59  		for _, slice := range swe.sw.GetTask().GetTaskSlices() {
    60  			if slice.Properties == nil {
    61  				slice.Properties = &swarmingpb.TaskProperties{}
    62  			}
    63  
    64  			if err := fn(slice); err != nil {
    65  				return err
    66  			}
    67  		}
    68  		return nil
    69  	})
    70  }
    71  
    72  func (swe *swarmingEditor) ClearCurrentIsolated() {
    73  	swe.tweak(func() error {
    74  		swe.jd.GetSwarming().CasUserPayload = nil
    75  
    76  		return nil
    77  	})
    78  	swe.tweakSlices(func(slc *swarmingpb.TaskSlice) error {
    79  		slc.Properties.CasInputRoot = nil
    80  		return nil
    81  	})
    82  }
    83  
    84  func (swe *swarmingEditor) ClearDimensions() {
    85  	swe.tweakSlices(func(slc *swarmingpb.TaskSlice) error {
    86  		slc.Properties.Dimensions = nil
    87  		return nil
    88  	})
    89  }
    90  
    91  func (swe *swarmingEditor) SetDimensions(dims ExpiringDimensions) {
    92  	swe.ClearDimensions()
    93  
    94  	dec := DimensionEditCommands{}
    95  	for key, vals := range dims {
    96  		dec[key] = &DimensionEditCommand{SetValues: vals}
    97  	}
    98  	swe.EditDimensions(dec)
    99  }
   100  
   101  // EditDimensions is a bit trickier for swarming than it is for buildbucket.
   102  //
   103  // We want to map the dimEdits onto existing slices; Slices in the swarming task
   104  // are listed with their expiration times relative to the previous slice, which
   105  // means we need to do a bit of precomputation to convert these to
   106  // expiration-relative-to-task-start times.
   107  //
   108  // If dimEdits contains set/add values which don't align with any existing
   109  // slices, this will set an error.
   110  func (swe *swarmingEditor) EditDimensions(dimEdits DimensionEditCommands) {
   111  	if len(dimEdits) == 0 {
   112  		return
   113  	}
   114  
   115  	swe.tweak(func() error {
   116  		taskRelativeExpirationSet := map[time.Duration]struct{}{}
   117  		slices := swe.sw.GetTask().GetTaskSlices()
   118  		sliceByExp := make([]struct {
   119  			// seconds from start-of-task to expiration of this slice.
   120  			TotalExpiration time.Duration
   121  			*swarmingpb.TaskSlice
   122  		}, len(slices))
   123  
   124  		for i, slc := range slices {
   125  			sliceRelativeExpiration := time.Duration(float64(slc.GetExpirationSecs()) * float64(time.Second))
   126  			taskRelativeExpiration := sliceRelativeExpiration
   127  			if i > 0 {
   128  				taskRelativeExpiration += sliceByExp[i-1].TotalExpiration
   129  			}
   130  			taskRelativeExpirationSet[taskRelativeExpiration] = struct{}{}
   131  
   132  			sliceByExp[i].TotalExpiration = taskRelativeExpiration
   133  			sliceByExp[i].TaskSlice = slc
   134  		}
   135  
   136  		checkValidExpiration := func(key string, value ExpiringValue, op string) error {
   137  			if value.Expiration == 0 {
   138  				return nil
   139  			}
   140  
   141  			if _, ok := taskRelativeExpirationSet[value.Expiration]; !ok {
   142  				validExpirations := make([]int64, len(sliceByExp)+1)
   143  				for i, slc := range sliceByExp {
   144  					validExpirations[i+1] = int64(slc.TotalExpiration / time.Second)
   145  				}
   146  
   147  				return errors.Reason(
   148  					"%s%s%s@%d has invalid expiration time: current slices expire at %v",
   149  					key, op, value.Value, value.Expiration/time.Second, validExpirations).Err()
   150  			}
   151  			return nil
   152  		}
   153  
   154  		for key, edits := range dimEdits {
   155  			for _, setval := range edits.SetValues {
   156  				if err := checkValidExpiration(key, setval, "="); err != nil {
   157  					return err
   158  				}
   159  			}
   160  			for _, addval := range edits.AddValues {
   161  				if err := checkValidExpiration(key, addval, "+="); err != nil {
   162  					return err
   163  				}
   164  			}
   165  		}
   166  
   167  		// Now we know that all the edits slot into some slice, we can actually
   168  		// apply them.
   169  		for _, slc := range sliceByExp {
   170  			if slc.Properties == nil {
   171  				slc.Properties = &swarmingpb.TaskProperties{}
   172  			}
   173  			dimMap := logicalDimensions{}
   174  			for _, dim := range slc.Properties.Dimensions {
   175  				dimMap.updateDuration(dim.Key, dim.Value, slc.TotalExpiration)
   176  			}
   177  			dimEdits.apply(dimMap, slc.TotalExpiration)
   178  			newDims := make([]*swarmingpb.StringPair, 0, len(dimMap))
   179  			for _, key := range keysOf(dimMap) {
   180  				for _, value := range keysOf(dimMap[key]) {
   181  					newDims = append(newDims, &swarmingpb.StringPair{
   182  						Key: key, Value: value,
   183  					})
   184  				}
   185  			}
   186  			slc.Properties.Dimensions = newDims
   187  		}
   188  
   189  		return nil
   190  	})
   191  }
   192  
   193  func (swe *swarmingEditor) Env(env map[string]string) {
   194  	if len(env) == 0 {
   195  		return
   196  	}
   197  
   198  	swe.tweakSlices(func(slc *swarmingpb.TaskSlice) error {
   199  		updateStringPairList(&slc.Properties.Env, env)
   200  		return nil
   201  	})
   202  }
   203  
   204  func (swe *swarmingEditor) Priority(priority int32) {
   205  	swe.tweak(func() error {
   206  		if priority < 0 {
   207  			return errors.Reason("negative Priority argument: %d", priority).Err()
   208  		}
   209  		if task := swe.sw.GetTask(); task == nil {
   210  			swe.sw.Task = &swarmingpb.NewTaskRequest{}
   211  		}
   212  		swe.sw.Task.Priority = priority
   213  		return nil
   214  	})
   215  }
   216  
   217  func (swe *swarmingEditor) CIPDPkgs(cipdPkgs CIPDPkgs) {
   218  	swe.tweakSlices(func(slc *swarmingpb.TaskSlice) error {
   219  		if slc.Properties.CipdInput == nil {
   220  			slc.Properties.CipdInput = &swarmingpb.CipdInput{}
   221  		}
   222  		cipdPkgs.updateCipdPkgs(&slc.Properties.CipdInput.Packages)
   223  		return nil
   224  	})
   225  }
   226  
   227  func (swe *swarmingEditor) SwarmingHostname(host string) {
   228  	swe.tweak(func() (err error) {
   229  		if host == "" {
   230  			return errors.New("empty SwarmingHostname")
   231  		}
   232  		swe.sw.Hostname = host
   233  		return
   234  	})
   235  }
   236  
   237  func (swe *swarmingEditor) TaskName(name string) {
   238  	swe.tweak(func() (err error) {
   239  		swe.sw.Task.Name = name
   240  		return
   241  	})
   242  }
   243  
   244  func updatePrefixPathEnv(values []string, prefixes *[]*swarmingpb.StringListPair) {
   245  	var pair *swarmingpb.StringListPair
   246  	for _, pair = range *prefixes {
   247  		if pair.Key == "PATH" {
   248  			newPath := make([]string, len(pair.Value))
   249  			copy(newPath, pair.Value)
   250  			pair.Value = newPath
   251  			break
   252  		}
   253  	}
   254  	if pair == nil {
   255  		pair = &swarmingpb.StringListPair{Key: "PATH"}
   256  		*prefixes = append(*prefixes, pair)
   257  	}
   258  
   259  	var newPath []string
   260  	for _, pair := range *prefixes {
   261  		if pair.Key == "PATH" {
   262  			newPath = make([]string, len(pair.Value))
   263  			copy(newPath, pair.Value)
   264  			break
   265  		}
   266  	}
   267  
   268  	for _, v := range values {
   269  		if strings.HasPrefix(v, "!") {
   270  			idx := 0
   271  			for _, cur := range newPath {
   272  				if cur != v[1:] {
   273  					newPath[idx] = cur
   274  					idx++
   275  				}
   276  			}
   277  			newPath = newPath[:idx]
   278  		} else {
   279  			newPath = append(newPath, v)
   280  		}
   281  	}
   282  
   283  	pair.Value = newPath
   284  }
   285  
   286  func (swe *swarmingEditor) PrefixPathEnv(values []string) {
   287  	if len(values) == 0 {
   288  		return
   289  	}
   290  
   291  	swe.tweakSlices(func(slc *swarmingpb.TaskSlice) error {
   292  		updatePrefixPathEnv(values, &slc.Properties.EnvPrefixes)
   293  		return nil
   294  	})
   295  }
   296  
   297  func validateTags(tags []string) error {
   298  	for _, tag := range tags {
   299  		if !strings.Contains(tag, ":") {
   300  			return errors.Reason("bad tag %q: must be in the form 'key:value'", tag).Err()
   301  		}
   302  	}
   303  	return nil
   304  }
   305  
   306  func (swe *swarmingEditor) Tags(values []string) {
   307  	if len(values) == 0 {
   308  		return
   309  	}
   310  	swe.tweak(func() (err error) {
   311  		if err = validateTags(values); err == nil {
   312  			if swe.sw.Task == nil {
   313  				swe.sw.Task = &swarmingpb.NewTaskRequest{}
   314  			}
   315  			swe.sw.Task.Tags = append(swe.sw.Task.Tags, values...)
   316  			sort.Strings(swe.sw.Task.Tags)
   317  		}
   318  		return
   319  	})
   320  }