From 8f545f8d940313da5cf1af6db770ca3f0405f2b5 Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Mon, 2 Mar 2015 01:32:03 -0500 Subject: [PATCH] [FIXED JENKINS-27182]: Allow to test Job DSL scripts (dry run) --- .../jobdsl/plugin/ExecuteDslScripts.java | 70 +++++++++++----- .../jobdsl/plugin/JenkinsJobManagement.java | 60 +++++++++++-- .../plugin/ExecuteDslScripts/config.jelly | 4 + .../plugin/ExecuteDslScripts/help-dryRun.html | 7 ++ .../plugin/JenkinsJobManagementSpec.groovy | 84 +++++++++++++++++++ 5 files changed, 197 insertions(+), 28 deletions(-) create mode 100644 job-dsl-plugin/src/main/resources/javaposse/jobdsl/plugin/ExecuteDslScripts/help-dryRun.html diff --git a/job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/ExecuteDslScripts.java b/job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/ExecuteDslScripts.java index c83625981..9745331db 100644 --- a/job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/ExecuteDslScripts.java +++ b/job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/ExecuteDslScripts.java @@ -94,6 +94,8 @@ public ScriptLocation(String value, String targets, String scriptText) { private final boolean ignoreExisting; + private final boolean dryRun; + private final RemovedJobAction removedJobAction; private final RemovedViewAction removedViewAction; @@ -105,7 +107,7 @@ public ScriptLocation(String value, String targets, String scriptText) { @DataBoundConstructor public ExecuteDslScripts(ScriptLocation scriptLocation, boolean ignoreExisting, RemovedJobAction removedJobAction, RemovedViewAction removedViewAction, LookupStrategy lookupStrategy, - String additionalClasspath) { + String additionalClasspath, boolean dryRun) { // Copy over from embedded object this.usingScriptText = scriptLocation == null || scriptLocation.usingScriptText; this.targets = scriptLocation == null ? null : scriptLocation.targets; @@ -115,6 +117,13 @@ public ExecuteDslScripts(ScriptLocation scriptLocation, boolean ignoreExisting, this.removedViewAction = removedViewAction; this.lookupStrategy = lookupStrategy == null ? LookupStrategy.JENKINS_ROOT : lookupStrategy; this.additionalClasspath = additionalClasspath; + this.dryRun = dryRun; + } + + public ExecuteDslScripts(ScriptLocation scriptLocation, boolean ignoreExisting, RemovedJobAction removedJobAction, + RemovedViewAction removedViewAction, LookupStrategy lookupStrategy, + String additionalClasspath) { + this(scriptLocation, ignoreExisting, removedJobAction, removedViewAction, lookupStrategy, additionalClasspath, false); } public ExecuteDslScripts(ScriptLocation scriptLocation, boolean ignoreExisting, RemovedJobAction removedJobAction, @@ -140,6 +149,7 @@ public ExecuteDslScripts(ScriptLocation scriptLocation, boolean ignoreExisting, this.removedViewAction = RemovedViewAction.IGNORE; this.lookupStrategy = LookupStrategy.JENKINS_ROOT; this.additionalClasspath = null; + this.dryRun = false; } ExecuteDslScripts() { @@ -162,6 +172,10 @@ public boolean isIgnoreExisting() { return ignoreExisting; } + public boolean isDryRun() { + return dryRun; + } + public RemovedJobAction getRemovedJobAction() { return removedJobAction; } @@ -199,9 +213,9 @@ public boolean perform(final AbstractBuild build, final Launcher launcher, EnvVars env = build.getEnvironment(listener); env.putAll(build.getBuildVariables()); - JobManagement jm = new InterruptibleJobManagement( - new JenkinsJobManagement(listener.getLogger(), env, build, getLookupStrategy()) - ); + JenkinsJobManagement jjm = new JenkinsJobManagement(listener.getLogger(), env, build, getLookupStrategy()); + jjm.setDryRun(dryRun); + JobManagement jm = new InterruptibleJobManagement(jjm); ScriptRequestGenerator generator = new ScriptRequestGenerator(build, env); try { @@ -272,8 +286,10 @@ private Set updateTemplates(AbstractBuild build, BuildListener lis Collection seedJobReferences = descriptor.getTemplateJobMap().get(templateName); Collection matching = Collections2.filter(seedJobReferences, new SeedNamePredicate(seedJobName)); if (!matching.isEmpty()) { - seedJobReferences.removeAll(matching); - descriptorMutated = true; + if (!isDryRun()) { + seedJobReferences.removeAll(matching); + descriptorMutated = true; + } } } @@ -289,16 +305,20 @@ private Set updateTemplates(AbstractBuild build, BuildListener lis // Just update digest SeedReference ref = Iterables.get(matching, 0); if (digest.equals(ref.getDigest())) { - ref.setDigest(digest); - descriptorMutated = true; + if (!isDryRun()) { + ref.setDigest(digest); + descriptorMutated = true; + } } } else { - if (matching.size() > 1) { - // Not sure how there could be more one, throw it all away and start over - seedJobReferences.removeAll(matching); + if (!isDryRun()) { + if (matching.size() > 1) { + // Not sure how there could be more one, throw it all away and start over + seedJobReferences.removeAll(matching); + } + seedJobReferences.add(new SeedReference(templateName, seedJobName, digest)); + descriptorMutated = true; } - seedJobReferences.add(new SeedReference(templateName, seedJobName, digest)); - descriptorMutated = true; } } @@ -328,11 +348,15 @@ private void updateGeneratedJobs(final AbstractBuild build, BuildListener Item removedItem = getLookupStrategy().getItem(build.getProject(), unreferencedJob.getJobName(), Item.class); if (removedItem != null && removedJobAction != RemovedJobAction.IGNORE) { if (removedJobAction == RemovedJobAction.DELETE) { - removedItem.delete(); + if (!isDryRun()) { + removedItem.delete(); + } removed.add(unreferencedJob); } else { if (removedItem instanceof AbstractProject) { - ((AbstractProject) removedItem).disable(); + if (!isDryRun()) { + ((AbstractProject) removedItem).disable(); + } disabled.add(unreferencedJob); } } @@ -366,8 +390,10 @@ private void updateGeneratedJobMap(AbstractProject seedJob, Set seedJob, Set build, BuildListener liste if (parent instanceof ViewGroup) { View view = ((ViewGroup) parent).getView(FilenameUtils.getName(viewName)); if (view != null) { - ((ViewGroup) parent).deleteView(view); + if(!isDryRun()) { + ((ViewGroup) parent).deleteView(view); + } removed.add(unreferencedView); } } else if (parent == null) { diff --git a/job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/JenkinsJobManagement.java b/job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/JenkinsJobManagement.java index 1c64927c7..791ec6800 100644 --- a/job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/JenkinsJobManagement.java +++ b/job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/JenkinsJobManagement.java @@ -83,6 +83,8 @@ public final class JenkinsJobManagement extends AbstractJobManagement { private final Map environments = new HashMap(); + private boolean dryRun; + public JenkinsJobManagement(PrintStream outputLogger, EnvVars envVars, AbstractBuild build, LookupStrategy lookupStrategy) { super(outputLogger); @@ -143,6 +145,11 @@ public void createOrUpdateView(String path, String config, boolean ignoreExistin validateUpdateArgs(path, config); String viewBaseName = FilenameUtils.getName(path); Jenkins.checkGoodName(viewBaseName); + if (isDryRun()) { + getOutputStream().format("DRY RUN: Would create or update view '%s' on path '%s' with config:%n%s%n%n", + viewBaseName, path, config); + return; + } try { InputStream inputStream = new ByteArrayInputStream(config.getBytes("UTF-8")); @@ -213,6 +220,11 @@ public void createOrUpdateUserContent(UserContent userContent, boolean ignoreExi try { FilePath file = Jenkins.getInstance().getRootPath().child("userContent").child(userContent.getPath()); if (!(file.exists() && ignoreExisting)) { + if (isDryRun()) { + getOutputStream().format("DRY RUN: Would create or update user content on '%s' from '%s'%n", file, + userContent.getPath()); + return; + } file.getParent().mkdirs(); file.copyFrom(userContent.getContent()); } @@ -252,6 +264,11 @@ public void queueJob(String path) throws NameNotProvidedException { BuildableItem project = lookupStrategy.getItem(build.getParent(), path, BuildableItem.class); + if (isDryRun()) { + getOutputStream().format("DRY RUN: Would schedule build of %s from %s%n", path, + build.getParent().getName()); + return; + } LOGGER.log(Level.INFO, format("Scheduling build of %s from %s", path, build.getParent().getName())); project.scheduleBuild(new Cause.UpstreamCause((Run) build)); } @@ -450,7 +467,9 @@ private boolean updateExistingItem(AbstractItem item, javaposse.jobdsl.dsl.Item diff = XMLUnit.compareXML(oldJob, config); if (diff.identical()) { LOGGER.log(Level.FINE, format("Item %s is identical", item.getName())); - notifyItemUpdated(item, dslItem); + if (!isDryRun()) { + notifyItemUpdated(item, dslItem); + } return false; } } catch (Exception e) { @@ -463,8 +482,12 @@ private boolean updateExistingItem(AbstractItem item, javaposse.jobdsl.dsl.Item LOGGER.log(Level.FINE, format("Updating item %s as %s", item.getName(), config)); Source streamSource = new StreamSource(new StringReader(config)); try { - item.updateByXml(streamSource); - notifyItemUpdated(item, dslItem); + if (!isDryRun()) { + item.updateByXml(streamSource); + notifyItemUpdated(item, dslItem); + } else { + getOutputStream().format("DRY RUN: Would update item '%s' as:%n%s%n%n", item.getName(), config); + } created = true; } catch (IOException e) { LOGGER.log(Level.WARNING, "Error writing updated item to file.", e); @@ -505,8 +528,12 @@ private boolean createNewItem(String path, javaposse.jobdsl.dsl.Item dslItem) { ItemGroup parent = lookupStrategy.getParent(build.getProject(), path); String itemName = FilenameUtils.getName(path); if (parent instanceof ModifiableTopLevelItemGroup) { - Item project = ((ModifiableTopLevelItemGroup) parent).createProjectFromXML(itemName, is); - notifyItemCreated(project, dslItem); + if (!isDryRun()) { + Item project = ((ModifiableTopLevelItemGroup) parent).createProjectFromXML(itemName, is); + notifyItemCreated(project, dslItem); + } else { + getOutputStream().format("DRY RUN: Would create item as '%s' as:%n%s%n%n", itemName, config); + } created = true; } else if (parent == null) { throw new DslException(format(Messages.CreateItem_UnknownParent(), path)); @@ -555,8 +582,13 @@ private void renameJob(Job from, String to) throws IOException { if (fromParent != toParent) { LOGGER.info(format("Moving Job %s to folder %s", fromParent.getFullName(), toParent.getFullName())); if (toParent instanceof DirectlyModifiableTopLevelItemGroup) { - DirectlyModifiableTopLevelItemGroup itemGroup = (DirectlyModifiableTopLevelItemGroup) toParent; - move(from, itemGroup); + if (!isDryRun()) { + DirectlyModifiableTopLevelItemGroup itemGroup = (DirectlyModifiableTopLevelItemGroup) toParent; + move(from, itemGroup); + } else { + getOutputStream().format("DRY RUN: Would move job '%s' to folder '%s'%n", fromParent.getFullName(), + toParent.getFullName()); + } } else { throw new DslException(format( Messages.RenameJobMatching_DestinationNotFolder(), @@ -565,11 +597,23 @@ private void renameJob(Job from, String to) throws IOException { )); } } - from.renameTo(FilenameUtils.getName(to)); + if (!isDryRun()) { + from.renameTo(FilenameUtils.getName(to)); + } else { + getOutputStream().format("DRY RUN: Would rename job '%s' to '%s'%n", from.getFullName(), to); + } } @SuppressWarnings("unchecked") private static I move(Item item, DirectlyModifiableTopLevelItemGroup destination) throws IOException { return Items.move((I) item, destination); } + + boolean isDryRun() { + return dryRun; + } + + void setDryRun(boolean dryRun) { + this.dryRun = dryRun; + } } diff --git a/job-dsl-plugin/src/main/resources/javaposse/jobdsl/plugin/ExecuteDslScripts/config.jelly b/job-dsl-plugin/src/main/resources/javaposse/jobdsl/plugin/ExecuteDslScripts/config.jelly index 38ca28b00..eab15ec8d 100644 --- a/job-dsl-plugin/src/main/resources/javaposse/jobdsl/plugin/ExecuteDslScripts/config.jelly +++ b/job-dsl-plugin/src/main/resources/javaposse/jobdsl/plugin/ExecuteDslScripts/config.jelly @@ -23,6 +23,10 @@ + + + diff --git a/job-dsl-plugin/src/main/resources/javaposse/jobdsl/plugin/ExecuteDslScripts/help-dryRun.html b/job-dsl-plugin/src/main/resources/javaposse/jobdsl/plugin/ExecuteDslScripts/help-dryRun.html new file mode 100644 index 000000000..b50fda782 --- /dev/null +++ b/job-dsl-plugin/src/main/resources/javaposse/jobdsl/plugin/ExecuteDslScripts/help-dryRun.html @@ -0,0 +1,7 @@ +
+ This option forces a dry run that does everything but modify existing configuration and rather prints XML diffs to the console logs + to be inspected. + This mode is useful with pull request/feature branch building and staging to automatically validate and inspect the proposed change + in the actual deployment environment. The latter allows to validate that external references (Credential IDs, Secret Files etc) are + properly found and resolved. +
diff --git a/job-dsl-plugin/src/test/groovy/javaposse/jobdsl/plugin/JenkinsJobManagementSpec.groovy b/job-dsl-plugin/src/test/groovy/javaposse/jobdsl/plugin/JenkinsJobManagementSpec.groovy index 45ab8804e..079d84e91 100644 --- a/job-dsl-plugin/src/test/groovy/javaposse/jobdsl/plugin/JenkinsJobManagementSpec.groovy +++ b/job-dsl-plugin/src/test/groovy/javaposse/jobdsl/plugin/JenkinsJobManagementSpec.groovy @@ -257,6 +257,22 @@ class JenkinsJobManagementSpec extends Specification { jenkinsRule.jenkins.getItemByFullName('oldName') == null } + def 'rename job, dry run'() { + setup: + jobManagement.setDryRun(true) + jenkinsRule.createFreeStyleProject('oldName') + + when: + jobManagement.renameJobMatching('oldName', 'newName') + + then: + jenkinsRule.jenkins.getItemByFullName('oldName') != null + jenkinsRule.jenkins.getItemByFullName('newName') == null + + cleanup: + jobManagement.setDryRun(false) + } + def 'rename job relative to seed job'() { setup: Folder folder = jenkinsRule.jenkins.createProject(Folder, 'folder') @@ -351,6 +367,23 @@ class JenkinsJobManagementSpec extends Specification { jenkinsRule.jenkins.getItemByFullName('/project') != null } + def 'createOrUpdateConfig with absolute path, dry run'() { + setup: + Folder folder = jenkinsRule.jenkins.createProject(Folder, 'folder') + FreeStyleProject project = folder.createProject(FreeStyleProject, 'seed') + AbstractBuild build = project.scheduleBuild2(0).get() + JenkinsJobManagement jobManagement = new JenkinsJobManagement( + new PrintStream(buffer), new EnvVars(), build, LookupStrategy.SEED_JOB + ) + jobManagement.setDryRun(true) + + when: + jobManagement.createOrUpdateConfig('/project', Resources.toString(getResource('minimal-job.xml'), UTF_8), true) + + then: + jenkinsRule.jenkins.getItemByFullName('/project') == null + } + def 'createOrUpdateView relative to folder'() { setup: Folder folder = jenkinsRule.jenkins.createProject(Folder, 'folder') @@ -543,6 +576,19 @@ class JenkinsJobManagementSpec extends Specification { view instanceof ListView } + def 'create view, dry run'() { + setup: + jobManagement.setDryRun(true) + when: + jobManagement.createOrUpdateView('test-view', '', false) + + then: + jenkinsRule.instance.getView('test-view') == null + + cleanup: + jobManagement.setDryRun(false) + } + def 'update view'() { setup: jenkinsRule.instance.addView(new ListView('test-view')) @@ -560,6 +606,27 @@ class JenkinsJobManagementSpec extends Specification { view.description == 'lorem ipsum' } + def 'update view, dry run'() { + setup: + jobManagement.setDryRun(true) + jenkinsRule.instance.addView(new ListView('test-view')) + + when: + jobManagement.createOrUpdateView( + 'test-view', + 'lorem ipsum', + false + ) + + then: + View view = jenkinsRule.instance.getView('test-view') + view instanceof ListView + view.description != 'lorem ipsum' + + cleanup: + jobManagement.setDryRun(false) + } + def 'update view ignoring changes'() { setup: jenkinsRule.instance.addView(new ListView('test-view')) @@ -669,6 +736,23 @@ class JenkinsJobManagementSpec extends Specification { jenkinsRule.instance.rootPath.child('userContent').child('foo.txt').readToString() == 'foo' } + def 'update user content, dry run'() { + setup: + jenkinsRule.instance.rootPath.child('userContent').child('foo.txt').write('lorem ipsum', 'UTF-8') + UserContent userContent = new UserContent('foo.txt', new ByteArrayInputStream('foo'.bytes)) + jobManagement.setDryRun(true) + + when: + jobManagement.createOrUpdateUserContent(userContent, false) + + then: + jenkinsRule.instance.rootPath.child('userContent').child('foo.txt').exists() + jenkinsRule.instance.rootPath.child('userContent').child('foo.txt').readToString() == 'lorem ipsum' + + cleanup: + jobManagement.setDryRun(false) + } + def 'do not update existing user content'() { setup: jenkinsRule.instance.rootPath.child('userContent').child('foo.txt').write('lorem ipsum', 'UTF-8')