/*
* Copyright 2016 ThoughtWorks, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.thoughtworks.go.config;
import com.thoughtworks.go.config.preprocessor.SkipParameterResolution;
import com.thoughtworks.go.config.validation.NameTypeValidator;
import com.thoughtworks.go.domain.ConfigErrors;
import com.thoughtworks.go.domain.NullTask;
import com.thoughtworks.go.domain.Task;
import com.thoughtworks.go.service.TaskFactory;
import com.thoughtworks.go.util.XmlUtils;
import org.apache.commons.lang.StringUtils;
import java.util.Map;
import java.util.regex.Pattern;
import static org.apache.commons.lang.StringUtils.isBlank;
/**
* @understands configuratin for a job
*/
@ConfigTag("job")
public class JobConfig implements Validatable, ParamsAttributeAware, EnvironmentVariableScope {
@SkipParameterResolution
@ConfigAttribute(value = "name", optional = false)
private CaseInsensitiveString jobName;
@ConfigSubtag
private EnvironmentVariablesConfig variables = new EnvironmentVariablesConfig();
@ConfigSubtag
private Tasks tasks = new Tasks();
@ConfigSubtag
private Tabs tabs = new Tabs();
@ConfigSubtag
private Resources resources = new Resources();
@ConfigSubtag
private ArtifactPlans artifactPlans = new ArtifactPlans();
@ConfigSubtag
private ArtifactPropertiesGenerators artifactPropertiesGenerators = new ArtifactPropertiesGenerators();
@ConfigAttribute(value = "runOnAllAgents", optional = true) private boolean runOnAllAgents = false;
@ConfigAttribute(value = "runInstanceCount", optional = true, allowNull = true) private String runInstanceCount;
@ConfigAttribute(value = "timeout", optional = true, allowNull = true) private String timeout;
@ConfigAttribute(value = "elasticProfileId", optional = true, allowNull = true) private String elasticProfileId;
private ConfigErrors errors = new ConfigErrors();
public static final String NAME = "name";
public static final String TASKS = "tasks";
public static final String RESOURCES = "resources";
public static final String TABS = "tabs";
public static final String ENVIRONMENT_VARIABLES = "variables";
public static final String ARTIFACT_PLANS = "artifactPlans";
public static final String DEFAULT_NAME = "defaultJob";
public static final String TIMEOUT = "timeout";
public static final String DEFAULT_TIMEOUT = "defaultTimeout";
public static final String OVERRIDE_TIMEOUT = "overrideTimeout";
public static final String NEVER_TIMEOUT = "neverTimeout";
public static final String RUN_TYPE = "runType";
public static final String RUN_SINGLE_INSTANCE = "runSingleInstance";
public static final String RUN_ON_ALL_AGENTS = "runOnAllAgents";
public static final String RUN_MULTIPLE_INSTANCE = "runMultipleInstance";
public static final String RUN_INSTANCE_COUNT = "runInstanceCount";
public static final String ELASTIC_PROFILE_ID = "elasticProfileId";
private static final String JOB_NAME_PATTERN = "[a-zA-Z0-9_\\-.]+";
private static final Pattern JOB_NAME_PATTERN_REGEX = Pattern.compile(String.format("^(%s)$", JOB_NAME_PATTERN));
public JobConfig() {
}
public JobConfig(CaseInsensitiveString jobName) {
this();
this.jobName = jobName;
}
public JobConfig(final CaseInsensitiveString jobName, Resources resources, ArtifactPlans artifactPlans) {
this(jobName, resources, artifactPlans, new Tasks());
}
public JobConfig(final CaseInsensitiveString jobName, Resources resources, ArtifactPlans artifactPlans, Tasks tasks) {
this(jobName);
this.resources = resources;
this.artifactPlans = artifactPlans;
this.tasks = tasks;
}
public JobConfig(final CaseInsensitiveString jobName, ArtifactPropertiesGenerators artifactPropertiesGenerators) {
this(jobName);
this.artifactPropertiesGenerators = artifactPropertiesGenerators;
}
public JobConfig(final CaseInsensitiveString jobName, Resources resources, ArtifactPlans artifactPlans, ArtifactPropertiesGenerators generators) {
this(jobName, resources, artifactPlans);
this.artifactPropertiesGenerators = generators;
}
public JobConfig(String planName) {
this(new CaseInsensitiveString(planName), new Resources(), new ArtifactPlans());
}
public CaseInsensitiveString name() {
return jobName;
}
public void setName(String name) {
setName(new CaseInsensitiveString(name));
}
public void setName(CaseInsensitiveString name) {
this.jobName = name;
}
public Resources resources() {
return resources;
}
public void setResources(Resources resources) {
this.resources = resources;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JobConfig jobConfig = (JobConfig) o;
if (runOnAllAgents != jobConfig.runOnAllAgents) return false;
if (jobName != null ? !jobName.equals(jobConfig.jobName) : jobConfig.jobName != null) return false;
if (variables != null ? !variables.equals(jobConfig.variables) : jobConfig.variables != null) return false;
if (tasks != null ? !tasks.equals(jobConfig.tasks) : jobConfig.tasks != null) return false;
if (tabs != null ? !tabs.equals(jobConfig.tabs) : jobConfig.tabs != null) return false;
if (resources != null ? !resources.equals(jobConfig.resources) : jobConfig.resources != null) return false;
if (artifactPlans != null ? !artifactPlans.equals(jobConfig.artifactPlans) : jobConfig.artifactPlans != null)
return false;
if (artifactPropertiesGenerators != null ? !artifactPropertiesGenerators.equals(jobConfig.artifactPropertiesGenerators) : jobConfig.artifactPropertiesGenerators != null)
return false;
if (runInstanceCount != null ? !runInstanceCount.equals(jobConfig.runInstanceCount) : jobConfig.runInstanceCount != null)
return false;
if (timeout != null ? !timeout.equals(jobConfig.timeout) : jobConfig.timeout != null) return false;
return elasticProfileId != null ? elasticProfileId.equals(jobConfig.elasticProfileId) : jobConfig.elasticProfileId == null;
}
@Override
public int hashCode() {
int result = jobName != null ? jobName.hashCode() : 0;
result = 31 * result + (variables != null ? variables.hashCode() : 0);
result = 31 * result + (tasks != null ? tasks.hashCode() : 0);
result = 31 * result + (tabs != null ? tabs.hashCode() : 0);
result = 31 * result + (resources != null ? resources.hashCode() : 0);
result = 31 * result + (artifactPlans != null ? artifactPlans.hashCode() : 0);
result = 31 * result + (artifactPropertiesGenerators != null ? artifactPropertiesGenerators.hashCode() : 0);
result = 31 * result + (runOnAllAgents ? 1 : 0);
result = 31 * result + (runInstanceCount != null ? runInstanceCount.hashCode() : 0);
result = 31 * result + (timeout != null ? timeout.hashCode() : 0);
result = 31 * result + (elasticProfileId != null ? elasticProfileId.hashCode() : 0);
return result;
}
public Tasks tasks() {
return tasks.isEmpty() ? defaultTasks() : tasks;
}
public void addTask(Task task) {
tasks.add(task);
}
private Tasks defaultTasks() {
return new Tasks(new NullTask());
}
public ArtifactPlans artifactPlans() {
return artifactPlans;
}
public void setArtifactPlans(ArtifactPlans artifactPlans) {
this.artifactPlans = artifactPlans;
}
public Tabs getTabs() {
return tabs;
}
public void setTabs(Tabs tabs) {
this.tabs = tabs;
}
public void addTab(String tab, String path) {
this.tabs.add(new Tab(tab.trim(), path));
}
public ArtifactPropertiesGenerators getProperties() {
return artifactPropertiesGenerators;
}
public void setProperties(ArtifactPropertiesGenerators properties) {
artifactPropertiesGenerators = properties;
}
public Tasks getTasks() {
return tasks;
}
public void setTasks(Tasks tasks) {
this.tasks = tasks;
}
public void addResource(String resource) {
resources.add(new Resource(resource));
}
/* Used in rails view */
public boolean isRunOnAllAgents() {
return runOnAllAgents;
}
public void setRunOnAllAgents(boolean runOnAllAgents) {
this.runOnAllAgents = runOnAllAgents;
}
public boolean isRunMultipleInstanceType() {
return getRunInstanceCountValue() > 0;
}
public Integer getRunInstanceCountValue() {
return runInstanceCount == null ? 0 : Integer.valueOf(runInstanceCount);
}
public String getRunInstanceCount() {
return runInstanceCount;
}
public void setRunInstanceCount(Integer runInstanceCount) {
setRunInstanceCount(Integer.toString(runInstanceCount));
}
public void setRunInstanceCount(String runInstanceCount) {
this.runInstanceCount = runInstanceCount;
}
public boolean isInstanceOf(String jobInstanceName, boolean ignoreCase) {
return jobTypeConfig().isInstanceOf(jobInstanceName, ignoreCase, CaseInsensitiveString.str(jobName));
}
public String translatedName(String jobInstanceName) {
return jobTypeConfig().translatedJobName(jobInstanceName, CaseInsensitiveString.str(jobName));
}
private JobTypeConfig jobTypeConfig() {
if (runOnAllAgents) {
return new RunOnAllAgentsJobTypeConfig();
} else if (isRunMultipleInstanceType()) {
return new RunMultipleInstanceJobTypeConfig();
} else {
return new SingleJobTypeConfig();
}
}
@Override
public String toString() {
return "JobConfig{" +
"jobName='" + jobName + '\'' +
", resources=" + resources +
", runOnAllAgents=" + runOnAllAgents +
", runInstanceCount=" + runInstanceCount +
'}';
}
// only called from tests
public void addVariable(String name, String value) {
variables.add(name, value);
}
public void setVariables(EnvironmentVariablesConfig variables) {
this.variables = variables;
}
public EnvironmentVariablesConfig getVariables() {
return variables;
}
public EnvironmentVariablesConfig getPlainTextVariables() {
return variables.getPlainTextVariables();
}
public EnvironmentVariablesConfig getSecureVariables() {
return variables.getSecureVariables();
}
public boolean hasVariable(String variableName) {
return variables.hasVariable(variableName);
}
public boolean hasTests() {
for (ArtifactPlan artifactPlan : artifactPlans) {
if (artifactPlan.getArtifactType().isTest()) {
return true;
}
}
return false;
}
public boolean validateTree(ValidationContext validationContext) {
validate(validationContext);
boolean isValid = errors.isEmpty();
ValidationContext contextForChildren = validationContext.withParent(this);
isValid = tasks.validateTree(contextForChildren) && isValid;
isValid = variables.validateTree(contextForChildren) && isValid;
isValid = resources.validateTree(contextForChildren) && isValid;
isValid = artifactPropertiesGenerators.validateTree(contextForChildren) && isValid;
isValid = tabs.validateTree(contextForChildren) && isValid;
isValid = artifactPlans.validateTree(contextForChildren) && isValid;
return isValid;
}
public void validate(ValidationContext validationContext) {
if (isBlank(CaseInsensitiveString.str(jobName))) {
errors.add(NAME, "Name is a required field");
} else {
if ((CaseInsensitiveString.str(jobName).length() > 255 || XmlUtils.doesNotMatchUsingXsdRegex(JOB_NAME_PATTERN_REGEX, CaseInsensitiveString.str(jobName)))) {
String message = String.format("Invalid job name '%s'. This must be alphanumeric and may contain underscores and periods. The maximum allowed length is %d characters.", jobName,
NameTypeValidator.MAX_LENGTH);
errors.add(NAME, message);
}
if (RunOnAllAgentsJobTypeConfig.hasMarker(CaseInsensitiveString.str(jobName))) {
errors.add(NAME, String.format("A job cannot have '%s' in it's name: %s because it is a reserved keyword", RunOnAllAgentsJobTypeConfig.MARKER, jobName));
}
if (RunMultipleInstanceJobTypeConfig.hasMarker(CaseInsensitiveString.str(jobName))) {
errors.add(NAME, String.format("A job cannot have '%s' in it's name: %s because it is a reserved keyword", RunMultipleInstanceJobTypeConfig.MARKER, jobName));
}
}
if (runInstanceCount != null) {
try {
int runInstanceCountForValidation = Integer.parseInt(this.runInstanceCount);
if (runInstanceCountForValidation < 0) {
errors().add(RUN_TYPE, "'Run Instance Count' cannot be a negative number as it represents number of instances Go needs to spawn during runtime.");
}
} catch (NumberFormatException e) {
errors().add(RUN_TYPE, "'Run Instance Count' should be a valid positive integer as it represents number of instances Go needs to spawn during runtime.");
}
}
if (isRunOnAllAgents() && isRunMultipleInstanceType()) {
errors.add(RUN_TYPE, "Job cannot be 'run on all agents' type and 'run multiple instance' type together.");
}
if (timeout != null) {
try {
double timeoutForValidation = Double.parseDouble(this.timeout);
if (timeoutForValidation < 0) {
errors().add(TIMEOUT, "Timeout cannot be a negative number as it represents number of minutes");
}
} catch (NumberFormatException e) {
errors().add(TIMEOUT, "Timeout should be a valid number as it represents number of minutes");
}
}
if (!resources.isEmpty() && !isBlank(elasticProfileId)) {
errors().add(RESOURCES, "Job cannot have both `resource` and `elasticProfileId`");
errors().add(ELASTIC_PROFILE_ID, "Job cannot have both `resource` and `elasticProfileId`");
}
if (!isBlank(elasticProfileId)) {
if (!validationContext.isWithinTemplates() && !validationContext.isValidProfileId(elasticProfileId)) {
errors().add(ELASTIC_PROFILE_ID, String.format("No profile defined corresponding to profile_id '%s'", elasticProfileId));
}
}
if (elasticProfileId != null && isBlank(elasticProfileId)){
errors().add(ELASTIC_PROFILE_ID, "Must not be a blank string");
}
for (Resource resource : resources) {
if (StringUtils.isEmpty(resource.getName())) {
CaseInsensitiveString pipelineName = validationContext.getPipeline().name();
CaseInsensitiveString stageName = validationContext.getStage().name();
String message = String.format("Empty resource name in job \"%s\" of stage \"%s\" of pipeline \"%s\". If a template is used, please ensure that the resource parameters are defined for this pipeline.", jobName, stageName, pipelineName);
errors.add(RESOURCES, message);
}
}
if (isRunOnAllAgents() && !isBlank(elasticProfileId)) {
errors.add(RUN_TYPE, "Job cannot be set to 'run on all agents' when assigned to an elastic agent");
}
}
public ConfigErrors errors() {
return errors;
}
public void setConfigAttributes(Object attributes) {
setConfigAttributes(attributes, null);
}
public void setConfigAttributes(Object attributes, TaskFactory taskFactory) {
Map attributesMap = (Map) attributes;
if (attributesMap.containsKey(NAME)) {
String nameString = (String) attributesMap.get(NAME);
jobName = nameString == null ? null : new CaseInsensitiveString(nameString);
}
if (attributesMap.containsKey("elasticProfileId")) {
String elasticProfileId = (String) attributesMap.get("elasticProfileId");
setElasticProfileId(StringUtils.isBlank(elasticProfileId) ? null : elasticProfileId);
}
if (attributesMap.containsKey(TASKS)) {
tasks.setConfigAttributes(attributesMap.get(TASKS), taskFactory);
}
if (attributesMap.containsKey(ENVIRONMENT_VARIABLES)) {
variables.setConfigAttributes(attributesMap.get(ENVIRONMENT_VARIABLES));
}
if (attributesMap.containsKey(TABS)) {
tabs.setConfigAttributes(attributesMap.get(TABS));
}
if (attributesMap.containsKey(RESOURCES)) {
resources.importFromCsv((String) attributesMap.get(RESOURCES));
}
if (attributesMap.containsKey(ARTIFACT_PLANS)) {
artifactPlans.setConfigAttributes(attributesMap.get(ARTIFACT_PLANS));
}
setTimeoutAttribute(attributesMap);
setJobRunTypeAttribute(attributesMap);
}
private void setTimeoutAttribute(Map attributesMap) {
if (attributesMap.containsKey("timeoutType")) {
String timeoutType = (String) attributesMap.get("timeoutType");
if (DEFAULT_TIMEOUT.equals(timeoutType)) {
this.timeout = null;
}
if (NEVER_TIMEOUT.equals(timeoutType)) {
this.timeout = "0";
}
if (OVERRIDE_TIMEOUT.equals(timeoutType)) {
String timeout = (String) attributesMap.get(TIMEOUT);
if (isBlank(timeout)) {
this.timeout = null;
} else {
this.timeout = timeout;
}
}
}
}
private void setJobRunTypeAttribute(Map attributesMap) {
if (attributesMap.containsKey(RUN_TYPE)) {
this.runOnAllAgents = false;
this.runInstanceCount = null;
String jobRunType = (String) attributesMap.get(RUN_TYPE);
if (RUN_ON_ALL_AGENTS.equals(jobRunType)) {
this.runOnAllAgents = true;
} else if (RUN_MULTIPLE_INSTANCE.equals(jobRunType)) {
String runInstanceCount = (String) attributesMap.get(RUN_INSTANCE_COUNT);
if (isBlank(runInstanceCount)) {
this.runInstanceCount = null;
} else {
this.runInstanceCount = runInstanceCount;
}
}
}
}
public void validateNameUniqueness(Map<String, JobConfig> visitedConfigs) {
if (isBlank(CaseInsensitiveString.str(name()))) return;
String currentJob = name().toLower();
if (visitedConfigs.containsKey(CaseInsensitiveString.str(name())) || visitedConfigs.containsKey(currentJob)) {
JobConfig conflictingConfig = visitedConfigs.get(currentJob);
conflictingConfig.addUniquenessViolationMessage();
this.addUniquenessViolationMessage();
} else {
visitedConfigs.put(currentJob, this);
}
}
private void addUniquenessViolationMessage() {
this.addError(NAME, String.format("You have defined multiple jobs called '%s'. Job names are case-insensitive and must be unique.", name()));
}
public void addError(String key, String message) {
errors.add(key, message);
}
public String getTimeout() {
return timeout;
}
public void setTimeout(String timeout) {
this.timeout = timeout;
}
public String getTimeoutType() {
return timeout == null ? DEFAULT_TIMEOUT : timeout.equals("0") ? NEVER_TIMEOUT : OVERRIDE_TIMEOUT;
}
public String getRunType() {
if (isRunOnAllAgents())
return RUN_ON_ALL_AGENTS;
if (isRunMultipleInstanceType())
return RUN_MULTIPLE_INSTANCE;
return RUN_SINGLE_INSTANCE;
}
public void injectTasksForTest(Tasks tasks) {
this.tasks = tasks;
}
public String getElasticProfileId() {
return elasticProfileId;
}
public void setElasticProfileId(String elasticProfileId) {
this.elasticProfileId = elasticProfileId;
}
}