From 8d8ea63fcefe0028c5168dffd9210955c41fe773 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Sun, 4 Oct 2020 16:25:29 +0100 Subject: [PATCH] Fix: Don't destroy credentials when re-creating folders As noted in JENKINS-44681, any credentials on folders are lost when re-creating folders, as we don't merge the existing credentials with the newly created folder. To do this, we need to find any instances of `FolderCredentialsProvider.FolderCredentialsProperty` in the `Folder`'s and if present, merge the credentials. We need to promote our dependency on `cloudbees-folder` to a runtime dependency, as we now need to reference the `Folder` and any `FolderCredentialsProperty`s. Note that in `JenkinsJobManagementSpec`, we need to use `getNodeTemplate` to ensure that the real implementation of `getXml` is called, which will execute the `configuredBlocks`. Because we need to call to the `configure` method, we need to call out to a new Groovy file, `DslItemConfigurer`, which processes the new `AbstractFolderProperty` and converts it to the correct XML representation. When parsing it, we need to make sure we add a valid XML header, otherwise `XmlParser` will reject it. Closes JENKINS-44681. --- job-dsl-plugin/build.gradle | 2 +- .../jobdsl/plugin/DslItemConfigurer.groovy | 21 ++++++++ .../jobdsl/plugin/JenkinsJobManagement.java | 26 +++++++++ .../plugin/JenkinsJobManagementSpec.groovy | 54 ++++++++++++++++++- job-dsl-plugin/src/test/resources/folder.xml | 2 + 5 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/DslItemConfigurer.groovy create mode 100644 job-dsl-plugin/src/test/resources/folder.xml diff --git a/job-dsl-plugin/build.gradle b/job-dsl-plugin/build.gradle index 7cf78e856..a9fa6d894 100644 --- a/job-dsl-plugin/build.gradle +++ b/job-dsl-plugin/build.gradle @@ -89,6 +89,7 @@ dependencies { compile(project(':job-dsl-core')) { exclude group: 'org.jvnet.hudson', module:'xstream' } + jenkinsPlugins 'org.jenkins-ci.plugins:cloudbees-folder:5.14' jenkinsPlugins 'org.jenkins-ci.plugins:structs:1.19' jenkinsPlugins 'org.jenkins-ci.plugins:script-security:1.54' optionalJenkinsPlugins('org.jenkins-ci.plugins:vsphere-cloud:1.1.11') { @@ -99,7 +100,6 @@ dependencies { optionalJenkinsPlugins 'io.jenkins:configuration-as-code:1.15' 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:matrix-auth:1.3' jenkinsTest 'org.jenkins-ci.plugins:nested-view:1.14' jenkinsTest 'org.jenkins-ci.plugins:credentials:2.1.10' diff --git a/job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/DslItemConfigurer.groovy b/job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/DslItemConfigurer.groovy new file mode 100644 index 000000000..0a6d21d6d --- /dev/null +++ b/job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/DslItemConfigurer.groovy @@ -0,0 +1,21 @@ +package javaposse.jobdsl.plugin + +import com.cloudbees.hudson.plugins.folder.AbstractFolderProperty +import hudson.model.Items +import javaposse.jobdsl.dsl.Item + +class DslItemConfigurer { + private static final String XML_HEADER = "" + + /** + * Merge an {@link AbstractFolderProperty} into a new {@link Item}'s properties. + * + * @param item the property to merge + * @param dslItem the DSL item to merge the properties into + */ + static void mergeCredentials(AbstractFolderProperty property, Item dslItem) { + String xml = Items.XSTREAM2.toXML(property) + Node node = new XmlParser().parseText(XML_HEADER + xml) + dslItem.configure { p -> p / 'properties' << node } + } +} 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 64b93431f..e2eeeb716 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 @@ -1,5 +1,13 @@ package javaposse.jobdsl.plugin; +import static hudson.model.Result.UNSTABLE; +import static hudson.model.View.createViewFromXML; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.cloudbees.hudson.plugins.folder.AbstractFolderProperty; +import com.cloudbees.hudson.plugins.folder.Folder; +import com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider.FolderCredentialsProperty; import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; import com.thoughtworks.xstream.io.xml.XppDriver; @@ -52,6 +60,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -444,6 +453,7 @@ private String lookupJob(String path) throws IOException { } private boolean updateExistingItem(AbstractItem item, javaposse.jobdsl.dsl.Item dslItem) { + mergeCredentials(item, dslItem); String config = dslItem.getXml(); item.checkPermission(Item.EXTENDED_READ); @@ -576,6 +586,22 @@ private void renameJob(Job from, String to) throws IOException { from.renameTo(FilenameUtils.getName(to)); } + private void mergeCredentials(AbstractItem item, javaposse.jobdsl.dsl.Item dslItem) { + if (item instanceof Folder) { + Folder folder = (Folder) item; + Optional> maybeProperty = + folder.getProperties().stream() + .filter(p -> p instanceof FolderCredentialsProperty) + .findFirst(); + + if (maybeProperty.isPresent()) { + LOGGER.log(Level.FINE, format("Merging credentials for %s", item.getName())); + DslItemConfigurer.mergeCredentials(maybeProperty.get(), dslItem); + } + } + } + + @SuppressWarnings("unchecked") private static void move(Item item, DirectlyModifiableTopLevelItemGroup destination) throws IOException { Items.move((I) item, destination); 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 9bed35cef..705c5457f 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 @@ -1,7 +1,16 @@ package javaposse.jobdsl.plugin +import com.cloudbees.hudson.plugins.folder.AbstractFolder +import com.cloudbees.hudson.plugins.folder.AbstractFolderProperty +import com.cloudbees.hudson.plugins.folder.AbstractFolderPropertyDescriptor import com.cloudbees.hudson.plugins.folder.Folder +import com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider +import com.cloudbees.plugins.credentials.CredentialsScope +import com.cloudbees.plugins.credentials.domains.Domain +import com.cloudbees.plugins.credentials.domains.DomainCredentials +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl import com.google.common.io.Resources +import hudson.Extension import hudson.FilePath import hudson.model.AbstractBuild import hudson.model.AllView @@ -632,6 +641,32 @@ class JenkinsJobManagementSpec extends Specification { e.message == 'Type of item "my-job" does not match existing type, item type can not be changed' } + def 'createOrUpdateConfig should preserve credentials if they exist on a folder'() { + setup: + Folder folder = jenkinsRule.createProject(Folder, 'folder') + folder.addProperty(createCredentialProperty()) + + when: + jobManagement.createOrUpdateConfig(createItem('folder', '/folder.xml'), false) + + then: + def actual = jenkinsRule.jenkins.getItem('folder') + actual.properties.size() == 1 + } + + def 'createOrUpdateConfig should ignore other properties on the folder'() { + setup: + Folder folder = jenkinsRule.createProject(Folder, 'folder') + folder.addProperty(new FakeProperty()) + + when: + jobManagement.createOrUpdateConfig(createItem('folder', '/folder.xml'), false) + + then: + def actual = jenkinsRule.jenkins.getItem('folder') + actual.properties.size() == 0 + } + def 'createOrUpdateView should work if view type changes'() { setup: jenkinsRule.jenkins.addView(new AllView('foo')) @@ -973,10 +1008,27 @@ class JenkinsJobManagementSpec extends Specification { Resources.toString(Resources.getResource(resourceName), UTF_8) } + private static FolderCredentialsProvider.FolderCredentialsProperty createCredentialProperty() { + UsernamePasswordCredentialsImpl credentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, '', + '', '', '') + DomainCredentials[] domainCredentials = new DomainCredentials[1] + domainCredentials[0] = new DomainCredentials(Domain.global(), Collections.singletonList(credentials)) + + new FolderCredentialsProvider.FolderCredentialsProperty(domainCredentials) + } + + private static class FakeProperty extends AbstractFolderProperty> { + + @Extension(optional = true) + static class DescriptorImpl extends AbstractFolderPropertyDescriptor { + final String displayName = '' + } + } + private Item createItem(String name, String config) { new Item(jobManagement, name) { @Override - Node getNode() { + Node getNodeTemplate() { new XmlParser().parse(JenkinsJobManagementSpec.getResourceAsStream(config)) } } diff --git a/job-dsl-plugin/src/test/resources/folder.xml b/job-dsl-plugin/src/test/resources/folder.xml new file mode 100644 index 000000000..4579fdb0d --- /dev/null +++ b/job-dsl-plugin/src/test/resources/folder.xml @@ -0,0 +1,2 @@ + +