Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ properties:
workflow_runs:
type: array
items:
$ref: "./list-workflow-runs-response-item.yaml"
$ref: "./workflow-run-metadata.yaml"
required:
- _pagination
- workflow_runs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ properties:
maximum: 100
workflow_instance_id:
type: string
task_queue_name:
type: string
status:
$ref: "./workflow-run-status.yaml"
priority:
Expand All @@ -53,6 +55,7 @@ required:
- id
- workflow_name
- workflow_version
- task_queue_name
- status
- priority
- created_at
2 changes: 2 additions & 0 deletions api/src/main/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ paths:
$ref: "./paths/teams__name_.yaml"
/team-memberships:
$ref: "./paths/team-memberships.yaml"
/workflow-instances/{id}:
$ref: "./paths/workflow-instances__id_.yaml"
/workflow-runs:
$ref: "./paths/workflow-runs.yaml"

Expand Down
58 changes: 58 additions & 0 deletions api/src/main/openapi/paths/workflow-instances__id_.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# This file is part of Dependency-Track.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.
get:
operationId: getWorkflowInstance
summary: Get a workflow instance
description: |-
Returns run metadata of a given workflow instance.

Multiple runs can share an instance ID, but only a single run can exist
in non-terminal state at any given time. Thus, this operation only
returns metadata if a run in non-terminal state exists.

To list all runs for an instance ID, including terminated ones,
use the `/workflow-runs` endpoint and filter by `workflow_instance_id`.

**Note:** This is an internal API endpoint and may change without notice.

Requires the `SYSTEM_CONFIGURATION` or `SYSTEM_CONFIGURATION_READ` permission.
tags:
- Workflows
parameters:
- name: id
description: ID of the workflow instance
in: path
schema:
type: string
required: true
responses:
"200":
description: Workflow run metadata
content:
application/json:
schema:
$ref: "../components/schemas/workflow-run-metadata.yaml"
"400":
$ref: "../components/responses/invalid-request-error.yaml"
"401":
$ref: "../components/responses/generic-unauthorized-error.yaml"
"403":
$ref: "../components/responses/generic-forbidden-error.yaml"
"404":
$ref: "../components/responses/generic-not-found-error.yaml"
default:
$ref: "../components/responses/generic-error.yaml"
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@

import alpine.server.auth.PermissionRequired;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
import org.dependencytrack.api.v2.WorkflowsApi;
import org.dependencytrack.api.v2.model.ListWorkflowRunsResponse;
import org.dependencytrack.api.v2.model.ListWorkflowRunsResponseItem;
import org.dependencytrack.api.v2.model.SortDirection;
import org.dependencytrack.api.v2.model.WorkflowRunStatus;
import org.dependencytrack.auth.Permissions;
Expand All @@ -49,6 +49,20 @@ public class WorkflowsResource extends AbstractApiResource implements WorkflowsA
this.dexEngine = dexEngine;
}

@Override
@PermissionRequired({
Permissions.Constants.SYSTEM_CONFIGURATION,
Permissions.Constants.SYSTEM_CONFIGURATION_READ
})
public Response getWorkflowInstance(String id) {
final WorkflowRunMetadata runMetadata = dexEngine.getRunMetadataByInstanceId(id);
if (runMetadata == null) {
throw new NotFoundException();
}

return Response.ok(convert(runMetadata)).build();
}

