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 }