Permalink
Browse files

[FIXED JENKINS-44621] Only remove job properties from Jenkinsfiles

Rather than actually using the properties(...) step directly, we
instead will now do this behind the scenes by interacting with the job
directly. We'll also record what job properties, triggers, and
parameters were defined in the Jenkinsfile for a given run, so that we
can refer back in the next build to see what existing job properties,
triggers, or parameters were defined through the Jenkinsfile vs
defined outside of the Jenkinsfile. This way, we can preserve the
defined-outside-Jenkinsfile properties et al.

Note that this won't *quite* work right if the first run of a job
after upgrading removes triggers/properties/etc, because there is no
record for builds beforehand, so all properties etc on the job at the
time of upgrade will be treated as if they were defined
externally. However, after that first build, you can remove properties
etc from the Jenkinsfile with the expected behavior.
  • Loading branch information...
abayer committed Jun 1, 2017
1 parent 27b76c3 commit d07d3501b898cf3087716e12004021d8c703efea
@@ -30,17 +30,23 @@ import com.google.common.cache.CacheLoader
import com.google.common.cache.LoadingCache;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import groovy.json.StringEscapeUtils
import hudson.BulkChange
import hudson.ExtensionList
import hudson.model.Describable
import hudson.model.Descriptor
import hudson.model.JobProperty
import hudson.model.ParameterDefinition
import hudson.model.ParametersDefinitionProperty
import hudson.model.Result
import hudson.triggers.Trigger
import jenkins.model.BuildDiscarderProperty
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.lang.StringUtils
import org.jenkinsci.plugins.pipeline.StageStatus
import org.jenkinsci.plugins.pipeline.StageTagsMetadata
import org.jenkinsci.plugins.pipeline.SyntheticStage
import org.jenkinsci.plugins.pipeline.modeldefinition.actions.ExecutionModelAction
import org.jenkinsci.plugins.pipeline.modeldefinition.actions.JobPropertyTrackerAction
import org.jenkinsci.plugins.pipeline.modeldefinition.ast.ModelASTEnvironment
import org.jenkinsci.plugins.pipeline.modeldefinition.ast.ModelASTInternalFunctionCall
import org.jenkinsci.plugins.pipeline.modeldefinition.ast.ModelASTPipelineDef
@@ -75,10 +81,12 @@ import org.jenkinsci.plugins.workflow.graph.FlowNode
import org.jenkinsci.plugins.workflow.job.WorkflowJob
import org.jenkinsci.plugins.workflow.job.WorkflowRun
import org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty
import org.jenkinsci.plugins.workflow.multibranch.BranchJobProperty
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException
import org.jenkinsci.plugins.workflow.steps.StepDescriptor
import org.jenkinsci.plugins.workflow.support.steps.StageStep

import javax.annotation.CheckForNull
import javax.annotation.Nonnull
import javax.annotation.Nullable
import javax.lang.model.SourceVersion
@@ -231,7 +239,7 @@ public class Utils {
return true
}
} else {
return true
return !(p instanceof BranchJobProperty)
}
}
}
@@ -735,4 +743,249 @@ public class Utils {
}
}

/**
* Translate a list of objects which may either be instances of a given class or {@link UninstantiatedDescribable}s,
* and return a list of those instances of the class and instantiated version of those {@link UninstantiatedDescribable}s.
*
* @param clazz The class we'll be instantiating, which must implement {@link Describable}.
* @param toInstantiate The list of either instances of the class or {@link UninstantiatedDescribable}s that can be
* instantiated to instances of the class.
* @return The list of instances. May be empty.
*/
@Nonnull
private static <T extends Describable> List<T> instantiateList(Class<T> clazz, List<Object> toInstantiate) {
List<T> l = []
toInstantiate.each { t ->
if (t instanceof UninstantiatedDescribable) {
l.add((T) t.instantiate())
} else if (clazz.isInstance(t)) {
l.add((T)t)
}
}

return l
}

