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  }