github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/model/project_matrix.go (about) 1 package model 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 8 "github.com/evergreen-ci/evergreen/command" 9 "github.com/evergreen-ci/evergreen/util" 10 "github.com/pkg/errors" 11 ) 12 13 // This file contains the code for matrix generation. 14 // Project matrices are a shortcut for defining many variants 15 // by combining multiple axes. A full explanation of the matrix format 16 // is available at #TODO GitHub wiki documentation (EVG-1175). 17 // 18 // On a high level, matrix variant construction takes the following steps: 19 // 1. Matrix definitions are read as part of a project's `buildvariants` field 20 // and moved into a separate "matrices" slice. 21 // 2. A tag selector evaluator is constructed for evaluating axis selectors 22 // 3. The matrix and axis definitions are passed to buildMatrixVariants, which 23 // creates all combinations of matrix cells and removes excluded ones. 24 // 4. During the generation of a single cell, we merge all axis values for the cell 25 // together to create a fully filled-in variant. Matrix rules concerning non-task settings 26 // are evaluated as well. Rules `add_tasks` and `remove_tasks` are stored in the variant 27 // for later evaluation. 28 // 5. Created variants are appended back to the project's list of buildvariants. 29 // 6. During evaluateBuildVariants in project_parser.go, rules are executed. 30 31 // matrix defines a set of variants programmatically by 32 // combining a series of axis values and rules. 33 type matrix struct { 34 Id string `yaml:"matrix_name"` 35 Spec matrixDefinition `yaml:"matrix_spec"` 36 Exclude matrixDefinitions `yaml:"exclude_spec"` 37 DisplayName string `yaml:"display_name"` 38 Tags parserStringSlice `yaml:"tags"` 39 Modules parserStringSlice `yaml:"modules"` 40 BatchTime *int `yaml:"batchtime"` 41 Stepback *bool `yaml:"stepback"` 42 RunOn parserStringSlice `yaml:"run_on"` 43 Tasks parserBVTasks `yaml:"tasks"` 44 Rules []matrixRule `yaml:"rules"` 45 } 46 47 // matrixAxis represents one axis of a matrix definition. 48 type matrixAxis struct { 49 Id string `yaml:"id"` 50 DisplayName string `yaml:"display_name"` 51 Values []axisValue `yaml:"values"` 52 } 53 54 // find returns the axisValue with the given name. 55 func (ma matrixAxis) find(id string) (axisValue, error) { 56 for _, v := range ma.Values { 57 if v.Id == id { 58 return v, nil 59 } 60 } 61 return axisValue{}, errors.Errorf("axis '%v' does not contain value '%v'", ma.Id, id) 62 } 63 64 // axisValues make up the "points" along a matrix axis. Values are 65 // combined during matrix evaluation to produce new variants. 66 type axisValue struct { 67 Id string `yaml:"id"` 68 DisplayName string `yaml:"display_name"` 69 Variables command.Expansions `yaml:"variables"` 70 RunOn parserStringSlice `yaml:"run_on"` 71 Tags parserStringSlice `yaml:"tags"` 72 Modules parserStringSlice `yaml:"modules"` 73 BatchTime *int `yaml:"batchtime"` 74 Stepback *bool `yaml:"stepback"` 75 } 76 77 // helper methods for tag selectors 78 func (av *axisValue) name() string { return av.Id } 79 func (av *axisValue) tags() []string { return av.Tags } 80 81 // matrixValue represents a "cell" of a matrix 82 type matrixValue map[string]string 83 84 // String returns the matrixValue in simple JSON format 85 func (mv matrixValue) String() string { 86 asJSON, err := json.Marshal(&mv) 87 if err != nil { 88 return fmt.Sprintf("%#v", mv) 89 } 90 return string(asJSON) 91 } 92 93 // matrixDefinition is a map of axis name -> axis value representing 94 // an n-dimensional matrix configuration. 95 type matrixDefinition map[string]parserStringSlice 96 97 // String returns the matrixDefinition in simple JSON format 98 func (mdef matrixDefinition) String() string { 99 asJSON, err := json.Marshal(&mdef) 100 if err != nil { 101 return fmt.Sprintf("%#v", mdef) 102 } 103 return string(asJSON) 104 } 105 106 // allCells returns every value (cell) within the matrix definition. 107 // IMPORTANT: this logic assume that all selectors have been evaluated 108 // and no duplicates exist. 109 func (mdef matrixDefinition) allCells() []matrixValue { 110 // this should never happen, we handle empty defs but just for sanity 111 if len(mdef) == 0 { 112 return nil 113 } 114 // You can think of the logic below as traversing an n-dimensional matrix, 115 // emulating an n-dimensional for-loop using a set of counters (like an old-school 116 // golf counter). We're doing this iteratively to avoid the overhead and sloppy code 117 // required to constantly copy and merge maps that using recursion would require. 118 type axisCache struct { 119 Id string 120 Vals []string 121 Count int 122 } 123 axes := []axisCache{} 124 for axis, values := range mdef { 125 if len(values) == 0 { 126 panic(fmt.Sprintf("axis '%v' has empty values list", axis)) 127 } 128 axes = append(axes, axisCache{Id: axis, Vals: values}) 129 } 130 carryOne := false 131 cells := []matrixValue{} 132 for { 133 c := matrixValue{} 134 for i := range axes { 135 if carryOne { 136 carryOne = false 137 axes[i].Count = (axes[i].Count + 1) % len(axes[i].Vals) 138 if axes[i].Count == 0 { // we overflowed--time to carry the one 139 carryOne = true 140 } 141 } 142 // set the current axis/value pair for the new cell 143 c[axes[i].Id] = axes[i].Vals[axes[i].Count] 144 } 145 // if carryOne is still true, that means the final bucket overflowed--we've finished. 146 if carryOne { 147 break 148 } 149 cells = append(cells, c) 150 // add one to the leftmost bucket on the next loop 151 carryOne = true 152 } 153 return cells 154 } 155 156 // evaluatedCopy returns a copy of the definition with its tag selectors evaluated. 157 func (mdef matrixDefinition) evaluatedCopy(ase *axisSelectorEvaluator) (matrixDefinition, []error) { 158 var errs []error 159 cpy := matrixDefinition{} 160 for axis, vals := range mdef { 161 evaluated, evalErrs := evaluateAxisTags(ase, axis, vals) 162 if len(evalErrs) > 0 { 163 errs = append(errs, evalErrs...) 164 continue 165 } 166 cpy[axis] = evaluated 167 } 168 return cpy, errs 169 } 170 171 // contains returns whether a value is contained by a definition. 172 // Note that a value that doesn't contain every matrix axis will still 173 // be evaluated based on the axes that exist. 174 func (mdef matrixDefinition) contains(mv matrixValue) bool { 175 for k, v := range mv { 176 axis, ok := mdef[k] 177 if !ok { 178 return false 179 } 180 if !util.SliceContains(axis, v) { 181 return false 182 } 183 } 184 return true 185 } 186 187 // matrixDefintinos is a helper type for parsing either a single definition 188 // or a slice of definitions from YAML. 189 type matrixDefinitions []matrixDefinition 190 191 // UnmarshalYAML allows the YAML parser to read both a single def or 192 // an array of them into a slice. 193 func (mds *matrixDefinitions) UnmarshalYAML(unmarshal func(interface{}) error) error { 194 var single matrixDefinition 195 if err := unmarshal(&single); err == nil { 196 *mds = matrixDefinitions{single} 197 return nil 198 } 199 var slice []matrixDefinition 200 if err := unmarshal(&slice); err != nil { 201 return err 202 } 203 *mds = slice 204 return nil 205 } 206 207 // contain returns true if *any* of the definitions contain the given value. 208 func (mds matrixDefinitions) contain(v matrixValue) bool { 209 for _, m := range mds { 210 if m.contains(v) { 211 return true 212 } 213 } 214 return false 215 } 216 217 // evaluatedCopies is like evaluatedCopy, but for multiple definitions. 218 func (mds matrixDefinitions) evaluatedCopies(ase *axisSelectorEvaluator) (matrixDefinitions, []error) { 219 var out matrixDefinitions 220 var errs []error 221 for _, md := range mds { 222 evaluated, evalErrs := md.evaluatedCopy(ase) 223 errs = append(errs, evalErrs...) 224 out = append(out, evaluated) 225 } 226 return out, errs 227 } 228 229 // evaluateAxisTags returns an evaluated list of axis value ids with tag selectors evaluated. 230 func evaluateAxisTags(ase *axisSelectorEvaluator, axis string, selectors []string) ([]string, []error) { 231 var errs []error 232 all := map[string]struct{}{} 233 for _, s := range selectors { 234 ids, err := ase.evalSelector(axis, ParseSelector(s)) 235 if err != nil { 236 errs = append(errs, err) 237 continue 238 } 239 for _, id := range ids { 240 all[id] = struct{}{} 241 } 242 } 243 out := []string{} 244 for id, _ := range all { 245 out = append(out, id) 246 } 247 return out, errs 248 } 249 250 // buildMatrixVariants takes in a list of axis definitions, an axisSelectorEvaluator, and a slice of 251 // matrix definitions. It returns a slice of parserBuildVariants constructed according to 252 // our matrix specification. 253 func buildMatrixVariants(axes []matrixAxis, ase *axisSelectorEvaluator, matrices []matrix) ( 254 []parserBV, []error) { 255 var errs []error 256 // for each matrix, build out its declarations 257 matrixVariants := []parserBV{} 258 for i, m := range matrices { 259 // for each axis value, iterate through possible inputs 260 evaluatedSpec, evalErrs := m.Spec.evaluatedCopy(ase) 261 if len(evalErrs) > 0 { 262 errs = append(errs, evalErrs...) 263 continue 264 } 265 evaluatedExcludes, evalErrs := m.Exclude.evaluatedCopies(ase) 266 if len(evalErrs) > 0 { 267 errs = append(errs, evalErrs...) 268 continue 269 } 270 unpruned := evaluatedSpec.allCells() 271 pruned := []parserBV{} 272 for _, cell := range unpruned { 273 // create the variant if it isn't excluded 274 if !evaluatedExcludes.contain(cell) { 275 v, err := buildMatrixVariant(axes, cell, &matrices[i], ase) 276 if err != nil { 277 errs = append(errs, errors.Wrapf(err, "%v: error building matrix cell %v", 278 m.Id, cell)) 279 continue 280 } 281 pruned = append(pruned, *v) 282 } 283 } 284 // safety check to make sure the exclude field is actually working 285 if len(m.Exclude) > 0 && len(unpruned) == len(pruned) { 286 errs = append(errs, errors.Errorf("%v: exclude field did not exclude anything", m.Id)) 287 } 288 matrixVariants = append(matrixVariants, pruned...) 289 } 290 return matrixVariants, errs 291 } 292 293 // buildMatrixVariant does the heavy lifting of building a matrix variant based on axis information. 294 // We do this by iterating over all axes and merging the axis value's settings when applicable. Expansions 295 // are evaluated during this process. Rules are parsed and added to the resulting parserBV for later 296 // execution. 297 func buildMatrixVariant(axes []matrixAxis, mv matrixValue, m *matrix, ase *axisSelectorEvaluator) (*parserBV, error) { 298 v := parserBV{ 299 matrixVal: mv, 300 matrixId: m.Id, 301 Stepback: m.Stepback, 302 BatchTime: m.BatchTime, 303 Modules: m.Modules, 304 RunOn: m.RunOn, 305 Expansions: *command.NewExpansions(mv), 306 } 307 // we declare a separate expansion map for evaluating the display name 308 displayNameExp := command.Expansions{} 309 310 // build up the variant id while iterating through axis values 311 idBuf := bytes.Buffer{} 312 idBuf.WriteString(m.Id) 313 idBuf.WriteString("__") 314 315 // track how many axes we cover, so we know the value is only using real axes 316 usedAxes := 0 317 318 // we must iterate over axis definitions to have a consistent ordering for our axis priority 319 for _, a := range axes { 320 // skip any axes that aren't used in the variant's definition 321 if _, ok := mv[a.Id]; !ok { 322 continue 323 } 324 usedAxes++ 325 axisVal, err := a.find(mv[a.Id]) 326 if err != nil { 327 return nil, err 328 } 329 if err := v.mergeAxisValue(axisVal); err != nil { 330 return nil, errors.Wrapf(err, "processing axis value %v, %v", a.Id, axisVal.Id) 331 } 332 // for display names, fall back to the axis values id so we have *something* 333 if axisVal.DisplayName != "" { 334 displayNameExp.Put(a.Id, axisVal.DisplayName) 335 } else { 336 displayNameExp.Put(a.Id, axisVal.Id) 337 } 338 339 // append to the variant's name 340 idBuf.WriteString(a.Id) 341 idBuf.WriteRune('~') 342 idBuf.WriteString(axisVal.Id) 343 if usedAxes < len(mv) { 344 idBuf.WriteRune('_') 345 } 346 } 347 if usedAxes != len(mv) { 348 // we could make this error more helpful at the expense of extra complexity 349 return nil, errors.Errorf("cell %v uses undefined axes", mv) 350 } 351 v.Name = idBuf.String() 352 disp, err := displayNameExp.ExpandString(m.DisplayName) 353 if err != nil { 354 return nil, errors.Wrap(err, "processing display name") 355 } 356 v.DisplayName = disp 357 358 // add final matrix-level tags and tasks 359 if err := v.mergeAxisValue(axisValue{Tags: m.Tags}); err != nil { 360 return nil, errors.Wrap(err, "processing matrix tags") 361 } 362 for _, t := range m.Tasks { 363 expTask, err := expandParserBVTask(t, v.Expansions) 364 if err != nil { 365 return nil, errors.Wrapf(err, "processing task %s", t.Name) 366 } 367 v.Tasks = append(v.Tasks, expTask) 368 } 369 370 // evaluate rules for matching matrix values 371 for i, rule := range m.Rules { 372 r, err := expandRule(rule, v.Expansions) 373 if err != nil { 374 return nil, errors.Wrapf(err, "processing rule[%d]", i) 375 } 376 matchers, errs := r.If.evaluatedCopies(ase) // we could cache this 377 if len(errs) > 0 { 378 return nil, errors.Errorf("evaluating rules for matrix %v: %v", m.Id, errs) 379 } 380 if matchers.contain(mv) { 381 if r.Then.Set != nil { 382 if err := v.mergeAxisValue(*r.Then.Set); err != nil { 383 return nil, errors.Wrapf(err, "evaluating %s rule %d", m.Id, i) 384 } 385 } 386 // we append add/remove task rules internally and execute them 387 // during task evaluation, when other tasks are being evaluated. 388 if len(r.Then.RemoveTasks) > 0 || len(r.Then.AddTasks) > 0 { 389 v.matrixRules = append(v.matrixRules, r.Then) 390 } 391 } 392 } 393 return &v, nil 394 } 395 396 // matrixRule allows users to manipulate arbitrary matrix values using selectors. 397 type matrixRule struct { 398 If matrixDefinitions `yaml:"if"` 399 Then ruleAction `yaml:"then"` 400 } 401 402 // ruleAction is used to define what work must be done when 403 // "matrixRule.If" is satisfied. 404 type ruleAction struct { 405 Set *axisValue `yaml:"set"` 406 RemoveTasks parserStringSlice `yaml:"remove_tasks"` 407 AddTasks parserBVTasks `yaml:"add_tasks"` 408 } 409 410 // mergeAxisValue overwrites a parserBV's fields based on settings 411 // in the axis value. Matrix expansions are evaluated as this process occurs. 412 // Returns any errors evaluating expansions. 413 func (pbv *parserBV) mergeAxisValue(av axisValue) error { 414 // expand the variant's expansions (woah, dude) and update them 415 if len(av.Variables) > 0 { 416 expanded, err := expandExpansions(av.Variables, pbv.Expansions) 417 if err != nil { 418 return errors.Wrap(err, "expanding variables") 419 } 420 pbv.Expansions.Update(expanded) 421 } 422 // merge tags, removing dupes 423 if len(av.Tags) > 0 { 424 expanded, err := expandStrings(av.Tags, pbv.Expansions) 425 if err != nil { 426 return errors.Wrap(err, "expanding tags") 427 } 428 pbv.Tags = util.UniqueStrings(append(pbv.Tags, expanded...)) 429 } 430 // overwrite run_on 431 var err error 432 if len(av.RunOn) > 0 { 433 pbv.RunOn, err = expandStrings(av.RunOn, pbv.Expansions) 434 if err != nil { 435 return errors.Wrap(err, "expanding run_on") 436 } 437 } 438 // overwrite modules 439 if len(av.Modules) > 0 { 440 pbv.Modules, err = expandStrings(av.Modules, pbv.Expansions) 441 if err != nil { 442 return errors.Wrap(err, "expanding modules") 443 } 444 } 445 if av.Stepback != nil { 446 pbv.Stepback = av.Stepback 447 } 448 if av.BatchTime != nil { 449 pbv.BatchTime = av.BatchTime 450 } 451 return nil 452 } 453 454 // expandStrings expands a slice of strings. 455 func expandStrings(strings []string, exp command.Expansions) ([]string, error) { 456 var expanded []string 457 for _, s := range strings { 458 newS, err := exp.ExpandString(s) 459 if err != nil { 460 return nil, errors.WithStack(err) 461 } 462 expanded = append(expanded, newS) 463 } 464 return expanded, nil 465 } 466 467 // expandExpansions expands expansion maps. 468 func expandExpansions(in, exp command.Expansions) (command.Expansions, error) { 469 newExp := command.Expansions{} 470 for k, v := range in { 471 newK, err := exp.ExpandString(k) 472 if err != nil { 473 return nil, errors.WithStack(err) 474 } 475 newV, err := exp.ExpandString(v) 476 if err != nil { 477 return nil, errors.WithStack(err) 478 } 479 newExp[newK] = newV 480 } 481 return newExp, nil 482 } 483 484 // expandParserBVTask expands strings inside parserBVTs. 485 func expandParserBVTask(pbvt parserBVTask, exp command.Expansions) (parserBVTask, error) { 486 var err error 487 newTask := pbvt 488 newTask.Name, err = exp.ExpandString(pbvt.Name) 489 if err != nil { 490 return parserBVTask{}, errors.Wrap(err, "expanding name") 491 } 492 newTask.RunOn, err = expandStrings(pbvt.RunOn, exp) 493 if err != nil { 494 return parserBVTask{}, errors.Wrap(err, "expanding run_on") 495 } 496 newTask.Distros, err = expandStrings(pbvt.Distros, exp) 497 if err != nil { 498 return parserBVTask{}, errors.Wrap(err, "expanding distros") 499 } 500 var newDeps parserDependencies 501 for i, d := range pbvt.DependsOn { 502 newDep := d 503 newDep.Status, err = exp.ExpandString(d.Status) 504 if err != nil { 505 return parserBVTask{}, errors.Wrapf(err, "expanding depends_on[%d/%d].status", i, len(pbvt.DependsOn)) 506 } 507 newDep.taskSelector, err = expandTaskSelector(d.taskSelector, exp) 508 if err != nil { 509 return parserBVTask{}, errors.Wrapf(err, "expanding depends_on[%d/%d]", i, len(pbvt.DependsOn)) 510 } 511 newDeps = append(newDeps, newDep) 512 } 513 newTask.DependsOn = newDeps 514 var newReqs taskSelectors 515 for i, r := range pbvt.Requires { 516 newReq, err := expandTaskSelector(r, exp) 517 if err != nil { 518 return parserBVTask{}, errors.Wrapf(err, "expanding requires[%d/%d]", i, len(pbvt.Requires)) 519 } 520 newReqs = append(newReqs, newReq) 521 } 522 newTask.Requires = newReqs 523 return newTask, nil 524 } 525 526 // expandTaskSelector expands strings inside task selectors. 527 func expandTaskSelector(ts taskSelector, exp command.Expansions) (taskSelector, error) { 528 newTS := taskSelector{} 529 newName, err := exp.ExpandString(ts.Name) 530 if err != nil { 531 return newTS, errors.Wrap(err, "expanding name") 532 } 533 newTS.Name = newName 534 if v := ts.Variant; v != nil { 535 if len(v.matrixSelector) > 0 { 536 newMS, err := expandMatrixDefinition(v.matrixSelector, exp) 537 if err != nil { 538 return newTS, errors.Wrap(err, "expanding variant") 539 } 540 newTS.Variant = &variantSelector{ 541 matrixSelector: newMS, 542 } 543 } else { 544 selector, err := exp.ExpandString(v.stringSelector) 545 if err != nil { 546 return newTS, errors.Wrap(err, "expanding variant") 547 } 548 newTS.Variant = &variantSelector{ 549 stringSelector: selector, 550 } 551 } 552 } 553 return newTS, nil 554 } 555 556 // expandMatrixDefinition expands strings inside matrix definitions. 557 func expandMatrixDefinition(md matrixDefinition, exp command.Expansions) (matrixDefinition, error) { 558 var err error 559 newMS := matrixDefinition{} 560 for axis, vals := range md { 561 newMS[axis], err = expandStrings(vals, exp) 562 if err != nil { 563 return nil, errors.Wrap(err, "matrix selector") 564 } 565 } 566 return newMS, nil 567 } 568 569 // expandRules expands strings inside of rules. 570 func expandRule(r matrixRule, exp command.Expansions) (matrixRule, error) { 571 newR := matrixRule{} 572 for _, md := range r.If { 573 newIf, err := expandMatrixDefinition(md, exp) 574 if err != nil { 575 return newR, errors.Wrap(err, "if") 576 } 577 newR.If = append(newR.If, newIf) 578 } 579 for _, t := range r.Then.AddTasks { 580 newTask, err := expandParserBVTask(t, exp) 581 if err != nil { 582 return newR, errors.Wrap(err, "add_tasks") 583 } 584 newR.Then.AddTasks = append(newR.Then.AddTasks, newTask) 585 } 586 if len(r.Then.RemoveTasks) > 0 { 587 var err error 588 newR.Then.RemoveTasks, err = expandStrings(r.Then.RemoveTasks, exp) 589 if err != nil { 590 return newR, errors.Wrap(err, "remove_tasks") 591 } 592 } 593 // r.Then.Set will be taken care of when mergeAxisValue is called 594 // so we don't have to do it in this function 595 return newR, nil 596 }