Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JENKINS-57252: Optional support for shelving projects instead of deleting them #1174

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions job-dsl-plugin/build.gradle
Expand Up @@ -96,10 +96,12 @@ dependencies {
optionalJenkinsPlugins 'org.jenkins-ci.plugins:config-file-provider:2.15.4'
optionalJenkinsPlugins 'org.jenkinsci.plugins:managed-scripts:1.3'
optionalJenkinsPlugins 'io.jenkins:configuration-as-code:1.15'
optionalJenkinsPlugins 'org.jenkins-ci.plugins:shelve-project-plugin:2.4'
jenkinsTest 'io.jenkins:configuration-as-code:1.15'
jenkinsTest 'io.jenkins:configuration-as-code:1.15:tests'
jenkinsTest 'org.jenkins-ci.plugins:cloudbees-folder:5.14'
jenkinsTest 'org.jenkins-ci.plugins:cloudbees-folder:6.5.1'
jenkinsTest 'org.jenkins-ci.plugins:matrix-auth:1.3'
jenkinsTest 'org.jenkins-ci.plugins:nested-view:1.14'
jenkinsTest 'org.jenkins-ci.plugins:credentials:2.1.10'
jenkinsTest 'org.jenkins-ci.plugins:credentials:2.1.11'
jenkinsTest 'org.jenkins-ci.plugins:shelve-project-plugin:2.4'
}
Expand Up @@ -64,6 +64,9 @@ public void setTemplateJobMap(Multimap<String, SeedReference> templateJobMap) {
public ListBoxModel doFillRemovedJobActionItems() {
ListBoxModel items = new ListBoxModel();
for (RemovedJobAction action : RemovedJobAction.values()) {
if (action == RemovedJobAction.SHELVE && Jenkins.get().getPlugin(ExecuteDslScripts.SHELVE_PLUGIN_ID) == null) {
continue;
}
items.add(action.getDisplayName(), action.name());
}
return items;
Expand Down
Expand Up @@ -15,15 +15,19 @@
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractItem;
import hudson.model.BuildableItem;
import hudson.model.Descriptor;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Items;
import hudson.model.Job;
import hudson.model.Queue;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.View;
import hudson.model.ViewGroup;
import hudson.model.queue.QueueTaskFuture;
import hudson.tasks.Builder;
import javaposse.jobdsl.dsl.DslException;
import javaposse.jobdsl.dsl.GeneratedConfigFile;
Expand All @@ -37,11 +41,11 @@
import javaposse.jobdsl.plugin.actions.GeneratedConfigFilesBuildAction;
import javaposse.jobdsl.plugin.actions.GeneratedJobsAction;
import javaposse.jobdsl.plugin.actions.GeneratedJobsBuildAction;
import javaposse.jobdsl.plugin.actions.GeneratedObjectsRunAction;
import javaposse.jobdsl.plugin.actions.GeneratedUserContentsAction;
import javaposse.jobdsl.plugin.actions.GeneratedUserContentsBuildAction;
import javaposse.jobdsl.plugin.actions.GeneratedViewsAction;
import javaposse.jobdsl.plugin.actions.GeneratedViewsBuildAction;
import javaposse.jobdsl.plugin.actions.GeneratedObjectsRunAction;
import jenkins.model.Jenkins;
import jenkins.model.ParameterizedJobMixIn.ParameterizedJob;
import jenkins.tasks.SimpleBuildStep;
Expand All @@ -51,21 +55,23 @@
import org.jenkinsci.plugins.scriptsecurity.scripts.ApprovalContext;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage;
import org.jvnet.hudson.plugins.shelveproject.ShelveProjectTask;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;

import static hudson.Util.fixEmptyAndTrim;
import static java.lang.String.format;
import static javaposse.jobdsl.plugin.actions.GeneratedObjectsAction.extractGeneratedObjects;
Expand All @@ -79,6 +85,8 @@ public class ExecuteDslScripts extends Builder implements SimpleBuildStep {

private static volatile boolean rebootRequired;

public static final String SHELVE_PLUGIN_ID = "shelve-project-plugin";

/**
* Newline-separated list of locations to load as dsl scripts.
*/
Expand Down Expand Up @@ -365,7 +373,7 @@ public void perform(@Nonnull Run<?, ?> run, @Nonnull FilePath workspace, @Nonnul
addJobAction(run, new GeneratedUserContentsBuildAction(freshUserContents));

updateTemplates(run.getParent(), listener, new HashSet<GeneratedJob>(run.getAction(GeneratedJobsBuildAction.class).getModifiedObjects()));
updateGeneratedJobs(run.getParent(), listener, new HashSet<GeneratedJob>(run.getAction(GeneratedJobsBuildAction.class).getModifiedObjects()));
updateGeneratedJobs(run, run.getParent(), listener, new HashSet<GeneratedJob>(run.getAction(GeneratedJobsBuildAction.class).getModifiedObjects()));
updateGeneratedViews(run.getParent(), listener, new HashSet<GeneratedView>(run.getAction(GeneratedViewsBuildAction.class).getModifiedObjects()));
updateGeneratedConfigFiles(run.getParent(), listener, new HashSet<GeneratedConfigFile>(run.getAction(GeneratedConfigFilesBuildAction.class).getModifiedObjects()));
updateGeneratedUserContents(run.getParent(), listener, new HashSet<GeneratedUserContent>(run.getAction(GeneratedUserContentsBuildAction.class).getModifiedObjects()));
Expand Down Expand Up @@ -449,15 +457,17 @@ private Set<String> updateTemplates(Job seedJob, TaskListener listener,
return freshTemplates;
}

private void updateGeneratedJobs(final Job seedJob, TaskListener listener,
private void updateGeneratedJobs(final Run<?, ?> run, final Job seedJob, TaskListener listener,
Set<GeneratedJob> freshJobs) throws IOException, InterruptedException {
// Update Project
Set<GeneratedJob> generatedJobs = extractGeneratedObjects(seedJob, GeneratedJobsAction.class);
Set<GeneratedJob> added = Sets.difference(freshJobs, generatedJobs);
Set<GeneratedJob> existing = Sets.intersection(generatedJobs, freshJobs);
Set<GeneratedJob> unreferenced = Sets.difference(generatedJobs, freshJobs);
Set<GeneratedJob> removed = new HashSet<>();
Set<GeneratedJob> shelved = new HashSet<>();
Set<GeneratedJob> disabled = new HashSet<>();
Map<Item,GeneratedJob> folders = new IdentityHashMap<>();

logItems(listener, "Added items", added);
logItems(listener, "Existing items", existing);
Expand All @@ -467,9 +477,17 @@ private void updateGeneratedJobs(final Job seedJob, TaskListener listener,
for (GeneratedJob unreferencedJob : unreferenced) {
Item removedItem = getLookupStrategy().getItem(seedJob, unreferencedJob.getJobName(), Item.class);
if (removedItem != null && removedJobAction != RemovedJobAction.IGNORE) {
if ("com.cloudbees.hudson.plugins.folder.Folder".equals(removedItem.getClass().getName())) {
folders.put(removedItem, unreferencedJob);
continue;
}

if (removedJobAction == RemovedJobAction.DELETE) {
removedItem.delete();
removed.add(unreferencedJob);
} else if (removedJobAction == RemovedJobAction.SHELVE) {
shelve(run, removedItem, listener);
shelved.add(unreferencedJob);
} else {
if (removedItem instanceof ParameterizedJob) {
ParameterizedJob project = (ParameterizedJob) removedItem;
Expand All @@ -481,13 +499,53 @@ private void updateGeneratedJobs(final Job seedJob, TaskListener listener,
}
}

// remove extraneous folders after jobs have been deleted/shelved
if (removedJobAction == RemovedJobAction.DELETE || removedJobAction == RemovedJobAction.SHELVE) {
for (Map.Entry<Item, GeneratedJob> folder : folders.entrySet()) {
folder.getKey().delete();
removed.add(folder.getValue());
}
}

// print what happened with unreferenced jobs
logItems(listener, "Disabled items", disabled);
logItems(listener, "Removed items", removed);
logItems(listener, "Shelved items", shelved);

updateGeneratedJobMap(seedJob, Sets.union(added, existing), unreferenced);
}

private void shelve(Run<?,?> run, Item project, TaskListener listener) throws InterruptedException {
Jenkins jenkins = Jenkins.get();
jenkins.checkPermission(Item.DELETE);

if (! (project instanceof BuildableItem)) {
failBuild(run, "Unable to shelve " + project + " since it is not a BuildableItem", listener, null);
return;
}
BuildableItem item = (BuildableItem) project;
if (jenkins.getPlugin(SHELVE_PLUGIN_ID) == null) {
failBuild(run, "Unable to shelve project " + item + " since the " + SHELVE_PLUGIN_ID + " plugin is not installed.", listener, null);
return;
}

Queue.WaitingItem waitingItem = jenkins.getQueue().schedule(new ShelveProjectTask(item), 0);
QueueTaskFuture<Queue.Executable> future = waitingItem.getFuture();
try {
future.get(); // wait for completion so that upper folders can be deleted
} catch (ExecutionException ex) {
failBuild(run, "Error shelving project " + project, listener, ex);
}
}

private void failBuild(Run<?,?> run, String message, TaskListener listener, @Nullable Exception ex) {
listener.error(message);
if (ex != null) {
ex.printStackTrace(listener.getLogger());
}
run.setResult(Result.UNSTABLE);
}

private void updateGeneratedJobMap(Job seedJob, Set<GeneratedJob> createdOrUpdatedJobs,
Set<GeneratedJob> removedJobs) throws IOException {
DescriptorImpl descriptor = Jenkins.get().getDescriptorByType(DescriptorImpl.class);
Expand Down
Expand Up @@ -3,7 +3,8 @@ package javaposse.jobdsl.plugin
enum RemovedJobAction {
IGNORE('Ignore'),
DISABLE('Disable'),
DELETE('Delete')
DELETE('Delete'),
SHELVE('Shelve')

final String displayName

Expand Down
@@ -1,3 +1,4 @@
<div>
Specifies what to do when a previously generated job is not referenced anymore.
<p>If the <a href="https://plugins.jenkins.io/shelve-project-plugin">Shelve Project"</a> plugin is installed, projects may be archived instead of permanently deleted.</p>
</div>
Expand Up @@ -33,6 +33,7 @@ import org.acegisecurity.Authentication
import org.jenkinsci.plugins.configfiles.GlobalConfigFiles
import org.jenkinsci.plugins.configfiles.custom.CustomConfig
import org.jenkinsci.plugins.managedscripts.PowerShellConfig
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval
import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage
import org.junit.ClassRule
import org.junit.Rule
Expand All @@ -44,8 +45,6 @@ import org.jvnet.hudson.test.WithoutJenkins
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval

import static hudson.model.Result.FAILURE
import static hudson.model.Result.SUCCESS
import static hudson.model.Result.UNSTABLE
Expand Down Expand Up @@ -263,6 +262,21 @@ folder('folder-a/folder-b') {
executeDslScripts.removedJobAction == RemovedJobAction.DELETE
}

@WithoutJenkins
def 'removed job action shelve'() {
setup:
ExecuteDslScripts executeDslScripts = new ExecuteDslScripts()

expect:
executeDslScripts.removedJobAction == RemovedJobAction.IGNORE

when:
executeDslScripts.removedJobAction = RemovedJobAction.SHELVE

then:
executeDslScripts.removedJobAction == RemovedJobAction.SHELVE
}

@WithoutJenkins
def 'removed view action'() {
setup:
Expand Down Expand Up @@ -579,7 +593,58 @@ folder('folder-a/folder-b') {
jenkinsRule.jenkins.getItemByFullName('/folder/test-job') == null
}

def 'only use last build to calculate items to be deleted'() {
def 'shelve a project instead of deleting it'() {
setup:
FreeStyleProject job = jenkinsRule.createFreeStyleProject('seed')

when:
String script1 = 'job("test-job")'
ExecuteDslScripts builder1 = new ExecuteDslScripts(script1)
builder1.removedJobAction = RemovedJobAction.SHELVE
runBuild(job, builder1)

then:
jenkinsRule.jenkins.getItemByFullName('test-job') instanceof FreeStyleProject

when:
String script2 = 'job("different-job")'
ExecuteDslScripts builder2 = new ExecuteDslScripts(script2)
builder2.removedJobAction = RemovedJobAction.SHELVE
runBuild(job, builder2)

then:
jenkinsRule.jenkins.getItemByFullName('different-job') instanceof FreeStyleProject
jenkinsRule.jenkins.getItemByFullName('test-job') == null
}

def shelveJobInFolder() {
setup:
jenkinsRule.jenkins.createProject(Folder, 'folder')
FreeStyleProject job = jenkinsRule.createFreeStyleProject('seed')

when:
String script1 = 'job("/folder/test-job")'
ExecuteDslScripts builder1 = new ExecuteDslScripts(script1)
builder1.removedJobAction = RemovedJobAction.DELETE
runBuild(job, builder1)

then:
jenkinsRule.jenkins.getItemByFullName('/folder/test-job') instanceof FreeStyleProject

when:
String script2 = 'job("/folder/different-job")'
ExecuteDslScripts builder2 = new ExecuteDslScripts(script2)
builder2.removedJobAction = RemovedJobAction.SHELVE
runBuild(job, builder2)

then:
jenkinsRule.jenkins.getItemByFullName('/folder/different-job') instanceof FreeStyleProject
jenkinsRule.jenkins.getItemByFullName('/folder/test-job') == null
// how to make sure there are no further jobs in /folder so that we can test that /folder gets deleted?
//jenkinsRule.jenkins.getItemByFullName('/folder') == null
}

def 'only use last build to calculate items to be deleted'() {
setup:
FreeStyleProject job = jenkinsRule.createFreeStyleProject('seed')

Expand Down