/**
* Given the values from {@link org.jenkinsci.plugins.pipeline.modeldefinition.model.Options#getProperties()},
* {@link org.jenkinsci.plugins.pipeline.modeldefinition.model.Triggers#getTriggers()}, and
* {@link org.jenkinsci.plugins.pipeline.modeldefinition.model.Parameters#getParameters()}, figure out which job
* properties, triggers, and parameters should be added/removed to the job, and actually do so, properly preserving
* such job properties, triggers, and parameters which were defined outside of the Jenkinsfile.
*
* @param propsOrUninstantiated Newly-defined job properties, potentially a mix of {@link JobProperty}s and
* {@link UninstantiatedDescribable}s.
* @param trigsOrUninstantiated Newly-defined triggers, potentially a mix of {@link Trigger}s and
* {@link UninstantiatedDescribable}s.
* @param paramsOrUninstantiated Newly-defined parameters, potentially a mix of {@link ParameterDefinition}s and
* {@link UninstantiatedDescribable}s.
* @param script
*/
static void updateJobProperties(@CheckForNull List<Object> propsOrUninstantiated,
@CheckForNull List<Object> trigsOrUninstantiated,
@CheckForNull List<Object> paramsOrUninstantiated,
@Nonnull CpsScript script) {
List<JobProperty> rawJobProperties = instantiateList(JobProperty.class, propsOrUninstantiated)
List<Trigger> rawTriggers = instantiateList(Trigger.class, trigsOrUninstantiated)
List<ParameterDefinition> rawParameters = instantiateList(ParameterDefinition.class, paramsOrUninstantiated)

WorkflowRun r = script.$build()
WorkflowJob j = r.getParent()

List<JobProperty> existingJobProperties = existingJobPropertiesForJob(j)
List<Trigger> existingTriggers = existingTriggersForJob(j)
List<ParameterDefinition> existingParameters = existingParametersForJob(j)

Set<String> previousProperties = new HashSet<>()
Set<String> previousTriggers = new HashSet<>()
Set<String> previousParameters = new HashSet<>()

JobPropertyTrackerAction previousAction = null
WorkflowRun previousBuild = r.getPreviousBuild()
if (previousBuild != null) {
previousAction = previousBuild.getAction(JobPropertyTrackerAction.class)
if (previousAction != null) {
previousProperties.addAll(previousAction.getJobProperties())
previousTriggers.addAll(previousAction.getTriggers())
previousParameters.addAll(previousAction.getParameters())
}
}

List<JobProperty> jobPropertiesToApply = []
Set<Class<? extends JobProperty>> seenClasses = new HashSet<>()
if (rawJobProperties != null) {
jobPropertiesToApply.addAll(rawJobProperties)
seenClasses.addAll(rawJobProperties.collect { it.class })
}
// Find all existing job properties that aren't of classes we've explicitly defined, *and* aren't
// in the set of classes of job properties defined by the Jenkinsfile in the previous build. Add those too.
// Oh, and ignore the PipelineTriggersJobProperty and ParameterDefinitionsProperty - we handle those separately.
// And stash the property classes that should be removed aside as well.
List<JobProperty> propsToRemove = []
existingJobProperties.each { p ->
// We only care about classes that we haven't already seen in the new properties list.
if (!(p.class in seenClasses)) {
if (!(p.class.name in previousProperties)) {
// This means it's a job property defined outside of our scope, so leave it there.
jobPropertiesToApply.add(p)
} else {
// This means we should be removing it - it was defined via the Jenkinsfile last time but is no
// longer defined.
propsToRemove.add(p)
}
}
}

List<Trigger> triggersToApply = getTriggersToApply(rawTriggers, existingTriggers, previousTriggers)
List<ParameterDefinition> parametersToApply = getParametersToApply(rawParameters, existingParameters, previousParameters)

BulkChange bc = new BulkChange(j)
try {
// Remove the triggers/parameters properties regardless.
j.removeProperty(PipelineTriggersJobProperty.class)
j.removeProperty(ParametersDefinitionProperty.class)

// Remove the job properties we defined in previous Jenkinsfiles but don't any more.
propsToRemove.each { j.removeProperty(it) }

// If there are any triggers and if there are any parameters, add those properties.
if (!triggersToApply.isEmpty()) {
j.addProperty(new PipelineTriggersJobProperty(triggersToApply))
}
if (!parametersToApply.isEmpty()) {
j.addProperty(new ParametersDefinitionProperty(parametersToApply))
}

// Now add all the other job properties we know need to be added.
jobPropertiesToApply.each { p ->
j.addProperty(p)
}

bc.commit();
// Add the action tracking what we added if there's anything for it.
if ((rawJobProperties != null && !rawJobProperties.isEmpty()) ||
(rawTriggers != null && !rawTriggers.isEmpty()) ||
(rawParameters != null && !rawParameters.isEmpty())) {
r.addAction(new JobPropertyTrackerAction(rawJobProperties, rawTriggers, rawParameters))
}
} finally {
bc.abort();
// Roll back and use the same action tracking as last build, if any.
if (previousAction != null) {
r.addAction(new JobPropertyTrackerAction(previousAction))
}
}
}

/**
* Given the new triggers defined in the Jenkinsfile, the existing triggers already on the job, and the set of
* trigger classes that may have been recorded as defined in the Jenkinsfile in the previous build, return a list of
* triggers that will actually be applied, including both the newly defined in Jenkinsfile triggers and any triggers
* defined outside of the Jenkinsfile.
*
* @param newTriggers New triggers from the Jenkinsfile.
* @param existingTriggers Any triggers already defined on the job.
* @param prevDefined Any trigger classes recorded in a {@link JobPropertyTrackerAction} on the previous run.
*
* @return A list of triggers to apply. May be empty.
*/
@Nonnull
private static List<Trigger> getTriggersToApply(@CheckForNull List<Trigger> newTriggers,
@Nonnull List<Trigger> existingTriggers,
@Nonnull Set<String> prevDefined) {
Set<Class<? extends Trigger>> seenTriggerClasses = new HashSet<>()
List<Trigger> toApply = []
if (newTriggers != null) {
toApply.addAll(newTriggers)
seenTriggerClasses.addAll(newTriggers.collect { it.class })
}

// Find all existing triggers that aren't of classes we've explicitly defined, *and* aren't
// in the set of classes of triggers defined by the Jenkinsfile in the previous build. Add those too.
toApply.addAll(existingTriggers.findAll {
!(it.class in seenTriggerClasses) && !(it.class.name in prevDefined)
})

return toApply
}

