github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/configvalidate/validate.go (about) 1 package configvalidate 2 3 import ( 4 "errors" 5 "fmt" 6 "net/url" 7 "strings" 8 9 "github.com/pf-qiu/concourse/v6/atc" 10 . "github.com/pf-qiu/concourse/v6/atc" 11 "github.com/pf-qiu/concourse/v6/atc/creds" 12 "github.com/gobwas/glob" 13 ) 14 15 func formatErr(groupName string, err error) string { 16 lines := strings.Split(err.Error(), "\n") 17 indented := make([]string, len(lines)) 18 19 for i, l := range lines { 20 indented[i] = "\t" + l 21 } 22 23 return fmt.Sprintf("invalid %s:\n%s\n", groupName, strings.Join(indented, "\n")) 24 } 25 26 func Validate(c Config) ([]ConfigWarning, []string) { 27 warnings := []ConfigWarning{} 28 errorMessages := []string{} 29 30 groupsWarnings, groupsErr := validateGroups(c) 31 if groupsErr != nil { 32 errorMessages = append(errorMessages, formatErr("groups", groupsErr)) 33 } 34 warnings = append(warnings, groupsWarnings...) 35 36 resourcesWarnings, resourcesErr := validateResources(c) 37 if resourcesErr != nil { 38 errorMessages = append(errorMessages, formatErr("resources", resourcesErr)) 39 } 40 warnings = append(warnings, resourcesWarnings...) 41 42 resourceTypesWarnings, resourceTypesErr := validateResourceTypes(c) 43 if resourceTypesErr != nil { 44 errorMessages = append(errorMessages, formatErr("resource types", resourceTypesErr)) 45 } 46 warnings = append(warnings, resourceTypesWarnings...) 47 48 varSourcesWarnings, varSourcesErr := validateVarSources(c) 49 if varSourcesErr != nil { 50 errorMessages = append(errorMessages, formatErr("variable sources", varSourcesErr)) 51 } 52 warnings = append(warnings, varSourcesWarnings...) 53 54 jobWarnings, jobsErr := validateJobs(c) 55 if jobsErr != nil { 56 errorMessages = append(errorMessages, formatErr("jobs", jobsErr)) 57 } 58 warnings = append(warnings, jobWarnings...) 59 60 displayWarnings, displayErr := validateDisplay(c) 61 if displayErr != nil { 62 errorMessages = append(errorMessages, formatErr("display config", displayErr)) 63 } 64 warnings = append(warnings, displayWarnings...) 65 66 return warnings, errorMessages 67 } 68 69 func validateGroups(c Config) ([]ConfigWarning, error) { 70 var warnings []ConfigWarning 71 var errorMessages []string 72 73 jobsGrouped := make(map[string]bool) 74 groupNames := make(map[string]int) 75 76 for _, job := range c.Jobs { 77 jobsGrouped[job.Name] = false 78 } 79 80 for i, group := range c.Groups { 81 var identifier string 82 if group.Name == "" { 83 identifier = fmt.Sprintf("groups[%d]", i) 84 } else { 85 identifier = fmt.Sprintf("groups.%s", group.Name) 86 } 87 88 warning := ValidateIdentifier(group.Name, identifier) 89 if warning != nil { 90 warnings = append(warnings, *warning) 91 } 92 93 if val, ok := groupNames[group.Name]; ok { 94 groupNames[group.Name] = val + 1 95 96 } else { 97 groupNames[group.Name] = 1 98 } 99 100 for _, jobGlob := range group.Jobs { 101 matchingJob := false 102 g, err := glob.Compile(jobGlob) 103 if err != nil { 104 errorMessages = append(errorMessages, 105 fmt.Sprintf("invalid glob expression '%s' for group '%s'", jobGlob, group.Name)) 106 continue 107 } 108 for _, job := range c.Jobs { 109 if g.Match(job.Name) { 110 jobsGrouped[job.Name] = true 111 matchingJob = true 112 } 113 } 114 if !matchingJob { 115 errorMessages = append(errorMessages, 116 fmt.Sprintf("no jobs match '%s' for group '%s'", jobGlob, group.Name)) 117 } 118 } 119 120 for _, resource := range group.Resources { 121 _, exists := c.Resources.Lookup(resource) 122 if !exists { 123 errorMessages = append(errorMessages, 124 fmt.Sprintf("group '%s' has unknown resource '%s'", group.Name, resource)) 125 } 126 } 127 } 128 129 for groupName, groupCount := range groupNames { 130 if groupCount > 1 { 131 errorMessages = append(errorMessages, 132 fmt.Sprintf("group '%s' appears %d times. Duplicate names are not allowed.", groupName, groupCount)) 133 } 134 } 135 136 if len(c.Groups) != 0 { 137 for job, grouped := range jobsGrouped { 138 if !grouped { 139 errorMessages = append(errorMessages, fmt.Sprintf("job '%s' belongs to no group", job)) 140 } 141 } 142 } 143 144 return warnings, compositeErr(errorMessages) 145 } 146 147 func validateResources(c Config) ([]ConfigWarning, error) { 148 var warnings []ConfigWarning 149 var errorMessages []string 150 151 names := map[string]int{} 152 153 for i, resource := range c.Resources { 154 var identifier string 155 if resource.Name == "" { 156 identifier = fmt.Sprintf("resources[%d]", i) 157 } else { 158 identifier = fmt.Sprintf("resources.%s", resource.Name) 159 } 160 161 warning := ValidateIdentifier(resource.Name, identifier) 162 if warning != nil { 163 warnings = append(warnings, *warning) 164 } 165 166 if other, exists := names[resource.Name]; exists { 167 errorMessages = append(errorMessages, 168 fmt.Sprintf( 169 "resources[%d] and resources[%d] have the same name ('%s')", 170 other, i, resource.Name)) 171 } else if resource.Name != "" { 172 names[resource.Name] = i 173 } 174 175 if resource.Name == "" { 176 errorMessages = append(errorMessages, identifier+" has no name") 177 } 178 179 if resource.Type == "" { 180 errorMessages = append(errorMessages, identifier+" has no type") 181 } 182 } 183 184 errorMessages = append(errorMessages, validateResourcesUnused(c)...) 185 186 return warnings, compositeErr(errorMessages) 187 } 188 189 func validateResourceTypes(c Config) ([]ConfigWarning, error) { 190 var warnings []ConfigWarning 191 var errorMessages []string 192 193 names := map[string]int{} 194 195 for i, resourceType := range c.ResourceTypes { 196 var identifier string 197 if resourceType.Name == "" { 198 identifier = fmt.Sprintf("resource_types[%d]", i) 199 } else { 200 identifier = fmt.Sprintf("resource_types.%s", resourceType.Name) 201 } 202 203 warning := ValidateIdentifier(resourceType.Name, identifier) 204 if warning != nil { 205 warnings = append(warnings, *warning) 206 } 207 208 if other, exists := names[resourceType.Name]; exists { 209 errorMessages = append(errorMessages, 210 fmt.Sprintf( 211 "resource_types[%d] and resource_types[%d] have the same name ('%s')", 212 other, i, resourceType.Name)) 213 } else if resourceType.Name != "" { 214 names[resourceType.Name] = i 215 } 216 217 if resourceType.Name == "" { 218 errorMessages = append(errorMessages, identifier+" has no name") 219 } 220 221 if resourceType.Type == "" { 222 errorMessages = append(errorMessages, identifier+" has no type") 223 } 224 } 225 226 return warnings, compositeErr(errorMessages) 227 } 228 229 func validateResourcesUnused(c Config) []string { 230 usedResources := usedResources(c) 231 232 var errorMessages []string 233 for _, resource := range c.Resources { 234 if _, used := usedResources[resource.Name]; !used { 235 message := fmt.Sprintf("resource '%s' is not used", resource.Name) 236 errorMessages = append(errorMessages, message) 237 } 238 } 239 240 return errorMessages 241 } 242 243 func usedResources(c Config) map[string]bool { 244 usedResources := make(map[string]bool) 245 246 for _, job := range c.Jobs { 247 _ = job.StepConfig().Visit(atc.StepRecursor{ 248 OnGet: func(step *GetStep) error { 249 usedResources[step.ResourceName()] = true 250 return nil 251 }, 252 OnPut: func(step *PutStep) error { 253 usedResources[step.ResourceName()] = true 254 return nil 255 }, 256 }) 257 } 258 259 return usedResources 260 } 261 262 func validateJobs(c Config) ([]ConfigWarning, error) { 263 var errorMessages []string 264 var warnings []ConfigWarning 265 266 names := map[string]int{} 267 268 if len(c.Jobs) == 0 { 269 errorMessages = append(errorMessages, "jobs: pipeline must contain at least one job") 270 return warnings, compositeErr(errorMessages) 271 } 272 273 for i, job := range c.Jobs { 274 var identifier string 275 if job.Name == "" { 276 identifier = fmt.Sprintf("jobs[%d]", i) 277 } else { 278 identifier = fmt.Sprintf("jobs.%s", job.Name) 279 } 280 281 warning := ValidateIdentifier(job.Name, identifier) 282 if warning != nil { 283 warnings = append(warnings, *warning) 284 } 285 286 if other, exists := names[job.Name]; exists { 287 errorMessages = append(errorMessages, 288 fmt.Sprintf( 289 "jobs[%d] and jobs[%d] have the same name ('%s')", 290 other, i, job.Name)) 291 } else if job.Name != "" { 292 names[job.Name] = i 293 } 294 295 if job.Name == "" { 296 errorMessages = append(errorMessages, identifier+" has no name") 297 } 298 299 if job.BuildLogRetention != nil && job.BuildLogsToRetain != 0 { 300 errorMessages = append( 301 errorMessages, 302 identifier+fmt.Sprintf(" can't use both build_log_retention and build_logs_to_retain"), 303 ) 304 } else if job.BuildLogsToRetain < 0 { 305 errorMessages = append( 306 errorMessages, 307 identifier+fmt.Sprintf(" has negative build_logs_to_retain: %d", job.BuildLogsToRetain), 308 ) 309 } 310 311 if job.BuildLogRetention != nil { 312 if job.BuildLogRetention.Builds < 0 { 313 errorMessages = append( 314 errorMessages, 315 identifier+fmt.Sprintf(" has negative build_log_retention.builds: %d", job.BuildLogRetention.Builds), 316 ) 317 } 318 if job.BuildLogRetention.Days < 0 { 319 errorMessages = append( 320 errorMessages, 321 identifier+fmt.Sprintf(" has negative build_log_retention.days: %d", job.BuildLogRetention.Days), 322 ) 323 } 324 if job.BuildLogRetention.MinimumSucceededBuilds < 0 { 325 errorMessages = append( 326 errorMessages, 327 identifier+fmt.Sprintf(" has negative build_log_retention.min_success_builds: %d", job.BuildLogRetention.MinimumSucceededBuilds), 328 ) 329 } 330 if job.BuildLogRetention.Builds > 0 && job.BuildLogRetention.MinimumSucceededBuilds > job.BuildLogRetention.Builds { 331 errorMessages = append( 332 errorMessages, 333 identifier+fmt.Sprintf(" has build_log_retention.min_success_builds: %d greater than build_log_retention.min_success_builds: %d", job.BuildLogRetention.MinimumSucceededBuilds, job.BuildLogRetention.Builds), 334 ) 335 } 336 } 337 338 step := job.Step() 339 340 validator := atc.NewStepValidator(c, []string{identifier, ".plan"}) 341 342 _ = validator.Validate(step) 343 344 warnings = append(warnings, validator.Warnings...) 345 346 errorMessages = append(errorMessages, validator.Errors...) 347 } 348 349 return warnings, compositeErr(errorMessages) 350 } 351 352 func compositeErr(errorMessages []string) error { 353 if len(errorMessages) == 0 { 354 return nil 355 } 356 357 return errors.New(strings.Join(errorMessages, "\n")) 358 } 359 360 func validateVarSources(c Config) ([]ConfigWarning, error) { 361 var warnings []ConfigWarning 362 var errorMessages []string 363 364 names := map[string]interface{}{} 365 366 for i, cm := range c.VarSources { 367 var identifier string 368 if cm.Name == "" { 369 identifier = fmt.Sprintf("var_sources[%d]", i) 370 } else { 371 identifier = fmt.Sprintf("var_sources.%s", cm.Name) 372 } 373 374 warning := ValidateIdentifier(cm.Name, identifier) 375 if warning != nil { 376 warnings = append(warnings, *warning) 377 } 378 379 if factory, exists := creds.ManagerFactories()[cm.Type]; exists { 380 // TODO: this check should eventually be removed once all credential managers 381 // are supported in pipeline. - @evanchaoli 382 switch cm.Type { 383 case "vault", "dummy", "ssm": 384 default: 385 errorMessages = append(errorMessages, fmt.Sprintf("credential manager type %s is not supported in pipeline yet", cm.Type)) 386 } 387 388 if _, ok := names[cm.Name]; ok { 389 errorMessages = append(errorMessages, fmt.Sprintf("duplicate var_source name: %s", cm.Name)) 390 } 391 names[cm.Name] = 0 392 393 if manager, err := factory.NewInstance(cm.Config); err == nil { 394 err = manager.Validate() 395 if err != nil { 396 errorMessages = append(errorMessages, fmt.Sprintf("credential manager %s is invalid: %s", cm.Name, err.Error())) 397 } 398 } else { 399 errorMessages = append(errorMessages, fmt.Sprintf("failed to create credential manager %s: %s", cm.Name, err.Error())) 400 } 401 } else { 402 errorMessages = append(errorMessages, fmt.Sprintf("unknown credential manager type: %s", cm.Type)) 403 } 404 } 405 406 if _, err := c.VarSources.OrderByDependency(); err != nil { 407 errorMessages = append(errorMessages, fmt.Sprintf("failed to order by dependency: %s", err.Error())) 408 } 409 410 return warnings, compositeErr(errorMessages) 411 } 412 413 func validateDisplay(c Config) ([]ConfigWarning, error) { 414 var warnings []ConfigWarning 415 416 if c.Display == nil { 417 return warnings, nil 418 } 419 420 url, err := url.Parse(c.Display.BackgroundImage) 421 422 if err != nil { 423 return warnings, fmt.Errorf("background_image is not a valid URL: %s", c.Display.BackgroundImage) 424 } 425 426 switch url.Scheme { 427 case "https": 428 case "http": 429 case "": 430 break 431 default: 432 return warnings, fmt.Errorf("background_image scheme must be either http, https or relative") 433 } 434 435 return warnings, nil 436 }