Skip to content

Commit

Permalink
support isLatest flag
Browse files Browse the repository at this point in the history
closes #286
  • Loading branch information
sephiroth-j committed Dec 20, 2024
1 parent 81f46bd commit 5c0f542
Show file tree
Hide file tree
Showing 14 changed files with 126 additions and 58 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
### ⚠ Breaking
- require Jenkins 2.479.1 or newer
- require Java 17 or newer (required since Jenkins 2.479.1)
- require Dependency-Track 4.12 or newer ([#286](https://github.com/jenkinsci/dependency-track-plugin/issues/286))

### ⭐ New Features
- Support "isLatest" flag ([#286](https://github.com/jenkinsci/dependency-track-plugin/issues/286))

### 🐞 Bugs Fixed

## v5.2.0 - 2024-12-08
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ Asynchronous publishing simply uploads the SBOM to Dependency-Track and the job
![build summary](docs/images/jenkins-build-summary.png)
![findings](docs/images/jenkins-build-findings.png) ![policy violations](docs/images/jenkins-build-policy-violations.png)

## Version Compatibility Matrix
Plugin Version | Dependency-Track | Jenkins | Java
---------------| ---------------- | ------- | ----
6.0.x (next) | 4.12+ | 2.479.1+ | 17+
5.2.x (current) | 4.9+ | 2.440.1+ | 11+

## Global Configuration
To setup, navigate to Jenkins > System Configuration and complete the Dependency-Track section.

Expand Down Expand Up @@ -75,7 +81,9 @@ Once configured with a valid URL and API key, simply configure a job to publish
- SWID tag ID
- group/vendor
- description
- ID of parent project (for Dependency-Track v4.7 and newer)
- ID of parent project
- name and version of parent project (as an alternative to the ID)
- "is latest version" flag

The use of environment variables in the form `${VARIABLE}` is supported here.

Expand Down
33 changes: 23 additions & 10 deletions src/main/java/org/jenkinsci/plugins/DependencyTrack/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,7 @@ public List<Violation> getViolations(@NonNull final String projectUuid) throws A

@NonNull
@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
public UploadResult upload(@Nullable final String projectId, @Nullable final String projectName, @Nullable final String projectVersion, @NonNull final FilePath artifact,
boolean autoCreateProject, @Nullable final ProjectProperties properties) throws ApiClientException {
public UploadResult upload(@NonNull final ProjectData project, @NonNull final FilePath artifact) throws ApiClientException {
final String encodedScan;
try (var in = artifact.read()) {
encodedScan = Base64.getEncoder().encodeToString(in.readAllBytes());
Expand All @@ -303,17 +302,19 @@ public UploadResult upload(@Nullable final String projectId, @Nullable final Str
// Creates the JSON payload that will be sent to Dependency-Track
final var bomSubmitRequest = new JSONObject();
bomSubmitRequest.element("bom", encodedScan);
if (StringUtils.isNotBlank(projectId)) {
bomSubmitRequest.element("project", projectId);
if (StringUtils.isNotBlank(project.id())) {
bomSubmitRequest.element("project", project.id());
} else {
bomSubmitRequest.element("projectName", projectName)
.element("projectVersion", projectVersion)
.element("autoCreate", autoCreateProject);
bomSubmitRequest.element("projectName", project.name())
.element("projectVersion", project.version())
.element("autoCreate", project.autoCreate());
}
final var properties = project.properties();
if (properties != null) {
bomSubmitRequest.elementOpt("parentUUID", properties.getParentId())
.elementOpt("parentName", properties.getParentName())
.elementOpt("parentVersion", properties.getParentVersion());
.elementOpt("parentVersion", properties.getParentVersion())
.elementOpt("isLatestProjectVersion", properties.getIsLatest());
}
final var request = createRequest(URI.create(BOM_URL), "PUT", RequestBody.create(bomSubmitRequest.toString(), APPLICATION_JSON));
return executeWithRetry(() -> {
Expand Down Expand Up @@ -365,14 +366,18 @@ public void updateProjectProperties(@NonNull final String projectUuid, @NonNull
updates.elementOpt("group", properties.getGroup());
// overwrite description only if it is set (means not null)
updates.elementOpt("description", properties.getDescription());
// overwrite isLatest only if it is set (means not null)
updates.elementOpt("isLatest", properties.getIsLatest());
// set new parent project if it is set (means not null)
if (properties.getParentId() != null) {
JSONObject newParent = new JSONObject().elementOpt("uuid", properties.getParentId());
updates.element("parent", newParent);
}

// update project
updateProject(projectUuid, updates);
// update project if necessary
if (!updates.isEmpty()) {

Check warning on line 378 in src/main/java/org/jenkinsci/plugins/DependencyTrack/ApiClient.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 378 is only partially covered, one branch is missing
updateProject(projectUuid, updates);
}
}

@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
Expand Down Expand Up @@ -450,4 +455,12 @@ private interface RetryAction<T, E extends IOException> {

T doWithRetry() throws E;
}

static record ProjectData(@Nullable String id,
@Nullable String name,
@Nullable String version,
boolean autoCreate,
@Nullable ProjectProperties properties) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,8 @@ public void perform(@NonNull final Run<?, ?> run, @NonNull final FilePath worksp
final ProjectProperties effectiveProjectProperties = expandProjectProperties(env);
logger.log(Messages.Builder_Publishing(effectiveUrl, effectiveArtifact));
final ApiClient apiClient = clientFactory.create(effectiveUrl, effectiveApiKey, logger, getEffectiveConnectionTimeout(), getEffectiveReadTimeout());
final UploadResult uploadResult = apiClient.upload(projectId, effectiveProjectName, effectiveProjectVersion,
artifactFilePath, effectiveAutocreate, effectiveProjectProperties);
final var projectData = new ApiClient.ProjectData(projectId, effectiveProjectName, effectiveProjectVersion, effectiveAutocreate, effectiveProjectProperties);
final UploadResult uploadResult = apiClient.upload(projectData, artifactFilePath);

if (!uploadResult.isSuccess()) {
throw new AbortException(Messages.Builder_Upload_Failed());
Expand Down Expand Up @@ -615,6 +615,7 @@ private void updateProjectProperties(final ConsoleLogger logger, final ApiClient
projectProperties.getDescription() != null
|| projectProperties.getGroup() != null
|| projectProperties.getSwidTagId() != null
|| projectProperties.getIsLatest() != null

Check warning on line 618 in src/main/java/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 618 is only partially covered, one branch is missing
|| !projectProperties.getTags().isEmpty());

if (doUpdateProject) {
Expand Down Expand Up @@ -646,6 +647,7 @@ private ProjectProperties expandProjectProperties(final EnvVars env) {
Optional.ofNullable(projectProperties.getParentVersion()).map(env::expand).ifPresent(expandedProperties::setParentVersion);
Optional.ofNullable(projectProperties.getSwidTagId()).map(env::expand).ifPresent(expandedProperties::setSwidTagId);
expandedProperties.setTags(projectProperties.getTags().stream().map(env::expand).toList());
expandedProperties.setIsLatest(projectProperties.getIsLatest());
return expandedProperties;
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ private FormValidation testConnection(final String dependencyTrackUrl, final Str
return FormValidation.error(Messages.Publisher_ConnectionTest_Error(poweredBy));
}
final VersionNumber version = apiClient.getVersion();
final var requiredVersion = new VersionNumber("4.9.0");
final var requiredVersion = new VersionNumber("4.12.0");
if (version.isOlderThan(requiredVersion)) {
return FormValidation.error(Messages.Publisher_ConnectionTest_VersionWarning(version, requiredVersion));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import jenkins.model.Jenkins;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
Expand Down Expand Up @@ -93,6 +94,13 @@ public final class ProjectProperties extends AbstractDescribableImpl<ProjectProp
@Nullable
private String parentVersion;

/**
* Mark this version of the project as the latest version
*/
@Nullable
@Setter(onMethod_ = {@DataBoundSetter})
private Boolean isLatest;

@NonNull
public List<String> getTags() {
return normalizeTags(tags);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ limitations under the License.
<f:entry field="description" title="${%description}">
<f:textbox id="description" />
</f:entry>
<f:entry field="isLatest" title="${%isLatest}">
<f:checkbox id="isLatest" />
</f:entry>
<f:entry field="parentId" title="${%parentId}">
<f:select id="parentId"/>
</f:entry>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ group=Namespace / Group / Vendor
description=Description
parentId=Parent project
parentName=Parent name
parentVersion=Parent version
parentVersion=Parent version
isLatest=Is latest version
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ group=Namensraum / Gruppe / Hersteller
description=Beschreibung
parentId=\u00dcbergeordnetes Projekt
parentName=Name des \u00fcbergeordneten Projekts
parentVersion=Version des \u00fcbergeordneten Projekts
parentVersion=Version des \u00fcbergeordneten Projekts
isLatest=Ist aktuellste Version
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
<p>Marks this version of the project as the latest.</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
<p>Markiert diese Version des Projekts als die neueste.</p>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import net.sf.json.JSONObject;
import okhttp3.OkHttpClient;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.jenkinsci.plugins.DependencyTrack.ApiClient.ProjectData;
import org.jenkinsci.plugins.DependencyTrack.model.Project;
import org.jenkinsci.plugins.DependencyTrack.model.UploadResult;
import org.junit.jupiter.api.AfterEach;
Expand Down Expand Up @@ -365,11 +366,13 @@ void uploadTestWithUuid(@TempDir Path tmp, JenkinsRule r) throws IOException, In

final var props = new ProjectProperties();
props.setParentId("parent-uuid");
props.setIsLatest(true);

ApiClient uut = createClient();
assertThat(uut.upload("uuid-1", null, null, new FilePath(bom.toFile()), false, props)).isEqualTo(new UploadResult(true, "uuid-1"));
var data = new ProjectData("uuid-1", null, null, false, props);
assertThat(uut.upload(data, new FilePath(bom.toFile()))).isEqualTo(new UploadResult(true, "uuid-1"));

String expectedBody = "{\"bom\":\"PHRlc3QgLz4=\",\"project\":\"uuid-1\",\"parentUUID\":\"parent-uuid\"}";
String expectedBody = "{\"bom\":\"PHRlc3QgLz4=\",\"project\":\"uuid-1\",\"parentUUID\":\"parent-uuid\",\"isLatestProjectVersion\":true}";
completionSignal.await(5, TimeUnit.SECONDS);
assertThat(completionSignal.getCount()).isZero();
assertThat(requestBody.get()).isEqualTo(expectedBody);
Expand Down Expand Up @@ -401,7 +404,8 @@ void uploadTestWithName(@TempDir Path tmp, JenkinsRule r) throws IOException, In
props.setParentVersion("parent-version");

ApiClient uut = createClient();
assertThat(uut.upload(null, "p1", "v1", new FilePath(bom.toFile()), false, props)).isEqualTo(new UploadResult(true));
var data = new ProjectData(null, "p1", "v1", false, props);
assertThat(uut.upload(data, new FilePath(bom.toFile()))).isEqualTo(new UploadResult(true));

String expectedBody = "{\"bom\":\"PHRlc3QgLz4=\",\"projectName\":\"p1\",\"projectVersion\":\"v1\",\"autoCreate\":false,\"parentName\":\"parent-name\",\"parentVersion\":\"parent-version\"}";
completionSignal.await(5, TimeUnit.SECONDS);
Expand All @@ -414,35 +418,36 @@ void uploadTestWithErrors(@TempDir Path tmp, JenkinsRule r) throws IOException {
ApiClient uut;
File bom = tmp.resolve("bom.xml").toFile();
bom.createNewFile();
var data = new ProjectData(null, "p1", "v1", true, null);

server = HttpServer.create().host("localhost").port(0).route(routes -> routes.put(ApiClient.BOM_URL, (request, response) -> response.status(HttpResponseStatus.BAD_REQUEST).send())).bindNow();
uut = createClient();
assertThat(uut.upload(null, "p1", "v1", new FilePath(bom), true, null)).isEqualTo(new UploadResult(false));
assertThat(uut.upload(data, new FilePath(bom))).isEqualTo(new UploadResult(false));
verify(logger).log(Messages.Builder_Payload_Invalid());
server.disposeNow();

server = HttpServer.create().host("localhost").port(0).route(routes -> routes.put(ApiClient.BOM_URL, (request, response) -> response.status(HttpResponseStatus.UNAUTHORIZED).send())).bindNow();
uut = createClient();
assertThat(uut.upload(null, "p1", "v1", new FilePath(bom), true, null)).isEqualTo(new UploadResult(false));
assertThat(uut.upload(data, new FilePath(bom))).isEqualTo(new UploadResult(false));
verify(logger).log(Messages.Builder_Unauthorized());
server.disposeNow();

server = HttpServer.create().host("localhost").port(0).route(routes -> routes.put(ApiClient.BOM_URL, (request, response) -> response.status(HttpResponseStatus.NOT_FOUND).send())).bindNow();
uut = createClient();
assertThat(uut.upload(null, "p1", "v1", new FilePath(bom), true, null)).isEqualTo(new UploadResult(false));
assertThat(uut.upload(data, new FilePath(bom))).isEqualTo(new UploadResult(false));
verify(logger).log(Messages.Builder_Project_NotFound());
server.disposeNow();

server = HttpServer.create().host("localhost").port(0).route(routes -> routes.put(ApiClient.BOM_URL, (request, response) -> response.status(HttpResponseStatus.GONE).send())).bindNow();
uut = createClient();
assertThat(uut.upload(null, "p1", "v1", new FilePath(bom), true, null)).isEqualTo(new UploadResult(false));
assertThat(uut.upload(data, new FilePath(bom))).isEqualTo(new UploadResult(false));
verify(logger).log(Messages.ApiClient_Error_Connection(HttpResponseStatus.GONE.code(), HttpResponseStatus.GONE.reasonPhrase()));
server.disposeNow();

File mockFile = mock(File.class);
when(mockFile.getPath()).thenReturn(tmp.toAbsolutePath().toString());
FilePath fileWithError = new FilePath(mockFile);
assertThat(uut.upload(null, "p1", "v1", fileWithError, true, null)).isEqualTo(new UploadResult(false));
assertThat(uut.upload(data, fileWithError)).isEqualTo(new UploadResult(false));
verify(logger).log(startsWith(Messages.Builder_Error_Processing(tmp.toAbsolutePath().toString(), "")));

final var httpClient = mock(OkHttpClient.class);
Expand All @@ -452,7 +457,7 @@ void uploadTestWithErrors(@TempDir Path tmp, JenkinsRule r) throws IOException {
doThrow(new java.net.SocketTimeoutException("oops"))
.when(call).execute();

assertThatCode(() -> uutWithMock.upload(null, "p1", "v1", new FilePath(bom), true, null))
assertThatCode(() -> uutWithMock.upload(data, new FilePath(bom)))
.hasMessage(Messages.ApiClient_Error_Connection("", ""))
.hasCauseInstanceOf(java.net.SocketTimeoutException.class);
verify(httpClient, times(2)).newCall(any(okhttp3.Request.class));
Expand Down Expand Up @@ -542,7 +547,7 @@ void updateProjectPropertiesTest(JenkinsRule r) throws InterruptedException {
assertThat(project.getParent()).hasFieldOrPropertyWithValue("uuid", props.getParentId());
assertThat(updatedProject.has("parentUuid")).isFalse();

assertThatCode(() -> createClient().updateProjectProperties("uuid-unknown", new ProjectProperties()))
assertThatCode(() -> createClient().updateProjectProperties("uuid-unknown", props))
.hasMessage(Messages.ApiClient_Error_ProjectUpdate("uuid-unknown", HttpResponseStatus.NOT_FOUND.code(), HttpResponseStatus.NOT_FOUND.reasonPhrase()))
.hasNoCause();
verify(logger).log("");
Expand All @@ -563,8 +568,10 @@ void updateProjectPropertiesTestWithStatus304(JenkinsRule r) throws InterruptedE
.bindNow();

ApiClient uut = createClient();
final var props = new ProjectProperties();
props.setSwidTagId("swid");

assertThatCode(() -> uut.updateProjectProperties("uuid-3", new ProjectProperties())).doesNotThrowAnyException();
assertThatCode(() -> uut.updateProjectProperties("uuid-3", props)).doesNotThrowAnyException();
completionSignal.await(5, TimeUnit.SECONDS);
assertThat(completionSignal.getCount()).isZero();
}
Expand All @@ -574,11 +581,13 @@ void updateProjectPropertiesTestWithErrorsOnUpdate() throws IOException {
final var httpClient = mock(OkHttpClient.class);
final var call = mock(okhttp3.Call.class);
final var uut = createClient(httpClient);
final var props = new ProjectProperties();
props.setSwidTagId("swid");
when(httpClient.newCall(any(okhttp3.Request.class))).thenReturn(call);
doThrow(new ConnectException("oops"))
.when(call).execute();

assertThatCode(() -> uut.updateProjectProperties("foo", new ProjectProperties()))
assertThatCode(() -> uut.updateProjectProperties("foo", props))
.hasMessage(Messages.ApiClient_Error_Connection("", ""))
.hasCauseInstanceOf(ConnectException.class);
verify(httpClient, times(2)).newCall(any(okhttp3.Request.class));
Expand Down
Loading

0 comments on commit 5c0f542

Please sign in to comment.