/job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/ExecuteDslScripts.java

https://github.com/malonem/job-dsl-plugin · Java · 387 lines · 225 code · 57 blank · 105 comment · 30 complexity · bd70c4dfc72efd824d96f5996aecc62f MD5 · raw file

  1. package javaposse.jobdsl.plugin;
  2. import com.google.common.base.Joiner;
  3. import com.google.common.base.Predicate;
  4. import com.google.common.collect.*;
  5. import hudson.EnvVars;
  6. import hudson.Extension;
  7. import hudson.Launcher;
  8. import hudson.Util;
  9. import hudson.model.*;
  10. import hudson.tasks.Builder;
  11. import javaposse.jobdsl.dsl.DslScriptLoader;
  12. import javaposse.jobdsl.dsl.GeneratedJob;
  13. import javaposse.jobdsl.dsl.ScriptRequest;
  14. import jenkins.YesNoMaybe;
  15. import jenkins.model.Jenkins;
  16. import org.kohsuke.stapler.DataBoundConstructor;
  17. import java.io.FileInputStream;
  18. import java.io.IOException;
  19. import java.net.URL;
  20. import java.util.Collection;
  21. import java.util.Set;
  22. import java.util.logging.Level;
  23. import java.util.logging.Logger;
  24. /**
  25. * This Builder keeps a list of job DSL scripts, and when prompted, executes these to create /
  26. * update Jenkins jobs.
  27. *
  28. * @author jryan
  29. */
  30. public class ExecuteDslScripts extends Builder {
  31. private static final Logger LOGGER = Logger.getLogger(ExecuteDslScripts.class.getName());
  32. // Artifact of how Jelly/Stapler puts conditional variables in blocks, which NEED to map to a sub-Object.
  33. // The alternative would have been to mess with DescriptorImpl.getInstance
  34. public static class ScriptLocation {
  35. @DataBoundConstructor
  36. public ScriptLocation(String value, String targets, String scriptText) {
  37. this.usingScriptText = value == null || Boolean.parseBoolean(value);
  38. this.targets = Util.fixEmptyAndTrim(targets);
  39. this.scriptText = Util.fixEmptyAndTrim(scriptText);
  40. }
  41. private final boolean usingScriptText;
  42. private final String targets;
  43. private final String scriptText;
  44. }
  45. /**
  46. * Newline-separated list of locations to load as dsl scripts.
  47. */
  48. private final String targets;
  49. /**
  50. * Text of a dsl script.
  51. */
  52. private final String scriptText;
  53. /**
  54. * Whether we're using some text for the script directly
  55. */
  56. private final boolean usingScriptText;
  57. private final boolean ignoreExisting;
  58. private final RemovedJobAction removedJobAction;
  59. @DataBoundConstructor
  60. public ExecuteDslScripts(ScriptLocation scriptLocation, boolean ignoreExisting, RemovedJobAction removedJobAction) {
  61. // Copy over from embedded object
  62. this.usingScriptText = scriptLocation == null || scriptLocation.usingScriptText;
  63. this.targets = scriptLocation==null?null:scriptLocation.targets; // May be null;
  64. this.scriptText = scriptLocation==null?null:scriptLocation.scriptText; // May be null
  65. this.ignoreExisting = ignoreExisting;
  66. this.removedJobAction = removedJobAction;
  67. }
  68. ExecuteDslScripts(String scriptText) {
  69. this.usingScriptText = true;
  70. this.scriptText = scriptText;
  71. this.targets = null;
  72. this.ignoreExisting = false;
  73. this.removedJobAction = RemovedJobAction.DISABLE;
  74. }
  75. ExecuteDslScripts() { /// Where is the empty constructor called?
  76. super();
  77. this.usingScriptText = true;
  78. this.scriptText = null;
  79. this.targets = null;
  80. this.ignoreExisting = false;
  81. this.removedJobAction = RemovedJobAction.DISABLE;
  82. }
  83. public String getTargets() {
  84. return targets;
  85. }
  86. public String getScriptText() {
  87. return scriptText;
  88. }
  89. public boolean isUsingScriptText() {
  90. return usingScriptText;
  91. }
  92. public boolean isIgnoreExisting() {
  93. return ignoreExisting;
  94. }
  95. public RemovedJobAction getRemovedJobAction() {
  96. return removedJobAction;
  97. }
  98. @Override
  99. public Action getProjectAction(AbstractProject<?, ?> project) {
  100. return new GeneratedJobsAction(project);
  101. }
  102. /**
  103. * Runs every job DSL script provided in the plugin configuration, which results in new /
  104. * updated Jenkins jobs. The created / updated jobs are reported in the build result.
  105. *
  106. * @param build
  107. * @param launcher
  108. * @param listener
  109. * @return
  110. * @throws InterruptedException
  111. * @throws IOException
  112. */
  113. @SuppressWarnings("rawtypes")
  114. @Override
  115. public boolean perform(final AbstractBuild<?, ?> build, final Launcher launcher, final BuildListener listener)
  116. throws InterruptedException, IOException {
  117. EnvVars env = build.getEnvironment(listener);
  118. env.putAll(build.getBuildVariables());
  119. ItemGroup groupName = build.getProject().getParent();
  120. // We run the DSL, it'll need some way of grabbing a template config.xml and how to save it
  121. JenkinsJobManagement jm = new JenkinsJobManagement(listener.getLogger(), env, groupName);
  122. Set<GeneratedJob> freshJobs;
  123. String jobName = build.getProject().getName();
  124. Item item = Jenkins.getInstance().getItem(jobName,groupName,Item.class);
  125. URL workspaceUrl = new URL(null, "workspace://" + item.getFullName().replace('/','.'), new WorkspaceUrlHandler());
  126. if(usingScriptText) {
  127. ScriptRequest request = new ScriptRequest(null, scriptText, workspaceUrl, ignoreExisting);
  128. freshJobs = DslScriptLoader.runDslEngine(request, jm);
  129. } else {
  130. String targetsStr = env.expand(this.targets);
  131. LOGGER.log(Level.FINE, String.format("Expanded targets to %s", targetsStr));
  132. String[] targets = targetsStr.split("\n");
  133. freshJobs = Sets.newHashSet();
  134. for (String target : targets) {
  135. ScriptRequest request = new ScriptRequest(target, null, workspaceUrl, ignoreExisting);
  136. Set<GeneratedJob> dslJobs = DslScriptLoader.runDslEngine(request, jm);
  137. freshJobs.addAll(dslJobs);
  138. }
  139. }
  140. // TODO Pull all this out, so that it can run outside of the plugin, e.g. JenkinsRestApiJobManagement
  141. updateTemplates(build, listener, freshJobs);
  142. updateGeneratedJobs(build, listener, freshJobs);
  143. // Save onto Builder, which belongs to a Project.
  144. GeneratedJobsBuildAction gjba = new GeneratedJobsBuildAction(freshJobs);
  145. gjba.getModifiedJobs().addAll(freshJobs); // Relying on Set to keep only unique values
  146. build.addAction(gjba);
  147. // Hint that our new jobs might have really shaken things up
  148. Jenkins.getInstance().rebuildDependencyGraph();
  149. return true;
  150. }
  151. // private List<String> collectBodies(AbstractBuild<?, ?> build, BuildListener listener, EnvVars env) throws IOException, InterruptedException {
  152. // List<String> bodies = Lists.newArrayList();
  153. // if (usingScriptText) {
  154. // listener.getLogger().println("Using dsl from string");
  155. // bodies.add(scriptText);
  156. // } else {
  157. // String targetsStr = env.expand(this.targets);
  158. // LOGGER.log(Level.FINE, String.format("Expanded targets to %s", targetsStr));
  159. // String[] targets = targetsStr.split("\n");
  160. //
  161. // for (String target : targets) {
  162. // FilePath targetPath = build.getModuleRoot().child(target);
  163. // if (!targetPath.exists()) {
  164. // targetPath = build.getWorkspace().child(target);
  165. // if (!targetPath.exists()) {
  166. // throw new FileNotFoundException("Unable to find DSL script at " + target);
  167. // }
  168. // }
  169. // listener.getLogger().println(String.format("Running dsl from %s", targetPath));
  170. //
  171. // String dslBody = targetPath.readToString();
  172. // bodies.add(dslBody);
  173. // }
  174. // }
  175. // return bodies;
  176. // }
  177. //
  178. // private Set<GeneratedJob> executeBodies(List<String> bodies, JenkinsJobManagement jm) {
  179. // Set<GeneratedJob> freshJobs = Sets.newLinkedHashSet();
  180. // for (String dslBody: bodies) {
  181. // LOGGER.log(Level.FINE, String.format("DSL Content: %s", dslBody));
  182. //
  183. // // Room for one dsl to succeed and another to fail, yet jobs from the first will finish
  184. // // TODO postpone saving jobs even later
  185. // Set<GeneratedJob> dslJobs = DslScriptLoader.runDslShell(dslBody, jm);
  186. //
  187. // freshJobs.addAll(dslJobs);
  188. // }
  189. // return freshJobs;
  190. // }
  191. /**
  192. * Uses generatedJobs as existing data, so call before updating generatedJobs.
  193. * @param build
  194. * @param listener
  195. * @param freshJobs
  196. * @return
  197. * @throws IOException
  198. */
  199. private Set<String> updateTemplates(AbstractBuild<?, ?> build, BuildListener listener, Set<GeneratedJob> freshJobs) throws IOException {
  200. Set<String> freshTemplates = JenkinsJobManagement.getTemplates(freshJobs);
  201. Set<String> existingTemplates = JenkinsJobManagement.getTemplates(extractGeneratedJobs(build.getProject()));
  202. Set<String> newTemplates = Sets.difference(freshTemplates, existingTemplates);
  203. Set<String> removedTemplates = Sets.difference(existingTemplates, freshTemplates);
  204. listener.getLogger().println("Existing Templates: " + Joiner.on(",").join( existingTemplates ));
  205. listener.getLogger().println("New Templates: " + Joiner.on(",").join( newTemplates ));
  206. listener.getLogger().println("Unreferenced Templates: " + Joiner.on(",").join(removedTemplates));
  207. // Collect information about the templates we loaded
  208. final String seedJobName = build.getProject().getName();
  209. DescriptorImpl descriptor = Jenkins.getInstance().getDescriptorByType(DescriptorImpl.class);
  210. boolean descriptorMutated = false;
  211. // Clean up
  212. for(String templateName: removedTemplates) {
  213. Collection<SeedReference> seedJobReferences = descriptor.getTemplateJobMap().get(templateName);
  214. Collection<SeedReference> matching = Collections2.filter(seedJobReferences, new SeedNamePredicate(seedJobName));
  215. if (!matching.isEmpty()) {
  216. seedJobReferences.removeAll(matching);
  217. descriptorMutated = true;
  218. }
  219. }
  220. // Ensure we have a reference
  221. for(String templateName: freshTemplates) {
  222. Collection<SeedReference> seedJobReferences = descriptor.getTemplateJobMap().get(templateName);
  223. Collection<SeedReference> matching = Collections2.filter(seedJobReferences, new SeedNamePredicate(seedJobName));
  224. AbstractProject templateProject = (AbstractProject) Jenkins.getInstance().getItem(templateName);
  225. final String digest = Util.getDigestOf(new FileInputStream(templateProject.getConfigFile().getFile()));
  226. if (matching.size() == 1) {
  227. // Just update digest
  228. SeedReference ref = Iterables.get(matching, 0);
  229. if (digest.equals(ref.digest)) {
  230. ref.digest = digest;
  231. descriptorMutated = true;
  232. }
  233. } else {
  234. if (matching.size() > 1) {
  235. // Not sure how there could be more one, throw it all away and start over
  236. seedJobReferences.removeAll(matching);
  237. }
  238. seedJobReferences.add(new SeedReference(templateName, seedJobName, digest));
  239. descriptorMutated = true;
  240. }
  241. }
  242. if (descriptorMutated) {
  243. descriptor.save();
  244. }
  245. return freshTemplates;
  246. }
  247. /**
  248. * @param listener
  249. * @param freshJobs
  250. * @throws IOException
  251. */
  252. private void updateGeneratedJobs(final AbstractBuild<?, ?> build, BuildListener listener, Set<GeneratedJob> freshJobs) throws IOException {
  253. // Update Project
  254. Set<GeneratedJob> generatedJobs = extractGeneratedJobs(build.getProject());
  255. Set<GeneratedJob> added = Sets.difference(freshJobs, generatedJobs);
  256. Set<GeneratedJob> existing = Sets.intersection(generatedJobs, freshJobs);
  257. Set<GeneratedJob> removed = Sets.difference(generatedJobs, freshJobs);
  258. listener.getLogger().println("Adding jobs: " + Joiner.on(",").join(added));
  259. listener.getLogger().println("Existing jobs: " + Joiner.on(",").join(existing));
  260. listener.getLogger().println("Removing jobs: " + Joiner.on(",").join(removed));
  261. // Update unreferenced jobs
  262. for(GeneratedJob removedJob: removed) {
  263. ItemGroup groupName = build.getProject().getParent();
  264. AbstractProject removedProject = (AbstractProject) Jenkins.getInstance().getItem(removedJob.getJobName(),groupName,Item.class);
  265. if (removedProject != null && removedJobAction != RemovedJobAction.IGNORE) {
  266. if (removedJobAction == RemovedJobAction.DELETE) {
  267. try {
  268. removedProject.delete();
  269. } catch(InterruptedException e) {
  270. listener.getLogger().println(String.format("Delete job failed: %s", removedJob));
  271. listener.getLogger().println(String.format("Disabling job instead: %s", removedJob));
  272. removedProject.disable();
  273. }
  274. } else {
  275. removedProject.disable();
  276. }
  277. }
  278. }
  279. // BuildAction is created with the result, we'll look at an aggregation of builds to know figure out our generated jobs
  280. }
  281. private Set<GeneratedJob> extractGeneratedJobs(AbstractProject project) {
  282. GeneratedJobsAction gja = project.getAction(GeneratedJobsAction.class);
  283. if (gja==null || gja.findLastGeneratedJobs() == null) {
  284. return Sets.newHashSet();
  285. } else {
  286. return gja.findLastGeneratedJobs();
  287. }
  288. }
  289. @Extension(dynamicLoadable = YesNoMaybe.YES)
  290. public static final class DescriptorImpl extends Descriptor<Builder> {
  291. private Multimap<String, SeedReference> templateJobMap; // K=templateName, V=Seed
  292. public DescriptorImpl() {
  293. super(ExecuteDslScripts.class);
  294. load();
  295. }
  296. public String getDisplayName() {
  297. return "Process Job DSLs";
  298. }
  299. public Multimap<String, SeedReference> getTemplateJobMap() {
  300. if (templateJobMap == null) {
  301. templateJobMap = HashMultimap.create();
  302. }
  303. return templateJobMap;
  304. }
  305. public void setTemplateJobMap(Multimap<String, SeedReference> templateJobMap) {
  306. this.templateJobMap = templateJobMap;
  307. }
  308. /*
  309. @Override
  310. public Builder newInstance(StaplerRequest req, JSONObject formData) throws FormException {
  311. return super.newInstance(req, formData);
  312. }
  313. @Override
  314. public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
  315. return super.configure(req, json);
  316. }
  317. */
  318. }
  319. private static class SeedNamePredicate implements Predicate<SeedReference> {
  320. private final String seedJobName;
  321. public SeedNamePredicate(String seedJobName) {
  322. this.seedJobName = seedJobName;
  323. }
  324. @Override
  325. public boolean apply(SeedReference input) {
  326. return seedJobName.equals(input.seedJobName);
  327. }
  328. }
  329. }