github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/validator/project_validator.go (about) 1 package validator 2 3 import ( 4 "fmt" 5 "strconv" 6 "strings" 7 8 "github.com/evergreen-ci/evergreen" 9 "github.com/evergreen-ci/evergreen/model" 10 "github.com/evergreen-ci/evergreen/model/distro" 11 "github.com/evergreen-ci/evergreen/plugin" 12 "github.com/evergreen-ci/evergreen/util" 13 "github.com/pkg/errors" 14 ) 15 16 type projectValidator func(*model.Project) []ValidationError 17 18 type ValidationErrorLevel int64 19 20 const ( 21 Error ValidationErrorLevel = iota 22 Warning 23 ) 24 25 func (vel ValidationErrorLevel) String() string { 26 switch vel { 27 case Error: 28 return "ERROR" 29 case Warning: 30 return "WARNING" 31 } 32 return "?" 33 } 34 35 type ValidationError struct { 36 Level ValidationErrorLevel `json:"level"` 37 Message string `json:"message"` 38 } 39 40 // Functions used to validate the syntax of a project configuration file. Any 41 // validation errors here for remote configuration files are fatal and will 42 // cause stubs to be created for the project. 43 var projectSyntaxValidators = []projectValidator{ 44 ensureHasNecessaryBVFields, 45 checkDependencyGraph, 46 validatePluginCommands, 47 ensureHasNecessaryProjectFields, 48 verifyTaskDependencies, 49 verifyTaskRequirements, 50 validateBVNames, 51 validateBVTaskNames, 52 checkAllDependenciesSpec, 53 validateProjectTaskNames, 54 validateProjectTaskIdsAndTags, 55 } 56 57 // Functions used to validate the semantics of a project configuration file. 58 // Validations errors here are not fatal. However, it is recommended that the 59 // suggested corrections are applied. 60 var projectSemanticValidators = []projectValidator{ 61 checkTaskCommands, 62 } 63 64 func (vr ValidationError) Error() string { 65 return vr.Message 66 } 67 68 // create a slice of all valid distro names 69 func getDistroIds() ([]string, error) { 70 // create a slice of all known distros 71 distros, err := distro.Find(distro.All) 72 if err != nil { 73 return nil, err 74 } 75 distroIds := []string{} 76 for _, d := range distros { 77 if !util.SliceContains(distroIds, d.Id) { 78 distroIds = append(distroIds, d.Id) 79 } 80 } 81 return distroIds, nil 82 } 83 84 // verify that the project configuration semantics is valid 85 func CheckProjectSemantics(project *model.Project) []ValidationError { 86 87 validationErrs := []ValidationError{} 88 for _, projectSemanticValidator := range projectSemanticValidators { 89 validationErrs = append(validationErrs, 90 projectSemanticValidator(project)...) 91 } 92 return validationErrs 93 } 94 95 // verify that the project configuration syntax is valid 96 func CheckProjectSyntax(project *model.Project) ([]ValidationError, error) { 97 98 validationErrs := []ValidationError{} 99 for _, projectSyntaxValidator := range projectSyntaxValidators { 100 validationErrs = append(validationErrs, 101 projectSyntaxValidator(project)...) 102 } 103 104 // get distroIds for ensureReferentialIntegrity validation 105 distroIds, err := getDistroIds() 106 if err != nil { 107 return nil, err 108 } 109 validationErrs = append(validationErrs, ensureReferentialIntegrity(project, distroIds)...) 110 return validationErrs, nil 111 } 112 113 // ensure that if any task spec references 'model.AllDependencies', it 114 // references no other dependency 115 func checkAllDependenciesSpec(project *model.Project) []ValidationError { 116 errs := []ValidationError{} 117 for _, task := range project.Tasks { 118 if len(task.DependsOn) > 1 { 119 for _, dependency := range task.DependsOn { 120 if dependency.Name == model.AllDependencies { 121 errs = append(errs, 122 ValidationError{ 123 Message: fmt.Sprintf("task '%v' in project '%v' "+ 124 "contains the all dependencies (%v)' "+ 125 "specification and other explicit dependencies", 126 project.Identifier, task.Name, 127 model.AllDependencies), 128 }, 129 ) 130 } 131 } 132 } 133 } 134 return errs 135 } 136 137 // Makes sure that the dependencies for the tasks in the project form a 138 // valid dependency graph (no cycles). 139 func checkDependencyGraph(project *model.Project) []ValidationError { 140 errs := []ValidationError{} 141 142 // map of task name and variant -> BuildVariantTask 143 tasksByNameAndVariant := map[model.TVPair]model.BuildVariantTask{} 144 145 // generate task nodes for every task and variant combination 146 visited := map[model.TVPair]bool{} 147 allNodes := []model.TVPair{} 148 for _, bv := range project.BuildVariants { 149 for _, t := range bv.Tasks { 150 t.Populate(project.GetSpecForTask(t.Name)) 151 node := model.TVPair{bv.Name, t.Name} 152 153 tasksByNameAndVariant[node] = t 154 visited[node] = false 155 allNodes = append(allNodes, node) 156 } 157 } 158 159 // run through the task nodes, checking their dependency graphs for cycles 160 for _, node := range allNodes { 161 // the visited nodes 162 if err := dependencyCycleExists(node, visited, tasksByNameAndVariant); err != nil { 163 errs = append(errs, 164 ValidationError{ 165 Message: fmt.Sprintf( 166 "dependency error for '%v' task: %v", node.TaskName, err), 167 }, 168 ) 169 } 170 } 171 172 return errs 173 } 174 175 // Helper for checking the dependency graph for cycles. 176 func dependencyCycleExists(node model.TVPair, visited map[model.TVPair]bool, 177 tasksByNameAndVariant map[model.TVPair]model.BuildVariantTask) error { 178 179 v, ok := visited[node] 180 // if the node does not exist, the deps are broken 181 if !ok { 182 return errors.Errorf("dependency %v is not present in the project config", node) 183 } 184 // if the task has already been visited, then a cycle certainly exists 185 if v { 186 return errors.Errorf("dependency %v is part of a dependency cycle", node) 187 } 188 189 visited[node] = true 190 191 task := tasksByNameAndVariant[node] 192 depNodes := []model.TVPair{} 193 // build a list of all possible dependency nodes for the task 194 for _, dep := range task.DependsOn { 195 if dep.Variant != model.AllVariants { 196 // handle regular dependencies 197 dn := model.TVPair{TaskName: dep.Name} 198 if dep.Variant == "" { 199 // use the current variant if none is specified 200 dn.Variant = node.Variant 201 } else { 202 dn.Variant = dep.Variant 203 } 204 // handle * case by grabbing all the variant's tasks that aren't the current one 205 if dn.TaskName == model.AllDependencies { 206 for n := range visited { 207 if n.TaskName != node.TaskName && n.Variant == dn.Variant { 208 depNodes = append(depNodes, n) 209 } 210 } 211 } else { 212 // normal case: just append the variant 213 depNodes = append(depNodes, dn) 214 } 215 } else { 216 // handle the all-variants case by adding all nodes that are 217 // of the same task (but not the current node) 218 if dep.Name != model.AllDependencies { 219 for n := range visited { 220 if n.TaskName == dep.Name && (n != node) { 221 depNodes = append(depNodes, n) 222 } 223 } 224 } else { 225 // edge case where variant and task name are both * 226 for n := range visited { 227 if n != node { 228 depNodes = append(depNodes, n) 229 } 230 } 231 } 232 } 233 } 234 235 // for each of the task's dependencies, make a recursive call 236 for _, dn := range depNodes { 237 if err := dependencyCycleExists(dn, visited, tasksByNameAndVariant); err != nil { 238 return err 239 } 240 } 241 242 // remove the task from the visited map so that higher-level calls do not see it 243 visited[node] = false 244 245 // no cycle found 246 return nil 247 } 248 249 // Ensures that the project has at least one buildvariant and also that all the 250 // fields required for any buildvariant definition are present 251 func ensureHasNecessaryBVFields(project *model.Project) []ValidationError { 252 errs := []ValidationError{} 253 if len(project.BuildVariants) == 0 { 254 return []ValidationError{ 255 { 256 Message: fmt.Sprintf("project '%v' must specify at least one "+ 257 "buildvariant", project.Identifier), 258 }, 259 } 260 } 261 262 for _, buildVariant := range project.BuildVariants { 263 hasTaskWithoutDistro := false 264 if buildVariant.Name == "" { 265 errs = append(errs, 266 ValidationError{ 267 Message: fmt.Sprintf("project '%v' buildvariant must "+ 268 "have a name", project.Identifier), 269 }, 270 ) 271 } 272 if len(buildVariant.Tasks) == 0 { 273 errs = append(errs, 274 ValidationError{ 275 Message: fmt.Sprintf("buildvariant '%v' in project '%v' "+ 276 "must have at least one task", buildVariant.Name, 277 project.Identifier), 278 }, 279 ) 280 } 281 for _, task := range buildVariant.Tasks { 282 if len(task.Distros) == 0 { 283 hasTaskWithoutDistro = true 284 break 285 } 286 } 287 if hasTaskWithoutDistro && len(buildVariant.RunOn) == 0 { 288 errs = append(errs, 289 ValidationError{ 290 Message: fmt.Sprintf("buildvariant '%v' in project '%v' "+ 291 "must either specify run_on field or have every task "+ 292 "specify a distro.", 293 buildVariant.Name, project.Identifier), 294 }, 295 ) 296 } 297 } 298 return errs 299 } 300 301 // Checks that the basic fields that are required by any project are present. 302 func ensureHasNecessaryProjectFields(project *model.Project) []ValidationError { 303 errs := []ValidationError{} 304 305 if project.BatchTime < 0 { 306 errs = append(errs, 307 ValidationError{ 308 Message: fmt.Sprintf("project '%v' must have a "+ 309 "non-negative 'batchtime' set", project.Identifier), 310 }, 311 ) 312 } 313 314 if project.CommandType != "" { 315 if project.CommandType != model.SystemCommandType && 316 project.CommandType != model.TestCommandType { 317 errs = append(errs, 318 ValidationError{ 319 Message: fmt.Sprintf("project '%v' contains an invalid "+ 320 "command type: %v", project.Identifier, project.CommandType), 321 }, 322 ) 323 } 324 } 325 return errs 326 } 327 328 // Ensures that: 329 // 1. a referenced task within a buildvariant task object exists in 330 // the set of project tasks 331 // 2. any referenced distro exists within the current setting's distro directory 332 func ensureReferentialIntegrity(project *model.Project, distroIds []string) []ValidationError { 333 errs := []ValidationError{} 334 // create a set of all the task names 335 allTaskNames := map[string]bool{} 336 for _, task := range project.Tasks { 337 allTaskNames[task.Name] = true 338 } 339 340 for _, buildVariant := range project.BuildVariants { 341 buildVariantTasks := map[string]bool{} 342 for _, task := range buildVariant.Tasks { 343 if _, ok := allTaskNames[task.Name]; !ok { 344 if task.Name == "" { 345 errs = append(errs, 346 ValidationError{ 347 Message: fmt.Sprintf("tasks for buildvariant '%v' "+ 348 "in project '%v' must each have a name field", 349 project.Identifier, buildVariant.Name), 350 }, 351 ) 352 } else { 353 errs = append(errs, 354 ValidationError{ 355 Message: fmt.Sprintf("buildvariant '%v' in "+ 356 "project '%v' references a non-existent "+ 357 "task '%v'", buildVariant.Name, 358 project.Identifier, task.Name), 359 }, 360 ) 361 } 362 } 363 buildVariantTasks[task.Name] = true 364 for _, distroId := range task.Distros { 365 if !util.SliceContains(distroIds, distroId) { 366 errs = append(errs, 367 ValidationError{ 368 Message: fmt.Sprintf("task '%v' in buildvariant "+ 369 "'%v' in project '%v' references a "+ 370 "non-existent distro '%v'.\nValid distros "+ 371 "include: \n\t- %v", task.Name, 372 buildVariant.Name, project.Identifier, 373 distroId, strings.Join(distroIds, "\n\t- ")), 374 Level: Warning, 375 }, 376 ) 377 } 378 } 379 } 380 for _, distroId := range buildVariant.RunOn { 381 if !util.SliceContains(distroIds, distroId) { 382 errs = append(errs, 383 ValidationError{ 384 Message: fmt.Sprintf("buildvariant '%v' in project "+ 385 "'%v' references a non-existent distro '%v'.\n"+ 386 "Valid distros include: \n\t- %v", 387 buildVariant.Name, project.Identifier, distroId, 388 strings.Join(distroIds, "\n\t- ")), 389 Level: Warning, 390 }, 391 ) 392 } 393 } 394 } 395 return errs 396 } 397 398 // Ensures there aren't any duplicate buildvariant names specified in the given 399 // project 400 func validateBVNames(project *model.Project) []ValidationError { 401 errs := []ValidationError{} 402 buildVariantNames := map[string]bool{} 403 displayNames := map[string]int{} 404 405 for _, buildVariant := range project.BuildVariants { 406 if _, ok := buildVariantNames[buildVariant.Name]; ok { 407 errs = append(errs, 408 ValidationError{ 409 Message: fmt.Sprintf("project '%v' buildvariant '%v' already exists", 410 project.Identifier, buildVariant.Name), 411 }, 412 ) 413 } 414 buildVariantNames[buildVariant.Name] = true 415 dispName := buildVariant.DisplayName 416 if dispName == "" { // Default display name to the actual name (identifier) 417 dispName = buildVariant.Name 418 } 419 displayNames[dispName] = displayNames[dispName] + 1 420 } 421 // don't bother checking for the warnings if we already found errors 422 if len(errs) > 0 { 423 return errs 424 } 425 for k, v := range displayNames { 426 if v > 1 { 427 errs = append(errs, 428 ValidationError{ 429 Level: Warning, 430 Message: fmt.Sprintf("%v build variants share the same display name: '%v'", v, k), 431 }, 432 ) 433 434 } 435 } 436 return errs 437 } 438 439 // Checks each task definitions to determine if a command is specified 440 func checkTaskCommands(project *model.Project) []ValidationError { 441 errs := []ValidationError{} 442 for _, task := range project.Tasks { 443 if len(task.Commands) == 0 { 444 errs = append(errs, 445 ValidationError{ 446 Message: fmt.Sprintf("task '%v' in project '%v' does not "+ 447 "contain any commands", 448 task.Name, project.Identifier), 449 Level: Warning, 450 }, 451 ) 452 } 453 } 454 return errs 455 } 456 457 // Ensures there aren't any duplicate task names specified for any buildvariant 458 // in this project 459 func validateBVTaskNames(project *model.Project) []ValidationError { 460 errs := []ValidationError{} 461 for _, buildVariant := range project.BuildVariants { 462 buildVariantTasks := map[string]bool{} 463 for _, task := range buildVariant.Tasks { 464 if _, ok := buildVariantTasks[task.Name]; ok { 465 errs = append(errs, 466 ValidationError{ 467 Message: fmt.Sprintf("task '%v' in buildvariant '%v' "+ 468 "in project '%v' already exists", 469 task.Name, buildVariant.Name, project.Identifier), 470 }, 471 ) 472 } 473 buildVariantTasks[task.Name] = true 474 } 475 } 476 return errs 477 } 478 479 // Helper for validating a set of plugin commands given a project/registry 480 func validateCommands(section string, project *model.Project, registry plugin.Registry, 481 commands []model.PluginCommandConf) []ValidationError { 482 errs := []ValidationError{} 483 484 for _, cmd := range commands { 485 command := fmt.Sprintf("'%v' command", cmd.Command) 486 _, err := registry.GetCommands(cmd, project.Functions) 487 if err != nil { 488 if cmd.Function != "" { 489 command = fmt.Sprintf("'%v' function", cmd.Function) 490 } 491 errs = append(errs, ValidationError{Message: fmt.Sprintf("%v section in %v: %v", section, command, err)}) 492 } 493 if cmd.Type != "" { 494 if cmd.Type != model.SystemCommandType && 495 cmd.Type != model.TestCommandType { 496 msg := fmt.Sprintf("%v section in '%v': invalid command type: '%v'", section, command, cmd.Type) 497 errs = append(errs, ValidationError{Message: msg}) 498 } 499 } 500 } 501 return errs 502 } 503 504 // Ensures there any plugin commands referenced in a project's configuration 505 // are specified in a valid format 506 func validatePluginCommands(project *model.Project) []ValidationError { 507 errs := []ValidationError{} 508 pluginRegistry := plugin.NewSimpleRegistry() 509 510 // register the published plugins 511 for _, pl := range plugin.CommandPlugins { 512 if err := pluginRegistry.Register(pl); err != nil { 513 errs = append(errs, 514 ValidationError{ 515 Message: fmt.Sprintf("failed to register plugin %v: %v", pl.Name(), err), 516 }, 517 ) 518 } 519 } 520 521 seen := make(map[string]bool) 522 523 // validate each function definition 524 for funcName, commands := range project.Functions { 525 valErrs := validateCommands("functions", project, pluginRegistry, commands.List()) 526 for _, err := range valErrs { 527 errs = append(errs, 528 ValidationError{ 529 Message: fmt.Sprintf("'%v' project's '%v' definition: %v", 530 project.Identifier, funcName, err), 531 }, 532 ) 533 } 534 535 for _, c := range commands.List() { 536 if c.Function != "" { 537 errs = append(errs, 538 ValidationError{ 539 Message: fmt.Sprintf("can not reference a function within a "+ 540 "function: '%v' referenced within '%v'", c.Function, funcName), 541 }, 542 ) 543 544 } 545 } 546 547 // this checks for duplicate function definitions in the project. 548 if seen[funcName] { 549 errs = append(errs, 550 ValidationError{ 551 Message: fmt.Sprintf(`project '%v' has duplicate definition of "%v"`, 552 project.Identifier, funcName), 553 }, 554 ) 555 } 556 seen[funcName] = true 557 } 558 559 if project.Pre != nil { 560 // validate project pre section 561 errs = append(errs, validateCommands("pre", project, pluginRegistry, project.Pre.List())...) 562 } 563 564 if project.Post != nil { 565 // validate project post section 566 errs = append(errs, validateCommands("post", project, pluginRegistry, project.Post.List())...) 567 } 568 569 if project.Timeout != nil { 570 // validate project timeout section 571 errs = append(errs, validateCommands("timeout", project, pluginRegistry, project.Timeout.List())...) 572 } 573 574 // validate project tasks section 575 for _, task := range project.Tasks { 576 errs = append(errs, validateCommands("tasks", project, pluginRegistry, task.Commands)...) 577 } 578 return errs 579 } 580 581 // Ensures there aren't any duplicate task names for this project 582 func validateProjectTaskNames(project *model.Project) []ValidationError { 583 errs := []ValidationError{} 584 // create a map to hold the task names 585 taskNames := map[string]bool{} 586 for _, task := range project.Tasks { 587 if _, ok := taskNames[task.Name]; ok { 588 errs = append(errs, 589 ValidationError{ 590 Message: fmt.Sprintf("task '%v' in project '%v' "+ 591 "already exists", task.Name, project.Identifier), 592 }, 593 ) 594 } 595 taskNames[task.Name] = true 596 } 597 return errs 598 } 599 600 // validateProjectTaskIdsAndTags ensures that task tags and ids only contain valid characters 601 func validateProjectTaskIdsAndTags(project *model.Project) []ValidationError { 602 errs := []ValidationError{} 603 // create a map to hold the task names 604 for _, task := range project.Tasks { 605 // check task name 606 if i := strings.IndexAny(task.Name, model.InvalidCriterionRunes); i == 0 { 607 errs = append(errs, ValidationError{ 608 Message: fmt.Sprintf("task '%v' has invalid name: starts with invalid character %v", 609 task.Name, strconv.QuoteRune(rune(task.Name[0])))}) 610 } 611 // check tag names 612 for _, tag := range task.Tags { 613 if i := strings.IndexAny(tag, model.InvalidCriterionRunes); i == 0 { 614 errs = append(errs, ValidationError{ 615 Message: fmt.Sprintf("task '%v' has invalid tag '%v': starts with invalid character %v", 616 task.Name, tag, strconv.QuoteRune(rune(tag[0])))}) 617 } 618 if i := util.IndexWhiteSpace(tag); i != -1 { 619 errs = append(errs, ValidationError{ 620 Message: fmt.Sprintf("task '%v' has invalid tag '%v': tag contains white space", 621 task.Name, tag)}) 622 } 623 } 624 } 625 return errs 626 } 627 628 // Makes sure that the dependencies for the tasks have the correct fields, 629 // and that the fields reference valid tasks. 630 func verifyTaskRequirements(project *model.Project) []ValidationError { 631 errs := []ValidationError{} 632 for _, bvt := range project.FindAllBuildVariantTasks() { 633 for _, r := range bvt.Requires { 634 if project.FindProjectTask(r.Name) == nil { 635 if r.Name == model.AllDependencies { 636 errs = append(errs, ValidationError{Message: fmt.Sprintf( 637 "task '%v': * is not supported for requirement selectors", bvt.Name)}) 638 } else { 639 errs = append(errs, 640 ValidationError{Message: fmt.Sprintf( 641 "task '%v' requires non-existent task '%v'", bvt.Name, r.Name)}) 642 } 643 } 644 if r.Variant != "" && r.Variant != model.AllVariants && project.FindBuildVariant(r.Variant) == nil { 645 errs = append(errs, ValidationError{Message: fmt.Sprintf( 646 "task '%v' requires non-existent variant '%v'", bvt.Name, r.Variant)}) 647 } 648 } 649 } 650 return errs 651 } 652 653 // Makes sure that the dependencies for the tasks have the correct fields, 654 // and that the fields have valid values 655 func verifyTaskDependencies(project *model.Project) []ValidationError { 656 errs := []ValidationError{} 657 // create a set of all the task names 658 taskNames := map[string]bool{} 659 for _, task := range project.Tasks { 660 taskNames[task.Name] = true 661 } 662 663 for _, task := range project.Tasks { 664 // create a set of the dependencies, to check for duplicates 665 depNames := map[model.TVPair]bool{} 666 667 for _, dep := range task.DependsOn { 668 // make sure the dependency is not specified more than once 669 if depNames[model.TVPair{dep.Name, dep.Variant}] { 670 errs = append(errs, 671 ValidationError{ 672 Message: fmt.Sprintf("project '%v' contains a "+ 673 "duplicate dependency '%v' specified for task '%v'", 674 project.Identifier, dep.Name, task.Name), 675 }, 676 ) 677 } 678 depNames[model.TVPair{dep.Name, dep.Variant}] = true 679 680 // check that the status is valid 681 switch dep.Status { 682 case evergreen.TaskSucceeded, evergreen.TaskFailed, model.AllStatuses, "": 683 // these are all valid 684 default: 685 errs = append(errs, 686 ValidationError{ 687 Message: fmt.Sprintf("project '%v' contains an invalid dependency status for task '%v': %v", 688 project.Identifier, task.Name, dep.Status)}) 689 } 690 691 // check that name of the dependency task is valid 692 if dep.Name != model.AllDependencies && !taskNames[dep.Name] { 693 errs = append(errs, 694 ValidationError{ 695 Message: fmt.Sprintf("project '%v' contains a "+ 696 "non-existent task name '%v' in dependencies for "+ 697 "task '%v'", project.Identifier, dep.Name, 698 task.Name), 699 }, 700 ) 701 } 702 } 703 } 704 return errs 705 }