diff --git a/docs_src/docs/CLI.md b/docs_src/docs/CLI.md index 7598f9b..762abbc 100644 --- a/docs_src/docs/CLI.md +++ b/docs_src/docs/CLI.md @@ -237,4 +237,109 @@ Because of the waterfall: number->match->regex(es), in addition with switches, a * `I` - search is case insensitive * Unlike original old `itemId` mode, item target is used also in `exact` match mode, and not only in regex mode. -Strict api is currently not used, is considered as experimental, but shoudl resolve any issue the versatile api may failedue to its number->match->regex(es) waterfall nature. \ No newline at end of file +Strict api is currently not used, is considered as experimental, but shoudl resolve any issue the versatile api may failedue to its number->match->regex(es) waterfall nature. + +## Print Queue API + +**Since 1.4.13**, a new API endpoint has been added to retrieve the current queue state as a plaintext list. + +### Overview + +The Print Queue API allows you to retrieve the current build queue as a simple plaintext list, with one job display name per line. This is useful for monitoring, scripting, or integration with external tools. + +### Endpoints + +#### Safe API (with CSRF protection) +``` +POST http://${JENKINS_URL}/simpleMove/printQueue +``` +- Requires POST method with CSRF token (crumb) +- Requires `SIMPLE_QUEUE_MOVE_PERMISSION` + +#### Unsafe API (without CSRF protection) +``` +GET/POST http://${JENKINS_URL}/simpleMoveUnsafe/printQueue +``` +- Works with both GET and POST methods +- No CSRF token required +- Must be explicitly enabled in plugin configuration (`enableUnsafe`) + +### Parameters + +- **`buildable`** (optional): Controls which items to include in the output + - `true` or omitted (default): Shows only buildable items + - `false`: Shows all items in the queue (including blocked/waiting items) +- **`viewName`** (optional): Filter queue items by view + - If specified, only shows items visible in that view + - If omitted, shows all queue items + +### Output Format + +The API returns a plaintext response with one job display name per line: +``` +Project A +Project B +Project C +``` + +### Queue Order + +The API returns the **sorted queue** order, which reflects: +- The current state after any manual reordering done through the Simple Queue plugin +- The actual execution order that Jenkins will follow +- All move operations (UP, DOWN, TOP, BOTTOM) that have been applied + +### Examples + +#### Get buildable items (default) +```bash +# Safe API (requires CSRF token) +curl -X POST --user username:apitoken \ + "http://${JENKINS_URL}/simpleMove/printQueue" + +# Unsafe API (no CSRF token needed, must be enabled) +curl "http://${JENKINS_URL}/simpleMoveUnsafe/printQueue" +``` + +#### Get all items (including non-buildable) +```bash +# Safe API +curl -X POST --user username:apitoken \ + "http://${JENKINS_URL}/simpleMove/printQueue?buildable=false" + +# Unsafe API +curl "http://${JENKINS_URL}/simpleMoveUnsafe/printQueue" +``` + +#### Get queue for a specific view +```bash +# Safe API +curl -X POST --user username:apitoken \ + "http://${JENKINS_URL}/simpleMove/printQueue?viewName=my_view" + +# Unsafe API +curl "http://${JENKINS_URL}/simpleMoveUnsafe/printQueue?viewName=my_view" +``` + +#### Using in scripts +```bash +#!/bin/bash +# Get the queue and process each job +curl -s --user username:apitoken \ + "http://${JENKINS_URL}/simpleMove/printQueue" | \ +while IFS= read -r job_name; do + echo "Processing: $job_name" + # Your processing logic here +done +``` + +```bash +curl "http://${JENKINS_URL}/simpleMoveUnsafe/printQueue" | wc -l + +``` + +### Security Considerations + +- The **safe API** (`/simpleMove/printQueue`) requires proper authentication and CSRF protection, making it suitable for web-based integrations +- The **unsafe API** (`/simpleMoveUnsafe/printQueue`) bypasses CSRF protection for easier CLI/automation use, but must be explicitly enabled in the plugin configuration +- Both APIs require appropriate Jenkins permissions diff --git a/src/main/java/cz/mendelu/xotradov/MoveAction.java b/src/main/java/cz/mendelu/xotradov/MoveAction.java index 0e8a9ce..a2a74a5 100644 --- a/src/main/java/cz/mendelu/xotradov/MoveAction.java +++ b/src/main/java/cz/mendelu/xotradov/MoveAction.java @@ -60,4 +60,27 @@ public void doMove(final StaplerRequest2 request, final StaplerResponse2 respons response.setStatus(StaplerResponse2.SC_INTERNAL_SERVER_ERROR); } } + + /** + * Print the current queue as plaintext list + * @param request Stapler request from user + * @param response Stapler response send back to users browser + */ + @RequirePOST + public void doPrintQueue(final StaplerRequest2 request, final StaplerResponse2 response) { + Jenkins j = Jenkins.get(); + if (!j.hasPermission(PermissionHandler.SIMPLE_QUEUE_MOVE_PERMISSION)) { + response.setStatus(StaplerResponse2.SC_FORBIDDEN); + return; + } + try { + Queue queue = j.getQueue(); + if (queue != null) { + printQueueImpl(request, response, queue, j); + } + } catch (Exception e) { + logger.warning(e.toString()); + response.setStatus(StaplerResponse2.SC_INTERNAL_SERVER_ERROR); + } + } } diff --git a/src/main/java/cz/mendelu/xotradov/MoveActionWorker.java b/src/main/java/cz/mendelu/xotradov/MoveActionWorker.java index efee22f..2e8921d 100644 --- a/src/main/java/cz/mendelu/xotradov/MoveActionWorker.java +++ b/src/main/java/cz/mendelu/xotradov/MoveActionWorker.java @@ -1185,4 +1185,47 @@ private void setSorter(Queue queue) { private void resort(Queue queue) { queue.getSorter().sortBuildableItems(queue.getBuildableItems()); } + + /** + * Print the current queue state as plaintext list + * @param request Stapler request from user + * @param response Stapler response to write the queue information + * @param queue The Jenkins queue + * @param jenkins Jenkins instance + * @throws IOException if writing to response fails + */ + protected void printQueueImpl(StaplerRequest2 request, StaplerResponse2 response, Queue queue, Jenkins jenkins) throws IOException { + response.setContentType("text/plain; charset=UTF-8"); + response.setStatus(StaplerResponse2.SC_OK); + PrintWriter writer = response.getWriter(); + + if (writer != null) { + String buildableParam = request.getParameter("buildable"); + boolean onlyBuildable = buildableParam == null || !buildableParam.equalsIgnoreCase("false"); + + // Get view if specified + String viewName = request.getParameter(VIEW_NAME_PARAM_NAME); + View view = viewName != null ? jenkins.getView(viewName) : null; + + // Get queue items - either from view or from full queue + Collection queueItems; + if (view != null && view.isFilterQueue()) { + queueItems = view.getQueueItems(); + } else { + queueItems = Arrays.asList(queue.getItems()); + } + + for (Queue.Item item : queueItems) { + if (item.task != null) { + if (onlyBuildable) { + if (item.isBuildable()) { + writer.println(item.task.getDisplayName()); + } + } else { + writer.println(item.task.getDisplayName()); + } + } + } + } + } } diff --git a/src/main/java/cz/mendelu/xotradov/UnsafeMoveAction.java b/src/main/java/cz/mendelu/xotradov/UnsafeMoveAction.java index 62bdc5e..a9dae62 100644 --- a/src/main/java/cz/mendelu/xotradov/UnsafeMoveAction.java +++ b/src/main/java/cz/mendelu/xotradov/UnsafeMoveAction.java @@ -46,4 +46,25 @@ public void doMove(final StaplerRequest2 request, final StaplerResponse2 respons response.setStatus(StaplerResponse2.SC_INTERNAL_SERVER_ERROR); } } + + /** + * Print the current queue as plaintext list (unsafe - no CSRF protection) + * @param request Stapler request from user + * @param response Stapler response send back to users browser + */ + public void doPrintQueue(final StaplerRequest2 request, final StaplerResponse2 response) { + if (!SimpleQueueConfig.getInstance().isEnableUnsafe()) { + throw new IllegalArgumentException("Unsafe print queue api attempted without being enabled"); + } + Jenkins j = Jenkins.get(); + try { + Queue queue = j.getQueue(); + if (queue != null) { + printQueueImpl(request, response, queue, j); + } + } catch (Exception e) { + logger.warning(e.toString()); + response.setStatus(StaplerResponse2.SC_INTERNAL_SERVER_ERROR); + } + } } diff --git a/src/test/java/cz/mendelu/xotradov/test/moves/MoveAction_doPrintQueueTest.java b/src/test/java/cz/mendelu/xotradov/test/moves/MoveAction_doPrintQueueTest.java new file mode 100644 index 0000000..58baba5 --- /dev/null +++ b/src/test/java/cz/mendelu/xotradov/test/moves/MoveAction_doPrintQueueTest.java @@ -0,0 +1,153 @@ +package cz.mendelu.xotradov.test.moves; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +import cz.mendelu.xotradov.MoveAction; +import cz.mendelu.xotradov.test.TestHelper; +import hudson.model.FreeStyleProject; +import hudson.model.Queue; +import java.io.PrintWriter; +import java.io.StringWriter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.StaplerResponse2; +import org.mockito.Mockito; + +@WithJenkins +public class MoveAction_doPrintQueueTest { + + private TestHelper helper; + + @AfterEach + public void waitForClean() throws Exception { + helper.cleanup(); + } + + @Test + public void doPrintQueueWithBuildableItems(JenkinsRule jenkinsRule) throws Exception { + helper = new TestHelper(jenkinsRule); + try { + long maxTestTime = 10000; + helper.fillQueueFor(maxTestTime); + FreeStyleProject C = helper.createAndSchedule("C", maxTestTime); + FreeStyleProject D = helper.createAndSchedule("D", maxTestTime); + Queue queue = jenkinsRule.jenkins.getQueue(); + + MoveAction moveAction = helper.getMoveAction(); + StaplerRequest2 request = Mockito.mock(StaplerRequest2.class); + StaplerResponse2 response = Mockito.mock(StaplerResponse2.class); + + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(writer); + when(request.getParameter("buildable")).thenReturn(null); // default to true + + moveAction.doPrintQueue(request, response); + writer.flush(); + + String output = stringWriter.toString(); + assertTrue(output.contains("D"), "Output should contain project D"); + assertTrue(output.contains("C"), "Output should contain project C"); + + // Verify format - each project on its own line + String[] lines = output.trim().split("\n"); + assertEquals(2, lines.length, "Should have 2 buildable items"); + assertEquals("D", lines[0].trim()); + assertEquals("C", lines[1].trim()); + } catch (Exception e) { + fail("Test failed with exception: " + e.getMessage()); + } + } + + @Test + public void doPrintQueueWithBuildableParamTrue(JenkinsRule jenkinsRule) throws Exception { + helper = new TestHelper(jenkinsRule); + try { + long maxTestTime = 10000; + helper.fillQueueFor(maxTestTime); + FreeStyleProject C = helper.createAndSchedule("C", maxTestTime); + FreeStyleProject D = helper.createAndSchedule("D", maxTestTime); + + MoveAction moveAction = helper.getMoveAction(); + StaplerRequest2 request = Mockito.mock(StaplerRequest2.class); + StaplerResponse2 response = Mockito.mock(StaplerResponse2.class); + + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(writer); + when(request.getParameter("buildable")).thenReturn("true"); + + moveAction.doPrintQueue(request, response); + writer.flush(); + + String output = stringWriter.toString(); + assertTrue(output.contains("D"), "Output should contain project D"); + assertTrue(output.contains("C"), "Output should contain project C"); + } catch (Exception e) { + fail("Test failed with exception: " + e.getMessage()); + } + } + + @Test + public void doPrintQueueWithBuildableParamFalse(JenkinsRule jenkinsRule) throws Exception { + helper = new TestHelper(jenkinsRule); + try { + long maxTestTime = 10000; + helper.fillQueueFor(maxTestTime); + FreeStyleProject C = helper.createAndSchedule("C", maxTestTime); + FreeStyleProject D = helper.createAndSchedule("D", maxTestTime); + + MoveAction moveAction = helper.getMoveAction(); + StaplerRequest2 request = Mockito.mock(StaplerRequest2.class); + StaplerResponse2 response = Mockito.mock(StaplerResponse2.class); + + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(writer); + when(request.getParameter("buildable")).thenReturn("false"); + + moveAction.doPrintQueue(request, response); + writer.flush(); + + String output = stringWriter.toString(); + // With buildable=false, should show all items including non-buildable + assertTrue(output.contains("D"), "Output should contain project D"); + assertTrue(output.contains("C"), "Output should contain project C"); + } catch (Exception e) { + fail("Test failed with exception: " + e.getMessage()); + } + } + + @Test + public void doPrintQueueEmptyQueue(JenkinsRule jenkinsRule) throws Exception { + helper = new TestHelper(jenkinsRule); + try { + // Don't add any items to queue + Queue queue = jenkinsRule.jenkins.getQueue(); + assertEquals(0, queue.getItems().length, "Queue should be empty"); + + MoveAction moveAction = helper.getMoveAction(); + StaplerRequest2 request = Mockito.mock(StaplerRequest2.class); + StaplerResponse2 response = Mockito.mock(StaplerResponse2.class); + + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(writer); + when(request.getParameter("buildable")).thenReturn(null); + + moveAction.doPrintQueue(request, response); + writer.flush(); + + String output = stringWriter.toString(); + assertTrue(output.trim().isEmpty(), "Output should be empty for empty queue"); + } catch (Exception e) { + fail("Test failed with exception: " + e.getMessage()); + } + } +} + +// Made with Bob diff --git a/src/test/java/cz/mendelu/xotradov/test/moves/UnsafeMoveAction_doPrintQueueTest.java b/src/test/java/cz/mendelu/xotradov/test/moves/UnsafeMoveAction_doPrintQueueTest.java new file mode 100644 index 0000000..ca69579 --- /dev/null +++ b/src/test/java/cz/mendelu/xotradov/test/moves/UnsafeMoveAction_doPrintQueueTest.java @@ -0,0 +1,186 @@ +package cz.mendelu.xotradov.test.moves; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +import cz.mendelu.xotradov.SimpleQueueConfig; +import cz.mendelu.xotradov.UnsafeMoveAction; +import cz.mendelu.xotradov.test.TestHelper; +import hudson.model.Action; +import hudson.model.FreeStyleProject; +import hudson.model.Queue; +import java.io.PrintWriter; +import java.io.StringWriter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.StaplerResponse2; +import org.mockito.Mockito; + +@WithJenkins +public class UnsafeMoveAction_doPrintQueueTest { + + private TestHelper helper; + + @AfterEach + public void waitForClean() throws Exception { + helper.cleanup(); + } + + private UnsafeMoveAction getUnsafeMoveAction(JenkinsRule jenkinsRule) { + for (Action action : jenkinsRule.jenkins.getActions()) { + if (action instanceof UnsafeMoveAction) { + return (UnsafeMoveAction) action; + } + } + return null; + } + + @Test + public void doPrintQueueWithUnsafeEnabled(JenkinsRule jenkinsRule) throws Exception { + helper = new TestHelper(jenkinsRule); + try { + // Enable unsafe API + SimpleQueueConfig.getInstance().setEnableUnsafe(true); + + long maxTestTime = 10000; + helper.fillQueueFor(maxTestTime); + FreeStyleProject C = helper.createAndSchedule("C", maxTestTime); + FreeStyleProject D = helper.createAndSchedule("D", maxTestTime); + Queue queue = jenkinsRule.jenkins.getQueue(); + + UnsafeMoveAction unsafeMoveAction = getUnsafeMoveAction(jenkinsRule); + assertNotNull(unsafeMoveAction, "UnsafeMoveAction should be available"); + + StaplerRequest2 request = Mockito.mock(StaplerRequest2.class); + StaplerResponse2 response = Mockito.mock(StaplerResponse2.class); + + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(writer); + when(request.getParameter("buildable")).thenReturn(null); + + unsafeMoveAction.doPrintQueue(request, response); + writer.flush(); + + String output = stringWriter.toString(); + assertTrue(output.contains("D"), "Output should contain project D"); + assertTrue(output.contains("C"), "Output should contain project C"); + + // Verify format + String[] lines = output.trim().split("\n"); + assertEquals(2, lines.length, "Should have 2 buildable items"); + } catch (Exception e) { + fail("Test failed with exception: " + e.getMessage()); + } finally { + SimpleQueueConfig.getInstance().setEnableUnsafe(false); + } + } + + @Test + public void doPrintQueueWithUnsafeDisabled(JenkinsRule jenkinsRule) throws Exception { + helper = new TestHelper(jenkinsRule); + try { + // Disable unsafe API + SimpleQueueConfig.getInstance().setEnableUnsafe(false); + + long maxTestTime = 10000; + helper.fillQueueFor(maxTestTime); + FreeStyleProject C = helper.createAndSchedule("C", maxTestTime); + FreeStyleProject D = helper.createAndSchedule("D", maxTestTime); + + UnsafeMoveAction unsafeMoveAction = getUnsafeMoveAction(jenkinsRule); + assertNotNull(unsafeMoveAction, "UnsafeMoveAction should be available"); + + StaplerRequest2 request = Mockito.mock(StaplerRequest2.class); + StaplerResponse2 response = Mockito.mock(StaplerResponse2.class); + + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(writer); + when(request.getParameter("buildable")).thenReturn(null); + + // Should throw IllegalArgumentException when unsafe is disabled + assertThrows(IllegalArgumentException.class, () -> { + unsafeMoveAction.doPrintQueue(request, response); + }, "Should throw IllegalArgumentException when unsafe API is disabled"); + + } catch (Exception e) { + fail("Test failed with exception: " + e.getMessage()); + } + } + + @Test + public void doPrintQueueWithBuildableParamFalse(JenkinsRule jenkinsRule) throws Exception { + helper = new TestHelper(jenkinsRule); + try { + // Enable unsafe API + SimpleQueueConfig.getInstance().setEnableUnsafe(true); + + long maxTestTime = 10000; + helper.fillQueueFor(maxTestTime); + FreeStyleProject C = helper.createAndSchedule("C", maxTestTime); + FreeStyleProject D = helper.createAndSchedule("D", maxTestTime); + + UnsafeMoveAction unsafeMoveAction = getUnsafeMoveAction(jenkinsRule); + assertNotNull(unsafeMoveAction, "UnsafeMoveAction should be available"); + + StaplerRequest2 request = Mockito.mock(StaplerRequest2.class); + StaplerResponse2 response = Mockito.mock(StaplerResponse2.class); + + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(writer); + when(request.getParameter("buildable")).thenReturn("false"); + + unsafeMoveAction.doPrintQueue(request, response); + writer.flush(); + + String output = stringWriter.toString(); + assertTrue(output.contains("D"), "Output should contain project D"); + assertTrue(output.contains("C"), "Output should contain project C"); + } catch (Exception e) { + fail("Test failed with exception: " + e.getMessage()); + } finally { + SimpleQueueConfig.getInstance().setEnableUnsafe(false); + } + } + + @Test + public void doPrintQueueEmptyQueue(JenkinsRule jenkinsRule) throws Exception { + helper = new TestHelper(jenkinsRule); + try { + // Enable unsafe API + SimpleQueueConfig.getInstance().setEnableUnsafe(true); + + // Don't add any items to queue + Queue queue = jenkinsRule.jenkins.getQueue(); + assertEquals(0, queue.getItems().length, "Queue should be empty"); + + UnsafeMoveAction unsafeMoveAction = getUnsafeMoveAction(jenkinsRule); + assertNotNull(unsafeMoveAction, "UnsafeMoveAction should be available"); + + StaplerRequest2 request = Mockito.mock(StaplerRequest2.class); + StaplerResponse2 response = Mockito.mock(StaplerResponse2.class); + + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(writer); + when(request.getParameter("buildable")).thenReturn(null); + + unsafeMoveAction.doPrintQueue(request, response); + writer.flush(); + + String output = stringWriter.toString(); + assertTrue(output.trim().isEmpty(), "Output should be empty for empty queue"); + } catch (Exception e) { + fail("Test failed with exception: " + e.getMessage()); + } finally { + SimpleQueueConfig.getInstance().setEnableUnsafe(false); + } + } +} + +// Made with Bob