@Override
@PermissionRequired({
Permissions.Constants.SYSTEM_CONFIGURATION,
Expand Down Expand Up @@ -101,27 +115,7 @@ public Response listWorkflowRuns(

final var response = ListWorkflowRunsResponse.builder()
.workflowRuns(runsPage.items().stream()
.<ListWorkflowRunsResponseItem>map(
runMetadata -> ListWorkflowRunsResponseItem.builder()
.id(runMetadata.id())
.workflowName(runMetadata.workflowName())
.workflowVersion(runMetadata.workflowVersion())
.workflowInstanceId(runMetadata.workflowInstanceId())
.status(convert(runMetadata.status()))
.priority(runMetadata.priority())
.concurrencyKey(runMetadata.concurrencyKey())
.labels(runMetadata.labels())
.createdAt(runMetadata.createdAt().toEpochMilli())
.updatedAt(runMetadata.updatedAt() != null
? runMetadata.updatedAt().toEpochMilli()
: null)
.startedAt(runMetadata.startedAt() != null
? runMetadata.startedAt().toEpochMilli()
: null)
.completedAt(runMetadata.completedAt() != null
? runMetadata.completedAt().toEpochMilli()
: null)
.build())
.map(WorkflowsResource::convert)
.toList())
.pagination(createPaginationMetadata(getUriInfo(), runsPage))
.build();
Expand Down Expand Up @@ -152,4 +146,28 @@ private static WorkflowRunStatus convert(org.dependencytrack.dex.engine.api.Work
};
}

private static org.dependencytrack.api.v2.model.WorkflowRunMetadata convert(WorkflowRunMetadata runMetadata) {
return org.dependencytrack.api.v2.model.WorkflowRunMetadata.builder()
.id(runMetadata.id())
.workflowName(runMetadata.workflowName())
.workflowVersion(runMetadata.workflowVersion())
.workflowInstanceId(runMetadata.workflowInstanceId())
.taskQueueName(runMetadata.taskQueueName())
.status(convert(runMetadata.status()))
.priority(runMetadata.priority())
.concurrencyKey(runMetadata.concurrencyKey())
.labels(runMetadata.labels())
.createdAt(runMetadata.createdAt().toEpochMilli())
.updatedAt(runMetadata.updatedAt() != null
? runMetadata.updatedAt().toEpochMilli()
: null)
.startedAt(runMetadata.startedAt() != null
? runMetadata.startedAt().toEpochMilli()
: null)
.completedAt(runMetadata.completedAt() != null
? runMetadata.completedAt().toEpochMilli()
: null)
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
Expand All @@ -67,6 +68,78 @@ void afterEach() {
Mockito.reset(DEX_ENGINE_MOCK);
}

@Test
void getWorkflowInstanceShouldReturnMetadataOfWorkflowRun() {
initializeWithPermissions(Permissions.SYSTEM_CONFIGURATION_READ);

final var workflowRunMetadata = new WorkflowRunMetadata(
UUID.fromString("724c0700-4eeb-45f0-8ff4-8bba369c0174"),
"workflowName",
66,
"workflowInstanceId",
"taskQueueName",
WorkflowRunStatus.RUNNING,
"customStatus",
12,
"concurrencyKey",
Map.of("foo", "bar"),
Instant.ofEpochMilli(666666),
Instant.ofEpochMilli(777777),
Instant.ofEpochMilli(888888),
null);

doReturn(workflowRunMetadata)
.when(DEX_ENGINE_MOCK).getRunMetadataByInstanceId(eq("foo"));

final Response response = jersey
.target("/workflow-instances/foo")
.request()
.header(X_API_KEY, apiKey)
.get();
assertThat(response.getStatus()).isEqualTo(200);
assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
{
"id": "724c0700-4eeb-45f0-8ff4-8bba369c0174",
"workflow_name": "workflowName",
"workflow_version": 66,
"workflow_instance_id": "workflowInstanceId",
"task_queue_name": "taskQueueName",
"status": "RUNNING",
"created_at": 666666,
"priority": 12,
"concurrency_key": "concurrencyKey",
"labels": {
"foo": "bar"
},
"updated_at": 777777,
"started_at": 888888
}
""");
}

@Test
void getWorkflowInstanceShouldReturnNotFoundWhenNoMatchingRunExists() {
initializeWithPermissions(Permissions.SYSTEM_CONFIGURATION_READ);

doReturn(null)
.when(DEX_ENGINE_MOCK).getRunMetadataByInstanceId(eq("foo"));

final Response response = jersey
.target("/workflow-instances/foo")
.request()
.header(X_API_KEY, apiKey)
.get();
assertThat(response.getStatus()).isEqualTo(404);
assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
{
"type":"about:blank",
"status": 404,
"title": "Not Found",
"detail": "The requested resource could not be found."
}
""");
}

@Test
public void listWorkflowRunsShouldReturnWorkflowRunMetadata() {
initializeWithPermissions(Permissions.SYSTEM_CONFIGURATION_READ);
Expand All @@ -76,6 +149,7 @@ public void listWorkflowRunsShouldReturnWorkflowRunMetadata() {
"workflowName",
66,
"workflowInstanceId",
"taskQueueName",
WorkflowRunStatus.RUNNING,
"customStatus",
12,
Expand All @@ -102,6 +176,7 @@ public void listWorkflowRunsShouldReturnWorkflowRunMetadata() {
"workflow_name": "workflowName",
"workflow_version": 66,
"workflow_instance_id": "workflowInstanceId",
"task_queue_name": "taskQueueName",
"status": "RUNNING",
"created_at": 666666,
"priority": 12,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,22 +148,32 @@ <A, R> void registerActivity(
* Retrieve all data about a workflow run, including its full event history.
* <p>
* If only high-level information about the run is required, prefer to use
* {@link #getRunMetadata(UUID)} as it is significantly more efficient.
* {@link #getRunMetadataById(UUID)} as it is significantly more efficient.
*
* @param id ID of the workflow run.
* @return The run data, or {@code null} if no run with the given ID exists.
*/
@Nullable
WorkflowRun getRun(UUID id);
WorkflowRun getRunById(UUID id);

/**
* Retrieve metadata about a workflow run.
* Retrieve metadata about a workflow run by ID.
*
* @param id ID of the workflow run.
* @return The run metadata, or {@code null} if no run with the given ID exists.
*/
@Nullable
WorkflowRunMetadata getRunMetadata(UUID id);
WorkflowRunMetadata getRunMetadataById(UUID id);

/**
* Retrieve metadata about a workflow run by workflow instance ID.
*
* @param instanceId Workflow instance ID of the workflow run.
* @return Metadata of the matching run, <strong>if and only if</strong>
* the run is in non-terminal state. Metadata of terminal runs is not returned.
*/
@Nullable
WorkflowRunMetadata getRunMetadataByInstanceId(String instanceId);

Page<WorkflowRunMetadata> listRuns(ListWorkflowRunsRequest request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public record WorkflowRunMetadata(
String workflowName,
int workflowVersion,
@Nullable String workflowInstanceId,
String taskQueueName,
WorkflowRunStatus status,
@Nullable String customStatus,
int priority,
Expand Down
Loading
Loading