diff --git a/CHANGELOG.md b/CHANGELOG.md index 77aec149..8dfc1f8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased ### ⚠ Breaking ### ⭐ New Features +- Added support for parent-child-relationships of projects with Dependency-Track v4.7 and newer (fixes [#139](https://github.com/jenkinsci/dependency-track-plugin/issues/139)) + ### 🐞 Bugs Fixed - Searching on the result page was partially broken due to [a bug in bootstrap-vue 2.22+](https://github.com/bootstrap-vue/bootstrap-vue/issues/6967) diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ApiClient.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ApiClient.java index 8fa20a5a..03d69212 100644 --- a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ApiClient.java +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ApiClient.java @@ -327,6 +327,11 @@ public void updateProjectProperties(@NonNull final String projectUuid, @NonNull rawProject.elementOpt("group", properties.getGroup()); // overwrite description only if it is set (means not null) rawProject.elementOpt("description", properties.getDescription()); + // set new parent project if it is set (means not null) + if (properties.getParentId() != null) { + JSONObject newParent = new JSONObject().elementOpt("uuid", properties.getParentId()); + rawProject.element("parent", newParent); + } // 3. update project updateProject(projectUuid, rawProject); } diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java index dd0bb8f6..e413c675 100644 --- a/src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java @@ -69,7 +69,7 @@ */ @Extension @Symbol("dependencyTrackPublisher") // This indicates to Jenkins that this is an implementation of an extension point. -public final class DescriptorImpl extends BuildStepDescriptor implements Serializable { +public class DescriptorImpl extends BuildStepDescriptor implements Serializable { private static final long serialVersionUID = -2018722914973282748L; diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ProjectParser.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ProjectParser.java index c9db5d5a..881c438f 100644 --- a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ProjectParser.java +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ProjectParser.java @@ -44,6 +44,7 @@ Project parse(final JSONObject json) { .active(activeStr != null ? Boolean.parseBoolean(activeStr) : null) .swidTagId(getKeyOrNull(json, "swidTagId")) .group(getKeyOrNull(json, "group")) + .parent(json.has("parent") ? ProjectParser.parse(json.getJSONObject("parent")) : null) .build(); } diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ProjectProperties.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ProjectProperties.java index bfa31cac..0642d2ff 100644 --- a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ProjectProperties.java +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ProjectProperties.java @@ -16,19 +16,27 @@ package org.jenkinsci.plugins.DependencyTrack; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; +import hudson.RelativePath; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; +import hudson.model.Item; +import hudson.util.ListBoxModel; import java.io.Serializable; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import jenkins.model.Jenkins; import lombok.EqualsAndHashCode; import lombok.Getter; import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.verb.POST; import static org.jenkinsci.plugins.DependencyTrack.PluginUtil.areAllElementsOfType; @@ -46,22 +54,32 @@ public final class ProjectProperties extends AbstractDescribableImpl tags; /** * SWID Tag ID for the project */ + @Nullable private String swidTagId; /** * Group to set for the project */ + @Nullable private String group; /** * Description to set for the project */ + @Nullable private String description; + + /** + * UUID of the parent project + */ + @Nullable + private String parentId; @NonNull public List getTags() { @@ -111,6 +129,11 @@ public void setDescription(final String description) { this.description = StringUtils.trimToNull(description); } + @DataBoundSetter + public void setParentId(final String parentId) { + this.parentId = StringUtils.trimToNull(parentId); + } + @NonNull public String getTagsAsText() { return StringUtils.join(getTags(), System.lineSeparator()); @@ -129,5 +152,19 @@ private List normalizeTags(final Collection values) { @Extension public static class DescriptorImpl extends Descriptor { + + /** + * Retrieve the projects to populate the dropdown. + * + * @param dependencyTrackUrl the base URL to Dependency-Track + * @param dependencyTrackApiKey the API key to use for authentication + * @param item used to lookup credentials in job config + * @return ListBoxModel + */ + @POST + public ListBoxModel doFillParentIdItems(@RelativePath("..") @QueryParameter final String dependencyTrackUrl, @RelativePath("..") @QueryParameter final String dependencyTrackApiKey, @AncestorInPath @Nullable final Item item) { + org.jenkinsci.plugins.DependencyTrack.DescriptorImpl pluginDescriptor = Jenkins.get().getDescriptorByType(org.jenkinsci.plugins.DependencyTrack.DescriptorImpl.class); + return pluginDescriptor.doFillProjectIdItems(dependencyTrackUrl, dependencyTrackApiKey, item); + } } } diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/Project.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/Project.java index cf75498c..2051d2c8 100644 --- a/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/Project.java +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/Project.java @@ -23,4 +23,5 @@ public class Project implements Serializable { private Boolean active; private String swidTagId; private String group; + private Project parent; } diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/config.jelly b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/config.jelly index d5d46036..210c25c7 100644 --- a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/config.jelly @@ -28,5 +28,8 @@ limitations under the License. + + + diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/config.properties b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/config.properties index 827cc021..f5088828 100644 --- a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/config.properties +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/config.properties @@ -16,3 +16,4 @@ tags=Tags swidTagId=SWID Tag ID group=Namespace / Group / Vendor description=Description +parentId=Parent project \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/config_de.properties b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/config_de.properties index 79575254..3b52dc75 100644 --- a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/config_de.properties +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/config_de.properties @@ -16,3 +16,4 @@ tags=Tags swidTagId=SWID Tag ID group=Namensraum / Gruppe / Hersteller description=Beschreibung +parentId=\u00dcbergeordnetes Projekt \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/help-parentId.html b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/help-parentId.html new file mode 100644 index 00000000..10f6cab2 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/help-parentId.html @@ -0,0 +1,3 @@ +
+ The ID (UUID) of the parent project. +
diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/help-parentId_de.html b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/help-parentId_de.html new file mode 100644 index 00000000..8e07eccc --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/ProjectProperties/help-parentId_de.html @@ -0,0 +1,3 @@ +
+ Die ID (UUID) des übergeordneten Projektes. +
diff --git a/src/test/java/org/jenkinsci/plugins/DependencyTrack/ApiClientTest.java b/src/test/java/org/jenkinsci/plugins/DependencyTrack/ApiClientTest.java index 0ba929b8..29c9bd41 100644 --- a/src/test/java/org/jenkinsci/plugins/DependencyTrack/ApiClientTest.java +++ b/src/test/java/org/jenkinsci/plugins/DependencyTrack/ApiClientTest.java @@ -359,7 +359,7 @@ public void updateProjectPropertiesTest() throws ApiClientException, Interrupted .get(ApiClient.PROJECT_URL + "/uuid-3", (request, response) -> { assertThat(request.requestHeaders().contains(ApiClient.API_KEY_HEADER, API_KEY, false)).isTrue(); assertThat(request.requestHeaders().contains(HttpHeaderNames.ACCEPT, HttpHeaderValues.APPLICATION_JSON, true)).isTrue(); - return response.sendString(Mono.just("{\"name\":\"test-project\",\"uuid\":\"uuid-3\",\"version\":\"1.2.3\",\"tags\":[{\"name\":\"tag1\"},{\"name\":\"tag2\"}]}")); + return response.sendString(Mono.just("{\"name\":\"test-project\",\"uuid\":\"uuid-3\",\"version\":\"1.2.3\",\"tags\":[{\"name\":\"tag1\"},{\"name\":\"tag2\"}],\"parent\":{\"uuid\":\"old-parent\"}}")); }) .post(ApiClient.PROJECT_URL, (request, response) -> { assertThat(request.requestHeaders().contains(ApiClient.API_KEY_HEADER, API_KEY, false)).isTrue(); @@ -380,6 +380,7 @@ public void updateProjectPropertiesTest() throws ApiClientException, Interrupted props.setSwidTagId("my swid tag id"); props.setGroup("my group"); props.setDescription("my description"); + props.setParentId("parent-uuid"); assertThatCode(() -> uut.updateProjectProperties("uuid-3", props)).doesNotThrowAnyException(); completionSignal.await(5, TimeUnit.SECONDS); @@ -388,6 +389,7 @@ public void updateProjectPropertiesTest() throws ApiClientException, Interrupted assertThat(project.getSwidTagId()).isEqualTo(props.getSwidTagId()); assertThat(project.getGroup()).isEqualTo(props.getGroup()); assertThat(project.getDescription()).isEqualTo(props.getDescription()); + assertThat(project.getParent()).hasFieldOrPropertyWithValue("uuid", props.getParentId()); } @Test diff --git a/src/test/java/org/jenkinsci/plugins/DependencyTrack/ProjectPropertiesTest.java b/src/test/java/org/jenkinsci/plugins/DependencyTrack/ProjectPropertiesTest.java index 93bd3e8f..f9760a08 100644 --- a/src/test/java/org/jenkinsci/plugins/DependencyTrack/ProjectPropertiesTest.java +++ b/src/test/java/org/jenkinsci/plugins/DependencyTrack/ProjectPropertiesTest.java @@ -15,15 +15,22 @@ */ package org.jenkinsci.plugins.DependencyTrack; +import hudson.util.ReflectionUtils; +import java.lang.reflect.Field; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import jenkins.model.Jenkins; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * @@ -70,8 +77,32 @@ void verifyEmptyStringsShallBeNull() { uut.setDescription(""); uut.setGroup("\t"); uut.setSwidTagId(System.lineSeparator()); + uut.setParentId(" "); assertThat(uut.getDescription()).isNull(); assertThat(uut.getGroup()).isNull(); assertThat(uut.getSwidTagId()).isNull(); + assertThat(uut.getParentId()).isNull(); + } + + @Nested + class DescriptorImplTest { + + @Test + void doFillParentIdItemsTest() throws Exception { + Field instanceField = ReflectionUtils.findField(Jenkins.class, "theInstance", Jenkins.class); + ReflectionUtils.makeAccessible(instanceField); + Jenkins origJenkins = (Jenkins) instanceField.get(null); + Jenkins mockJenkins = mock(Jenkins.class); + ReflectionUtils.setField(instanceField, null, mockJenkins); + org.jenkinsci.plugins.DependencyTrack.DescriptorImpl descriptorMock = mock(org.jenkinsci.plugins.DependencyTrack.DescriptorImpl.class); + when(mockJenkins.getDescriptorByType(org.jenkinsci.plugins.DependencyTrack.DescriptorImpl.class)).thenReturn(descriptorMock); + ProjectProperties.DescriptorImpl uut = new ProjectProperties.DescriptorImpl(); + + uut.doFillParentIdItems("url", "key", null); + + ReflectionUtils.setField(instanceField, null, origJenkins); + verify(mockJenkins).getDescriptorByType(org.jenkinsci.plugins.DependencyTrack.DescriptorImpl.class); + verify(descriptorMock).doFillProjectIdItems("url", "key", null); + } } }