From 00268997c1dae073da1fe605d928453685c5ae62 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 3 Jan 2013 11:45:26 -0800 Subject: [PATCH] Add Builder for RundeckClient support multiple API versions --- src/main/java/org/rundeck/api/ApiCall.java | 43 ++++--- .../java/org/rundeck/api/RundeckClient.java | 112 ++++++++++++++---- .../org/rundeck/api/RundeckClientBuilder.java | 104 ++++++++++++++++ .../org/rundeck/api/RundeckClientTest.java | 37 ++++-- 4 files changed, 237 insertions(+), 59 deletions(-) create mode 100644 src/main/java/org/rundeck/api/RundeckClientBuilder.java diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java index 51d6143..880845e 100644 --- a/src/main/java/org/rundeck/api/ApiCall.java +++ b/src/main/java/org/rundeck/api/ApiCall.java @@ -15,22 +15,7 @@ */ package org.rundeck.api; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.ProxySelector; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; -import java.util.Map.Entry; import org.apache.commons.lang.StringUtils; -import org.apache.http.cookie.Cookie; import org.apache.http.Header; import org.apache.http.HttpException; import org.apache.http.HttpRequest; @@ -38,7 +23,6 @@ import org.apache.http.HttpRequestInterceptor; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.ParseException; -import org.apache.http.client.CookieStore; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpDelete; @@ -65,6 +49,21 @@ import org.rundeck.api.parser.ParserHelper; import org.rundeck.api.parser.XmlNodeParser; import org.rundeck.api.util.AssertUtil; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.ProxySelector; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; + /** * Class responsible for making the HTTP API calls * @@ -157,7 +156,7 @@ class ApiCall { */ public void testTokenAuth() throws RundeckApiTokenException { try { - execute(new HttpGet(client.getUrl() + RundeckClient.API_ENDPOINT + "/system/info")); + execute(new HttpGet(client.getUrl() + client.getApiEndpoint() + "/system/info")); } catch (RundeckApiTokenException e) { throw e; } catch (RundeckApiException e) { @@ -178,7 +177,7 @@ class ApiCall { */ public T get(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException { - return execute(new HttpGet(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath), parser); + return execute(new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath), parser); } /** @@ -193,7 +192,7 @@ class ApiCall { */ public InputStream get(ApiPathBuilder apiPath) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException { - ByteArrayInputStream response = execute(new HttpGet(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath)); + ByteArrayInputStream response = execute(new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath)); // try to load the document, to throw an exception in case of error ParserHelper.loadDocument(response); @@ -257,7 +256,7 @@ class ApiCall { */ public T post(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException { - HttpPost httpPost = new HttpPost(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath); + HttpPost httpPost = new HttpPost(client.getUrl() + client.getApiEndpoint() + apiPath); // POST a multi-part request, with all attachments if(apiPath.getAttachments().size()>0){ @@ -292,7 +291,7 @@ class ApiCall { */ public T delete(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException { - return execute(new HttpDelete(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath), parser); + return execute(new HttpDelete(client.getUrl() + client.getApiEndpoint() + apiPath), parser); } /** @@ -470,7 +469,7 @@ class ApiCall { DefaultHttpClient httpClient = new DefaultHttpClient(); // configure user-agent - HttpProtocolParams.setUserAgent(httpClient.getParams(), "RunDeck API Java Client " + RundeckClient.API_VERSION); + HttpProtocolParams.setUserAgent(httpClient.getParams(), "RunDeck API Java Client " + client.getApiVersion()); // configure SSL SSLSocketFactory socketFactory = null; diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index f817301..eb5adbf 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -15,16 +15,6 @@ */ package org.rundeck.api; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.TimeUnit; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; @@ -38,6 +28,17 @@ import org.rundeck.api.util.AssertUtil; import org.rundeck.api.util.PagedResults; import org.rundeck.api.util.ParametersUtil; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + /** * Main entry point to talk to a RunDeck instance.
* You have 2 methods for authentication : login-based or token-based. If you want to use the first, you need to provide @@ -71,11 +72,31 @@ public class RundeckClient implements Serializable { private static final long serialVersionUID = 1L; + /** + * Supported version numbers + */ + public static enum Version { + V5(5), + V6(6), + ; + + private int versionNumber; + + Version(final int i) { + versionNumber = i; + } + + public int getVersionNumber() { + return versionNumber; + } + } /** Version of the API supported */ - public static final transient int API_VERSION = 5; + public static final transient int API_VERSION = Version.V6.getVersionNumber(); + + private static final String API = "/api/"; /** End-point of the API */ - public static final transient String API_ENDPOINT = "/api/" + API_VERSION; + public static final transient String API_ENDPOINT = API + API_VERSION; /** Default value for the "pooling interval" used when running jobs/commands/scripts */ private static final transient long DEFAULT_POOLING_INTERVAL = 5; @@ -86,17 +107,48 @@ public class RundeckClient implements Serializable { /** URL of the RunDeck instance ("http://localhost:4440", "http://rundeck.your-compagny.com/", etc) */ private final String url; - /** Auth-token for authentication (if not using login-based auth) */ - private final String token; + private int apiVersion = API_VERSION; - /** Login to use for authentication on the RunDeck instance (if not using token-based auth) */ - private final String login; + private String token; - /** Password to use for authentication on the RunDeck instance (if not using token-based auth) */ - private final String password; + private String login; + + private String password; private String sessionID; + void setToken(String token) { + this.token = token; + } + + void setLogin(String login) { + this.login = login; + } + + void setPassword(String password) { + this.password = password; + } + + void setSessionID(String sessionID) { + this.sessionID = sessionID; + } + + int getApiVersion() { + return apiVersion; + } + + void setApiVersion(int apiVersion) { + this.apiVersion = apiVersion; + } + + void setApiVersion(Version apiVersion) { + this.apiVersion = apiVersion.getVersionNumber(); + } + + String getApiEndpoint() { + return API + getApiVersion(); + } + /** * Instantiate a new {@link RundeckClient} for the RunDeck instance at the given url, using login-based * authentication. @@ -107,11 +159,9 @@ public class RundeckClient implements Serializable { * @throws IllegalArgumentException if the url, login or password is blank (null, empty or whitespace) */ public RundeckClient(String url, String login, String password) throws IllegalArgumentException { - super(); - AssertUtil.notBlank(url, "The RunDeck URL is mandatory !"); + this(url); AssertUtil.notBlank(login, "The RunDeck login is mandatory !"); AssertUtil.notBlank(password, "The RunDeck password is mandatory !"); - this.url = url; this.login = login; this.password = password; this.token = null; @@ -126,10 +176,11 @@ public class RundeckClient implements Serializable { * @param sessionID to use for session authentication on the RunDeck instance * @param useToken should be true if using token, false if using sessionID * @throws IllegalArgumentException if the url or token is blank (null, empty or whitespace) + * @deprecated Use the builder {@link RundeckClientBuilder} */ public RundeckClient(String url, String token, String sessionID, boolean useToken) throws IllegalArgumentException { - super(); - AssertUtil.notBlank(url, "The RunDeck URL is mandatory !"); + this(url); + if(useToken){ AssertUtil.notBlank(token, "Token is mandatory!"); this.token = token; @@ -140,7 +191,6 @@ public class RundeckClient implements Serializable { this.sessionID = sessionID; this.token = null; } - this.url = url; this.login = null; this.password = null; } @@ -150,6 +200,20 @@ public class RundeckClient implements Serializable { this(url, token, null, true); } + /** + * Used by RundeckClientBuilder + */ + RundeckClient(final String url) throws IllegalArgumentException { + AssertUtil.notBlank(url, "The RunDeck URL is mandatory !"); + this.url=url; + } + + /** + * Create a builder for RundeckClient + */ + public static RundeckClientBuilder builder() { + return new RundeckClientBuilder(); + } /** * Try to "ping" the RunDeck instance to see if it is alive diff --git a/src/main/java/org/rundeck/api/RundeckClientBuilder.java b/src/main/java/org/rundeck/api/RundeckClientBuilder.java new file mode 100644 index 0000000..58a1dd6 --- /dev/null +++ b/src/main/java/org/rundeck/api/RundeckClientBuilder.java @@ -0,0 +1,104 @@ +package org.rundeck.api; + +import org.rundeck.api.util.AssertUtil; + + +/** + * Builder to create a {@link RundeckClient}, you must specify a url, and at least one of (login, password), token, or + * sessionId. + */ +public class RundeckClientBuilder { + private String url; + private String login; + private String password; + private String token = null; + private String id = null; + private int version = -1; + + RundeckClientBuilder(){ + + } + /** + * Specify the URL + */ + public RundeckClientBuilder url(String url) { + this.url = url; + return this; + } + + public RundeckClientBuilder login(String login) { + this.login = login; + return this; + } + + public RundeckClientBuilder login(String login, String password) { + this.login = login; + this.password = password; + return this; + } + + public RundeckClientBuilder password(String password) { + this.password = password; + return this; + } + + /** + * Specify a Rundeck API Token string for authentication + */ + public RundeckClientBuilder token(String token) { + this.token = token; + return this; + } + + /** + * Specify a web session ID string for authentication + */ + public RundeckClientBuilder sessionId(String id) { + this.id = id; + return this; + } + + /** + * Specify another version number to use + */ + public RundeckClientBuilder version(final RundeckClient.Version version) { + this.version = version.getVersionNumber(); + return this; + } + + /** + * Specify another version number to use + */ + public RundeckClientBuilder version(final int version) { + this.version = version; + return this; + } + + /** + * Create the RundeckClient instance + */ + public RundeckClient build() { + if (null == url) { + AssertUtil.notBlank(url, "The Rundeck URL is required"); + } + final RundeckClient client = new RundeckClient(url); + if (null != login && null != password) { + AssertUtil.notBlank(login, "login cannot be blank"); + AssertUtil.notBlank(password, "password cannot be blank"); + client.setLogin(login); + client.setPassword(password); + } else if (null != token) { + AssertUtil.notBlank(token, "token cannot be blank"); + client.setToken(token); + } else if (null != id) { + AssertUtil.notBlank(token, "sessionId cannot be blank"); + client.setSessionID(id); + } else { + throw new IllegalStateException("login/password, token, or sessionID must be specified"); + } + if (version > 0) { + client.setApiVersion(version); + } + return client; + } +} \ No newline at end of file diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java index 77ab178..5fc8843 100644 --- a/src/test/java/org/rundeck/api/RundeckClientTest.java +++ b/src/test/java/org/rundeck/api/RundeckClientTest.java @@ -15,12 +15,9 @@ */ package org.rundeck.api; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; - +import betamax.Betamax; import betamax.MatchRule; +import betamax.Recorder; import betamax.TapeMode; import org.junit.Assert; import org.junit.Before; @@ -32,11 +29,14 @@ import org.rundeck.api.domain.RundeckHistory; import org.rundeck.api.domain.RundeckJobDelete; import org.rundeck.api.domain.RundeckJobDeleteBulk; import org.rundeck.api.domain.RundeckProject; -import betamax.Betamax; -import betamax.Recorder; import org.rundeck.api.query.ExecutionQuery; import org.rundeck.api.util.PagedResults; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + /** * Test the {@link RundeckClient}. Uses betamax to unit-test HTTP requests without a live RunDeck instance. @@ -45,6 +45,10 @@ import org.rundeck.api.util.PagedResults; */ public class RundeckClientTest { + public static final String TEST_TOKEN_0 = "PVnN5K3OPc5vduS3uVuVnEsD57pDC5pd"; + public static final String TEST_TOKEN_1 = "0UUNkeRp4d58EDeCs7S6UdODp334DvK9"; + public static final String TEST_TOKEN_2 = "PP4s4SdCRO6KUoNPd1D303Dc304ORN87"; + @Rule public Recorder recorder = new Recorder(); @@ -132,7 +136,7 @@ public class RundeckClientTest { match = {MatchRule.uri, MatchRule.headers, MatchRule.method, MatchRule.path, MatchRule.query}) public void getExecutions() throws Exception { - RundeckClient client = new RundeckClient("http://rundeck.local:4440", "0UUNkeRp4d58EDeCs7S6UdODp334DvK9"); + RundeckClient client = createClient(TEST_TOKEN_1); final String projectName = "blah"; @@ -259,7 +263,7 @@ public class RundeckClientTest { @Test @Betamax(tape = "get_executions_paging") public void getExecutionsPaging() throws Exception{ - RundeckClient client = new RundeckClient("http://rundeck.local:4440", "0UUNkeRp4d58EDeCs7S6UdODp334DvK9"); + RundeckClient client = createClient(TEST_TOKEN_1); final String projectName = "blah"; //2 max, 1 offset final PagedResults adhocTest = client.getExecutions(ExecutionQuery.builder() @@ -290,7 +294,7 @@ public class RundeckClientTest { @Test @Betamax(tape = "bulk_delete") public void bulkDelete() throws Exception { - RundeckClient client = new RundeckClient("http://rundeck.local:4440", "PP4s4SdCRO6KUoNPd1D303Dc304ORN87"); + RundeckClient client = createClient(TEST_TOKEN_2); final RundeckJobDeleteBulk deleteTest = client.deleteJobs(Arrays.asList("0ce457b5-ba84-41ca-812e-02b31da355a4")); @@ -308,7 +312,7 @@ public class RundeckClientTest { @Test @Betamax(tape = "bulk_delete_dne") public void bulkDeleteFailDNE() throws Exception { - RundeckClient client = new RundeckClient("http://rundeck.local:4440", "PP4s4SdCRO6KUoNPd1D303Dc304ORN87"); + RundeckClient client = createClient(TEST_TOKEN_2); final RundeckJobDeleteBulk deleteTest = client.deleteJobs(Arrays.asList("does-not-exist")); @@ -326,7 +330,7 @@ public class RundeckClientTest { @Test @Betamax(tape = "bulk_delete_unauthorized") public void bulkDeleteFailUnauthorized() throws Exception { - RundeckClient client = new RundeckClient("http://rundeck.local:4440", "PP4s4SdCRO6KUoNPd1D303Dc304ORN87"); + RundeckClient client = createClient(TEST_TOKEN_2); final RundeckJobDeleteBulk deleteTest = client.deleteJobs(Arrays.asList("3a6d16be-4268-4d26-86a9-cebc1781f768")); @@ -356,7 +360,14 @@ public class RundeckClientTest { public void setUp() throws Exception { // not that you can put whatever here, because we don't actually connect to the RunDeck instance // but instead use betamax as a proxy to serve the previously recorded tapes (in src/test/resources) - client = new RundeckClient("http://rundeck.local:4440", "PVnN5K3OPc5vduS3uVuVnEsD57pDC5pd"); + client = createClient(TEST_TOKEN_0); + } + + private RundeckClient createClient(final String token) { + return RundeckClient.builder().url("http://rundeck.local:4440") + .token(token) + .version(5) + .build(); } }