From a7c0c22699475fd31ceb283b8e35e5d516f7135f Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 6 Nov 2014 09:32:30 -0800 Subject: [PATCH] Add bulk Delete executions endpoint --- .../java/org/rundeck/api/RundeckClient.java | 28 ++++++- .../api/domain/DeleteExecutionsResponse.java | 84 +++++++++++++++++++ .../generator/DeleteExecutionsGenerator.java | 38 +++++++++ .../DeleteExecutionsResponseParser.java | 52 ++++++++++++ .../org/rundeck/api/RundeckClientTest.java | 82 ++++++++++++++++++ .../tapes/delete_executions_mixed.yaml | 26 ++++++ .../tapes/delete_executions_success.yaml | 26 ++++++ .../tapes/delete_executions_unauthorized.yaml | 26 ++++++ 8 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/rundeck/api/domain/DeleteExecutionsResponse.java create mode 100644 src/main/java/org/rundeck/api/generator/DeleteExecutionsGenerator.java create mode 100644 src/main/java/org/rundeck/api/parser/DeleteExecutionsResponseParser.java create mode 100644 src/test/resources/betamax/tapes/delete_executions_mixed.yaml create mode 100644 src/test/resources/betamax/tapes/delete_executions_success.yaml create mode 100644 src/test/resources/betamax/tapes/delete_executions_unauthorized.yaml diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index 8462dba..93ced66 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -23,6 +23,7 @@ import org.rundeck.api.RundeckApiException.RundeckApiLoginException; import org.rundeck.api.RundeckApiException.RundeckApiTokenException; import org.rundeck.api.domain.*; import org.rundeck.api.domain.RundeckExecution.ExecutionStatus; +import org.rundeck.api.generator.DeleteExecutionsGenerator; import org.rundeck.api.generator.ProjectConfigGenerator; import org.rundeck.api.generator.ProjectConfigPropertyGenerator; import org.rundeck.api.generator.ProjectGenerator; @@ -95,6 +96,7 @@ public class RundeckClient implements Serializable { V9(9), V10(10), V11(11), + V12(12), ; private int versionNumber; @@ -108,7 +110,7 @@ public class RundeckClient implements Serializable { } } /** Version of the API supported */ - public static final transient int API_VERSION = Version.V11.getVersionNumber(); + public static final transient int API_VERSION = Version.V12.getVersionNumber(); private static final String API = "/api/"; @@ -1643,6 +1645,30 @@ public class RundeckClient implements Serializable { return new ApiCall(this).get(apiPath, new AbortParser(rootXpath()+"/abort")); } + /** + * Delete a set of executions, identified by the given IDs + * + * @param executionIds set of identifiers for the executions - mandatory + * @return a {@link RundeckExecution} instance - won't be null + * @throws RundeckApiException in case of error when calling the API (non-existent execution with this ID) + * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) + * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) + * @throws IllegalArgumentException if the executionId is null + */ + public DeleteExecutionsResponse deleteExecutions(Set executionIds) + throws RundeckApiException, RundeckApiLoginException, + RundeckApiTokenException, IllegalArgumentException + { + AssertUtil.notNull(executionIds, "executionIds is mandatory to abort an execution !"); + final ApiPathBuilder apiPath = new ApiPathBuilder("/executions/delete").xml( + new DeleteExecutionsGenerator(executionIds) + ); + return new ApiCall(this).post( + apiPath, + new DeleteExecutionsResponseParser( rootXpath() + "/deleteExecutions") + ); + } + /* * History */ diff --git a/src/main/java/org/rundeck/api/domain/DeleteExecutionsResponse.java b/src/main/java/org/rundeck/api/domain/DeleteExecutionsResponse.java new file mode 100644 index 0000000..6d1f53b --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/DeleteExecutionsResponse.java @@ -0,0 +1,84 @@ +package org.rundeck.api.domain; + +import java.io.Serializable; +import java.util.List; + +/** + * DeleteExecutionsResponse is ... + * + * @author Greg Schueler + * @since 2014-11-06 + */ +public class DeleteExecutionsResponse implements Serializable { + + private static final long serialVersionUID = 1L; + + private int failedCount; + private int successCount; + private boolean allsuccessful; + private int requestCount; + private List failures; + + public int getFailedCount() { + return failedCount; + } + + public void setFailedCount(final int failedCount) { + this.failedCount = failedCount; + } + + public int getSuccessCount() { + return successCount; + } + + public void setSuccessCount(final int successCount) { + this.successCount = successCount; + } + + public boolean isAllsuccessful() { + return allsuccessful; + } + + public void setAllsuccessful(final boolean allsuccessful) { + this.allsuccessful = allsuccessful; + } + + public int getRequestCount() { + return requestCount; + } + + public void setRequestCount(final int requestCount) { + this.requestCount = requestCount; + } + + public List getFailures() { + return failures; + } + + public void setFailures(final List failures) { + this.failures = failures; + } + + public static class DeleteFailure implements Serializable{ + + private static final long serialVersionUID = 1L; + private Long executionId; + private String message; + + public Long getExecutionId() { + return executionId; + } + + public void setExecutionId(final Long executionId) { + this.executionId = executionId; + } + + public String getMessage() { + return message; + } + + public void setMessage(final String message) { + this.message = message; + } + } +} diff --git a/src/main/java/org/rundeck/api/generator/DeleteExecutionsGenerator.java b/src/main/java/org/rundeck/api/generator/DeleteExecutionsGenerator.java new file mode 100644 index 0000000..8efd2bf --- /dev/null +++ b/src/main/java/org/rundeck/api/generator/DeleteExecutionsGenerator.java @@ -0,0 +1,38 @@ +package org.rundeck.api.generator; + +import org.dom4j.DocumentFactory; +import org.dom4j.Element; +import org.rundeck.api.domain.ProjectConfig; + +import java.util.List; +import java.util.Set; + +/** + * DeleteExecutionsGenerator is ... + * + * @author Greg Schueler + * @since 2014-11-06 + */ +public class DeleteExecutionsGenerator extends BaseDocGenerator { + private Set executionIds; + + public DeleteExecutionsGenerator(final Set executionIds) { + this.executionIds = executionIds; + } + + @Override public Element generateXmlElement() { + Element rootElem = DocumentFactory.getInstance().createElement("executions"); + for (Long executionId : executionIds) { + rootElem.addElement("execution").addAttribute("id", Long.toString(executionId)); + } + return rootElem; + } + + public Set getExecutionIds() { + return executionIds; + } + + public void setExecutionIds(final Set executionIds) { + this.executionIds = executionIds; + } +} diff --git a/src/main/java/org/rundeck/api/parser/DeleteExecutionsResponseParser.java b/src/main/java/org/rundeck/api/parser/DeleteExecutionsResponseParser.java new file mode 100644 index 0000000..c3de8af --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/DeleteExecutionsResponseParser.java @@ -0,0 +1,52 @@ +package org.rundeck.api.parser; + +import org.dom4j.Node; +import org.rundeck.api.domain.DeleteExecutionsResponse; + +import java.util.ArrayList; +import java.util.List; + +/** + * DeleteExecutionsResponseParser is ... + * + * @author Greg Schueler + * @since 2014-11-06 + */ +public class DeleteExecutionsResponseParser implements XmlNodeParser { + private String xpath; + + public DeleteExecutionsResponseParser(final String xpath) { + this.xpath = xpath; + } + + @Override public DeleteExecutionsResponse parseXmlNode(final Node node) { + final Node baseNode = xpath != null ? node.selectSingleNode(xpath) : node; + + final DeleteExecutionsResponse response = new DeleteExecutionsResponse(); + response.setAllsuccessful(Boolean.parseBoolean(baseNode.valueOf("@allsuccessful"))); + response.setRequestCount(Integer.parseInt(baseNode.valueOf("@requestCount"))); + response.setSuccessCount(Integer.parseInt(baseNode.valueOf("successful/@count"))); + + final Node failedNode = baseNode.selectSingleNode("failed"); + //parse failures + final List failures = new ArrayList + (); + int failedCount = 0; + if (null != failedNode) { + failedCount = Integer.parseInt(baseNode.valueOf("failed/@count")); + final List list = baseNode.selectNodes("failed/execution"); + + for (final Object o : list) { + final Node execNode = (Node) o; + final DeleteExecutionsResponse.DeleteFailure deleteFailure = + new DeleteExecutionsResponse.DeleteFailure(); + deleteFailure.setExecutionId(Long.parseLong(execNode.valueOf("@id"))); + deleteFailure.setMessage(execNode.valueOf("@message")); + failures.add(deleteFailure); + } + } + response.setFailedCount(failedCount); + response.setFailures(failures); + return response; + } +} diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java index fb01b80..27fa3eb 100644 --- a/src/test/java/org/rundeck/api/RundeckClientTest.java +++ b/src/test/java/org/rundeck/api/RundeckClientTest.java @@ -56,6 +56,7 @@ public class RundeckClientTest { public static final String TEST_TOKEN_5 = "C3O6d5O98Kr6Dpv71sdE4ERdCuU12P6d"; public static final String TEST_TOKEN_6 = "Do4d3NUD5DKk21DR4sNK755RcPk618vn"; public static final String TEST_TOKEN_7 = "8Dp9op111ER6opsDRkddvE86K9sE499s"; + public static final String TEST_TOKEN_8 = "GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI"; @Rule public Recorder recorder = new Recorder(); @@ -1490,6 +1491,87 @@ public class RundeckClientTest { } } + /** + * delete executions with failure + */ + @Test + @Betamax(tape = "delete_executions_unauthorized", mode = TapeMode.READ_ONLY) + public void deleteExecutionsUnauthorized() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_8, 12); + final DeleteExecutionsResponse response = client.deleteExecutions( + new HashSet() {{ + add(640L); + add(641L); + }} + ); + Assert.assertEquals(2, response.getRequestCount()); + Assert.assertEquals(0, response.getSuccessCount()); + Assert.assertEquals(2, response.getFailedCount()); + Assert.assertFalse(response.isAllsuccessful()); + Assert.assertNotNull(response.getFailures()); + Assert.assertEquals(2, response.getFailures().size()); + Assert.assertEquals(Long.valueOf(641L), response.getFailures().get(0).getExecutionId()); + Assert.assertEquals( + "Unauthorized: Delete execution in project test", + response.getFailures().get(0).getMessage() + ); + Assert.assertEquals(Long.valueOf(640L), response.getFailures().get(1).getExecutionId()); + Assert.assertEquals( + "Unauthorized: Delete execution in project test", + response.getFailures().get(1).getMessage() + ); + } + /** + * delete executions with success + */ + @Test + @Betamax(tape = "delete_executions_success", mode = TapeMode.READ_ONLY) + public void deleteExecutionsSuccess() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_8, 12); + final DeleteExecutionsResponse response = client.deleteExecutions( + new HashSet() {{ + add(640L); + add(641L); + }} + ); + Assert.assertEquals(2, response.getRequestCount()); + Assert.assertEquals(2, response.getSuccessCount()); + Assert.assertEquals(0, response.getFailedCount()); + Assert.assertTrue(response.isAllsuccessful()); + Assert.assertNotNull(response.getFailures()); + Assert.assertEquals(0, response.getFailures().size()); + } + /** + * delete executions mixed success + */ + @Test + @Betamax(tape = "delete_executions_mixed", mode = TapeMode.READ_ONLY) + public void deleteExecutionsMixed() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_8, 12); + final DeleteExecutionsResponse response = client.deleteExecutions( + new HashSet() {{ + add(642L); + add(640L); + add(1640L); + }} + ); + Assert.assertEquals(3, response.getRequestCount()); + Assert.assertEquals(1, response.getSuccessCount()); + Assert.assertEquals(2, response.getFailedCount()); + Assert.assertFalse(response.isAllsuccessful()); + Assert.assertNotNull(response.getFailures()); + Assert.assertEquals(2, response.getFailures().size()); + Assert.assertEquals(Long.valueOf(1640L), response.getFailures().get(0).getExecutionId()); + Assert.assertEquals( + "Execution Not found: 1640", + response.getFailures().get(0).getMessage() + ); + Assert.assertEquals(Long.valueOf(640L), response.getFailures().get(1).getExecutionId()); + Assert.assertEquals( + "Execution Not found: 640", + response.getFailures().get(1).getMessage() + ); + } @Before public void setUp() throws Exception { // not that you can put whatever here, because we don't actually connect to the RunDeck instance diff --git a/src/test/resources/betamax/tapes/delete_executions_mixed.yaml b/src/test/resources/betamax/tapes/delete_executions_mixed.yaml new file mode 100644 index 0000000..c21657d --- /dev/null +++ b/src/test/resources/betamax/tapes/delete_executions_mixed.yaml @@ -0,0 +1,26 @@ +!tape +name: delete_executions_mixed +interactions: +- recorded: 2014-11-06T17:29:44.266Z + request: + method: POST + uri: http://rundeck.local:4440/api/12/executions/delete + headers: + Accept: text/xml + Content-Type: application/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + Transfer-Encoding: chunked + User-Agent: RunDeck API Java Client 12 + X-RunDeck-Auth-Token: GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI + response: + status: 200 + headers: + Content-Type: application/xml;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=smlj6wcfe4yccemt6bptnmgy;Path=/ + X-Rundeck-API-Version: '12' + X-Rundeck-API-XML-Response-Wrapper: 'false' + body: !!binary |- + PGRlbGV0ZUV4ZWN1dGlvbnMgcmVxdWVzdENvdW50PSczJyBhbGxzdWNjZXNzZnVsPSdmYWxzZSc+CiAgPHN1Y2Nlc3NmdWwgY291bnQ9JzEnIC8+CiAgPGZhaWxlZCBjb3VudD0nMic+CiAgICA8ZXhlY3V0aW9uIGlkPScxNjQwJyBtZXNzYWdlPSdFeGVjdXRpb24gTm90IGZvdW5kOiAxNjQwJyAvPgogICAgPGV4ZWN1dGlvbiBpZD0nNjQwJyBtZXNzYWdlPSdFeGVjdXRpb24gTm90IGZvdW5kOiA2NDAnIC8+CiAgPC9mYWlsZWQ+CjwvZGVsZXRlRXhlY3V0aW9ucz4= diff --git a/src/test/resources/betamax/tapes/delete_executions_success.yaml b/src/test/resources/betamax/tapes/delete_executions_success.yaml new file mode 100644 index 0000000..d412342 --- /dev/null +++ b/src/test/resources/betamax/tapes/delete_executions_success.yaml @@ -0,0 +1,26 @@ +!tape +name: delete_executions_success +interactions: +- recorded: 2014-11-06T17:24:56.487Z + request: + method: POST + uri: http://rundeck.local:4440/api/12/executions/delete + headers: + Accept: text/xml + Content-Type: application/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + Transfer-Encoding: chunked + User-Agent: RunDeck API Java Client 12 + X-RunDeck-Auth-Token: GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI + response: + status: 200 + headers: + Content-Type: application/xml;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=1xueq6cjfrsnxjn43hcfadqyk;Path=/ + X-Rundeck-API-Version: '12' + X-Rundeck-API-XML-Response-Wrapper: 'false' + body: !!binary |- + PGRlbGV0ZUV4ZWN1dGlvbnMgcmVxdWVzdENvdW50PScyJyBhbGxzdWNjZXNzZnVsPSd0cnVlJz4KICA8c3VjY2Vzc2Z1bCBjb3VudD0nMicgLz4KPC9kZWxldGVFeGVjdXRpb25zPg== diff --git a/src/test/resources/betamax/tapes/delete_executions_unauthorized.yaml b/src/test/resources/betamax/tapes/delete_executions_unauthorized.yaml new file mode 100644 index 0000000..2f89d99 --- /dev/null +++ b/src/test/resources/betamax/tapes/delete_executions_unauthorized.yaml @@ -0,0 +1,26 @@ +!tape +name: delete_executions_success +interactions: +- recorded: 2014-11-06T17:15:24.673Z + request: + method: POST + uri: http://rundeck.local:4440/api/12/executions/delete + headers: + Accept: text/xml + Content-Type: application/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + Transfer-Encoding: chunked + User-Agent: RunDeck API Java Client 12 + X-RunDeck-Auth-Token: GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI + response: + status: 200 + headers: + Content-Type: application/xml;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=1kzmot0r2scpjfxlcpfthh7tz;Path=/ + X-Rundeck-API-Version: '12' + X-Rundeck-API-XML-Response-Wrapper: 'false' + body: !!binary |- + PGRlbGV0ZUV4ZWN1dGlvbnMgcmVxdWVzdENvdW50PScyJyBhbGxzdWNjZXNzZnVsPSdmYWxzZSc+CiAgPHN1Y2Nlc3NmdWwgY291bnQ9JzAnIC8+CiAgPGZhaWxlZCBjb3VudD0nMic+CiAgICA8ZXhlY3V0aW9uIGlkPSc2NDEnIG1lc3NhZ2U9J1VuYXV0aG9yaXplZDogRGVsZXRlIGV4ZWN1dGlvbiBpbiBwcm9qZWN0IHRlc3QnIC8+CiAgICA8ZXhlY3V0aW9uIGlkPSc2NDAnIG1lc3NhZ2U9J1VuYXV0aG9yaXplZDogRGVsZXRlIGV4ZWN1dGlvbiBpbiBwcm9qZWN0IHRlc3QnIC8+CiAgPC9mYWlsZWQ+CjwvZGVsZXRlRXhlY3V0aW9ucz4=