/**
* Given the new parameters defined in the Jenkinsfile, the existing parameters already on the job, and the set of
* parameter names that may have been recorded as defined in the Jenkinsfile in the previous build, return a list of
* parameters that will actually be applied, including both the newly defined in Jenkinsfile parameters and any
* parameters defined outside of the Jenkinsfile.
*
* @param newParameters New parameters from the Jenkinsfile.
* @param existingParameters Any parameters already defined on the job.
* @param prevDefined Any parameter names recorded in a {@link JobPropertyTrackerAction} on the previous run.
*
* @return A list of parameters to apply. May be empty.
*/
@Nonnull
private static List<ParameterDefinition> getParametersToApply(@CheckForNull List<ParameterDefinition> newParameters,
@Nonnull List<ParameterDefinition> existingParameters,
@Nonnull Set<String> prevDefined) {
Set<String> seenNames = new HashSet<>()
List<ParameterDefinition> toApply = []
if (newParameters != null) {
toApply.addAll(newParameters)
seenNames.addAll(newParameters.collect { it.name })
}
// Find all existing parameters that aren't of names we've explicitly defined, *and* aren't
// in the set of names of parameters defined by the Jenkinsfile in the previous build. Add those too.
toApply.addAll(existingParameters.findAll {
!(it.name in seenNames) && !(it.name in prevDefined)
})

return toApply
}

/**
* Helper method for getting the appropriate {@link JobProperty}s from a job.
*
* @param j a job
* @return A list of all {@link JobProperty}s on the given job, other than ones specifically excluded because we're
* handling them elsewhere. May be empty.
*/
@Nonnull
private static List<JobProperty> existingJobPropertiesForJob(@Nonnull WorkflowJob j) {
List<JobProperty> existing = []
existing.addAll(j.getAllProperties().findAll {
!(it instanceof PipelineTriggersJobProperty) && !(it instanceof ParametersDefinitionProperty)
})

return existing
}

/**
* Helper method for getting all {@link Trigger}s on a job.
*
* @param j a job
* @return A list of all {@link Trigger}s defined in the job's {@link PipelineTriggersJobProperty}. May be empty.
*/
@Nonnull
private static List<Trigger> existingTriggersForJob(@Nonnull WorkflowJob j) {
List<Trigger> existing = []
if (j.getProperty(PipelineTriggersJobProperty.class) != null) {
existing.addAll(j.getProperty(PipelineTriggersJobProperty.class)?.getTriggers())
}
return existing
}

/**
* Helper method for getting all {@link ParameterDefinition}s on a job.
*
* @param j a job
* @return A list of all {@link ParameterDefinition}s defined in the job's {@link ParametersDefinitionProperty}. May
* be empty.
*/
@Nonnull
private static List<ParameterDefinition> existingParametersForJob(@Nonnull WorkflowJob j) {
List<ParameterDefinition> existing = []
if (j.getProperty(ParametersDefinitionProperty.class) != null) {
existing.addAll(j.getProperty(ParametersDefinitionProperty.class)?.getParameterDefinitions())
}
return existing
}

}
@@ -76,6 +76,18 @@ public class Options implements Serializable {
}
}

public List<JobProperty> getProperties() {
return properties
}

public Map<String, DeclarativeOption> getOptions() {
return options
}

public Map<String, Object> getWrappers() {
return wrappers
}

private static final Object OPTION_CACHE_KEY = new Object()
private static final Object CACHE_KEY = new Object()
private static final Object WRAPPER_STEPS_KEY = new Object()
@@ -54,6 +54,10 @@ public class Parameters implements Serializable, MethodsToList<ParameterDefiniti
this.parameters = params
}

public List<ParameterDefinition> getParameters() {
return parameters
}

/**
* Get a map of allowed parameter type keys to their actual type ID. If a {@link org.jenkinsci.Symbol} is on the descriptor for a given
* parameter definition, use that as the key. Otherwise, use the class name.
@@ -56,6 +56,10 @@ public class Triggers implements Serializable, MethodsToList<Trigger> {
this.triggers = t
}

public List<Trigger> getTriggers() {
return triggers
}

protected Object readResolve() throws IOException {
// Need to make sure triggers is initialized on deserialization, even if it's going to be empty.
this.triggers = []
Oops, something went wrong.

0 comments on commit d07d350

Please sign in to comment.