diff --git a/docs/tests-db.md b/docs/tests-db.md index c6e594055d..003ff3fd7e 100644 --- a/docs/tests-db.md +++ b/docs/tests-db.md @@ -56,13 +56,10 @@ Stop MySQl: Using built-in profiles to run database tests using docker containers ------- -The project provides specific profiles to run database tests using containers. The supported databases and their respective profiles are: +The project provides specific profiles to run database tests using containers. Below is a just a sample of implemented profiles. In order to get a full list, please invoke (`mvn help:all-profiles -pl testsuite/integration-arquillian | grep -- db- | grep -v allocator`): * `db-mysql` * `db-postgres` -* `db-mariadb` -* `db-mssql2017` -* `db-oracle11g` As an example, to run tests using a MySQL docker container on Undertow auth-server: @@ -94,3 +91,31 @@ name or tag for the image. Note that Docker containers may occupy some space even after termination, and especially with databases that might be easily a gigabyte. It is thus advisable to run `docker system prune` occasionally to reclaim that space. + + +Using DB Allocator Service +------- + +The testsuite can use the DB Allocator Service to allocate and release desired database automatically. +Since some of the database properties (such as JDBC URL, Username or Password) need to be used when building the Auth Server, +the allocation and deallocation need to happen when building the `integration-arquillian` project (instead of `tests/base` as +it happens in other cases). + +In order to use the DB Allocator Service, you must use the `jpa` profile with one of the `db-allocator-*`. Here's a full example to +run JPA with Auth Server Wildfly and MSSQL 2016: + +``` +mvn -f testsuite/integration-arquillian/pom.xml \ + -Pjpa,auth-server-wildfly,db-allocator-db-mssql2016 \ + -Ddballocator.uri=<> \ + -Ddballocator.user=<> \ + -Dmaven.test.failure.ignore=true +``` + +Using `-Dmaven.test.failure.ignore=true` is not strictly required but highly recommended. After running the tests, +the DB Allocator Plugin should release the allocated database. + +Below is a just a sample of implemented profiles. In order to get a full list, please invoke (`mvn help:all-profiles -pl testsuite/integration-arquillian | grep -- db-allocator-db-`): + +* `db-allocator-db-postgres` - dor testing with Postgres 9.6.x +* `db-allocator-db-mysql` - dor testing with MySQL 5.7 \ No newline at end of file diff --git a/testsuite/db-allocator-plugin/pom.xml b/testsuite/db-allocator-plugin/pom.xml new file mode 100644 index 0000000000..74032cd091 --- /dev/null +++ b/testsuite/db-allocator-plugin/pom.xml @@ -0,0 +1,76 @@ + + + + + + keycloak-testsuite-pom + org.keycloak + 6.0.0-SNAPSHOT + + 4.0.0 + + db-allocator-plugin + maven-plugin + DB Allocator Plugin + + + 3.6.0 + + + + + org.apache.maven + maven-plugin-api + ${maven.version} + + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${maven.version} + provided + + + org.apache.maven + maven-core + 3.6.0 + provided + + + org.jboss.resteasy + resteasy-client + + + + junit + junit + test + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + ${maven.version} + + + + diff --git a/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/AllocateDBMojo.java b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/AllocateDBMojo.java new file mode 100644 index 0000000000..9a72b34712 --- /dev/null +++ b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/AllocateDBMojo.java @@ -0,0 +1,176 @@ +package org.keycloak.testsuite.dballocator; + +import org.apache.commons.lang3.StringUtils; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.keycloak.testsuite.dballocator.client.data.AllocationResult; +import org.keycloak.testsuite.dballocator.client.DBAllocatorServiceClient; +import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorException; +import org.keycloak.testsuite.dballocator.client.retry.IncrementalBackoffRetryPolicy; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Allocated a DB from DB Allocator Service. + */ +@Mojo(name = "allocate", defaultPhase = LifecyclePhase.PROCESS_TEST_RESOURCES) +public class AllocateDBMojo extends AbstractMojo { + + + private final Log logger = getLog(); + + @Parameter(defaultValue = "${project}", required = true, readonly = true) + protected MavenProject project; + + @Parameter(defaultValue = "${reactorProjects}", readonly = true) + List reactorProjects; + + /** + * Enables printing out a summary after execution. + */ + @Parameter(property = Constants.PROPERTY_PRINT_SUMMARY, defaultValue = "true") + private boolean printSummary; + + /** + * Skips the execution of this Mojo. + */ + @Parameter(property = Constants.PROPERTY_SKIP, defaultValue = "false") + private boolean skip; + + /** + * The number of retries for reaching the DB Allocator Service + */ + @Parameter(property = Constants.PROPERTY_RETRY_TOTAL_RETRIES, defaultValue = "3") + private int totalRetries; + + /** + * Backoff time for reaching out the DB Allocator Service. + */ + @Parameter(property = Constants.PROPERTY_RETRY_BACKOFF_SECONDS, defaultValue = "10") + private int backoffTimeSeconds; + + /** + * URI to the DB Allocator Service. + */ + @Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_URI) + private String dbAllocatorURI; + + /** + * Username used for allocating DBs. + */ + @Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_USER) + private String user; + + /** + * Fallback username used for allocating DBs. + */ + @Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_USER_FALLBACK) + private String fallbackUser; + + /** + * Type of the database to be used. + */ + @Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_DATABASE_TYPE) + private String type; + + /** + * Expiration in minutes. + */ + @Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_EXPIRATION_MIN, defaultValue = "1440") + private int expirationInMinutes; + + /** + * Preferred DB location. + */ + @Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_LOCATION, defaultValue = "geo_RDU") + private String location; + + /** + * A property set as an output of this Mojo for JDBC Driver. + */ + @Parameter(property = Constants.PROPERTY_TO_BE_SET_DRIVER, defaultValue = "keycloak.connectionsJpa.driver") + private String propertyDriver; + + /** + * A property set as an output of this Mojo for Database Schema. + */ + @Parameter(property = Constants.PROPERTY_TO_BE_SET_DATABASE, defaultValue = "keycloak.connectionsJpa.database") + private String propertyDatabase; + + /** + * A property set as an output of this Mojo for DB Username. + */ + @Parameter(property = Constants.PROPERTY_TO_BE_SET_USER, defaultValue = "keycloak.connectionsJpa.user") + private String propertyUser; + + /** + * A property set as an output of this Mojo for DB Password. + */ + @Parameter(property = Constants.PROPERTY_TO_BE_SET_PASSWORD, defaultValue = "keycloak.connectionsJpa.password") + private String propertyPassword; + + /** + * A property set as an output of this Mojo for JDBC Connection URI. + */ + @Parameter(property = Constants.PROPERTY_TO_BE_SET_JDBC_URL, defaultValue = "keycloak.connectionsJpa.url") + private String propertyURL; + + @Override + public void execute() throws MojoFailureException { + if (skip) { + logger.info("Skipping"); + return; + } + + try { + IncrementalBackoffRetryPolicy retryPolicy = new IncrementalBackoffRetryPolicy(totalRetries, backoffTimeSeconds, TimeUnit.SECONDS); + DBAllocatorServiceClient client = new DBAllocatorServiceClient(dbAllocatorURI, retryPolicy); + + setFallbackUserIfNecessary(); + AllocationResult allocate = client.allocate(user, type, expirationInMinutes, TimeUnit.MINUTES, location); + + reactorProjects.forEach((project) -> setPropertiesToProject(project, allocate)); + + if (printSummary) { + logger.info("Allocated database:"); + logger.info("-- UUID: " + allocate.getUUID()); + logger.info("-- Driver: " + allocate.getDriver()); + logger.info("-- Database: " + allocate.getDatabase()); + logger.info("-- User: " + allocate.getUser()); + logger.info("-- Password: " + allocate.getPassword()); + logger.info("-- URL: " + allocate.getURL()); + } + + } catch (DBAllocatorException e) { + String error = e.getMessage(); + if (e.getErrorResponse() != null) { + error = String.format("[%s](%s)", e.getErrorResponse().getStatus(), e.getErrorResponse().readEntity(String.class)); + } + throw new MojoFailureException("An error occurred while communicating with DBAllocator (" + error + ")", e); + } + } + + private void setFallbackUserIfNecessary() { + if (StringUtils.isBlank(user)) { + if (StringUtils.isBlank(fallbackUser)) { + throw new IllegalArgumentException("Both " + Constants.PROPERTY_DB_ALLOCATOR_USER + " and " + Constants.PROPERTY_DB_ALLOCATOR_USER_FALLBACK + " are empty"); + } + user = fallbackUser; + } + } + + private void setPropertiesToProject(MavenProject project, AllocationResult allocate) { + project.getProperties().setProperty(propertyDriver, allocate.getDriver()); + project.getProperties().setProperty(propertyDatabase, allocate.getDatabase()); + project.getProperties().setProperty(propertyUser, allocate.getUser()); + project.getProperties().setProperty(propertyPassword, allocate.getPassword()); + project.getProperties().setProperty(propertyURL, allocate.getURL()); + project.getProperties().setProperty(Constants.PROPERTY_ALLOCATED_DB, allocate.getUUID()); + } +} diff --git a/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/Constants.java b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/Constants.java new file mode 100644 index 0000000000..1ca67bc006 --- /dev/null +++ b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/Constants.java @@ -0,0 +1,20 @@ +package org.keycloak.testsuite.dballocator; + +public interface Constants { + String PROPERTY_ALLOCATED_DB = "dballocator.allocated.uuid"; + String PROPERTY_DB_ALLOCATOR_URI = "dballocator.uri"; + String PROPERTY_DB_ALLOCATOR_USER = "dballocator.user"; + String PROPERTY_DB_ALLOCATOR_USER_FALLBACK = "user.name"; + String PROPERTY_DB_ALLOCATOR_DATABASE_TYPE = "dballocator.type"; + String PROPERTY_DB_ALLOCATOR_EXPIRATION_MIN = "dballocator.expirationMin"; + String PROPERTY_DB_ALLOCATOR_LOCATION = "dballocator.location"; + String PROPERTY_TO_BE_SET_DRIVER = "dballocator.properties.driver"; + String PROPERTY_TO_BE_SET_DATABASE = "dballocator.properties.database"; + String PROPERTY_TO_BE_SET_USER = "dballocator.properties.user"; + String PROPERTY_TO_BE_SET_PASSWORD = "dballocator.properties.password"; + String PROPERTY_TO_BE_SET_JDBC_URL = "dballocator.properties.url"; + String PROPERTY_PRINT_SUMMARY = "dballocator.summary"; + String PROPERTY_SKIP = "dballocator.skip"; + String PROPERTY_RETRY_TOTAL_RETRIES = "dballocator.retry.totalRetries"; + String PROPERTY_RETRY_BACKOFF_SECONDS = "dballocator.retry.backoffSeconds"; +} diff --git a/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/ReleaseDBMojo.java b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/ReleaseDBMojo.java new file mode 100644 index 0000000000..8332e63992 --- /dev/null +++ b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/ReleaseDBMojo.java @@ -0,0 +1,92 @@ +package org.keycloak.testsuite.dballocator; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.keycloak.testsuite.dballocator.client.data.AllocationResult; +import org.keycloak.testsuite.dballocator.client.DBAllocatorServiceClient; +import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorException; +import org.keycloak.testsuite.dballocator.client.retry.IncrementalBackoffRetryPolicy; +import org.keycloak.testsuite.dballocator.client.data.ReleaseResult; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +/** + * Releases a DB from DB Allocator Service. + */ +@Mojo(name = "release", defaultPhase = LifecyclePhase.TEST) +public class ReleaseDBMojo extends AbstractMojo { + + private final Log logger = getLog(); + + @Parameter(defaultValue = "${project}", required = true) + protected MavenProject project; + + /** + * Enables printing out a summary after execution. + */ + @Parameter(property = Constants.PROPERTY_PRINT_SUMMARY, defaultValue = "true") + private boolean printSummary; + + /** + * Skips the execution of this Mojo. + */ + @Parameter(property = Constants.PROPERTY_SKIP, defaultValue = "false") + private boolean skip; + + /** + * The number of retries for reaching the DB Allocator Service + */ + @Parameter(property = Constants.PROPERTY_RETRY_TOTAL_RETRIES, defaultValue = "3") + private int totalRetries; + + /** + * Backoff time for reaching out the DB Allocator Service. + */ + @Parameter(property = Constants.PROPERTY_RETRY_BACKOFF_SECONDS, defaultValue = "10") + private int backoffTimeSeconds; + + /** + * URI to the DB Allocator Service. + */ + @Parameter(property = Constants.PROPERTY_DB_ALLOCATOR_URI) + private String dbAllocatorURI; + + /** + * UUID for releasing the allocated DB. + */ + @Parameter(property = Constants.PROPERTY_ALLOCATED_DB) + private String allocatedUUID; + + @Override + public void execute() throws MojoFailureException { + if (skip) { + logger.info("Skipping"); + return; + } + + try { + IncrementalBackoffRetryPolicy retryPolicy = new IncrementalBackoffRetryPolicy(totalRetries, backoffTimeSeconds, TimeUnit.SECONDS); + DBAllocatorServiceClient client = new DBAllocatorServiceClient(dbAllocatorURI, retryPolicy); + + ReleaseResult release = client.release(AllocationResult.forRelease(allocatedUUID)); + + if (printSummary) { + logger.info("Released database:"); + logger.info("-- UUID: " + release.getUUID()); + } + + } catch (DBAllocatorException e) { + String error = e.getMessage(); + if (e.getErrorResponse() != null) { + error = String.format("[%s](%s)", e.getErrorResponse().getStatus(), e.getErrorResponse().readEntity(String.class)); + } + throw new MojoFailureException("An error occurred while communicating with DBAllocator (" + error + ")", e); + } + } +} diff --git a/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/BackoffRetryPolicy.java b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/BackoffRetryPolicy.java new file mode 100644 index 0000000000..69f124d211 --- /dev/null +++ b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/BackoffRetryPolicy.java @@ -0,0 +1,11 @@ +package org.keycloak.testsuite.dballocator.client; + +import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorUnavailableException; + +import javax.ws.rs.core.Response; +import java.util.concurrent.Callable; + +@FunctionalInterface +public interface BackoffRetryPolicy { + Response retryTillHttpOk(Callable callableSupplier) throws DBAllocatorUnavailableException; +} diff --git a/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/DBAllocatorServiceClient.java b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/DBAllocatorServiceClient.java new file mode 100644 index 0000000000..fd5d0ed622 --- /dev/null +++ b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/DBAllocatorServiceClient.java @@ -0,0 +1,100 @@ +package org.keycloak.testsuite.dballocator.client; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine; +import org.keycloak.testsuite.dballocator.client.data.AllocationResult; +import org.keycloak.testsuite.dballocator.client.data.ReleaseResult; +import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorException; +import org.keycloak.testsuite.dballocator.client.retry.IncrementalBackoffRetryPolicy; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Response; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +public class DBAllocatorServiceClient { + + private static final int TIMEOUT = 10_000; + + private final Client restClient; + private final URI allocatorServletURI; + private final BackoffRetryPolicy retryPolicy; + + public DBAllocatorServiceClient(String allocatorServletURI, BackoffRetryPolicy retryPolicy) { + Objects.requireNonNull(allocatorServletURI, "DB Allocator URI must not be null"); + + this.allocatorServletURI = URI.create(allocatorServletURI); + this.retryPolicy = retryPolicy != null ? retryPolicy : new IncrementalBackoffRetryPolicy(); + this.restClient = new ResteasyClientBuilder().httpEngine(createEngine()).build(); + } + + private final ApacheHttpClient43Engine createEngine() { + RequestConfig reqConfig = RequestConfig.custom() + .setConnectTimeout(TIMEOUT) + .setSocketTimeout(TIMEOUT) + .setConnectionRequestTimeout(TIMEOUT) + .build(); + CloseableHttpClient httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(reqConfig) + .setMaxConnTotal(1) + .build(); + + ApacheHttpClient43Engine engine = new ApacheHttpClient43Engine(httpClient); + engine.setFollowRedirects(true); + return engine; + } + + public AllocationResult allocate(String user, String type, int expiration, TimeUnit expirationTimeUnit, String location) throws DBAllocatorException { + Objects.requireNonNull(user, "User can not be null"); + Objects.requireNonNull(type, "DB Type must not be null"); + + try { + String typeWithLocation = location != null ? type + "&&" + location : type; + Invocation.Builder target = restClient + .target(allocatorServletURI) + .queryParam("operation", "allocate") + .queryParam("requestee", user) + .queryParam("expression", typeWithLocation) + .queryParam("expiry", expirationTimeUnit.toMinutes(expiration)) + .request(); + + Response response = retryPolicy.retryTillHttpOk(() -> target.get()); + Properties properties = new Properties(); + String content = response.readEntity(String.class); + + if (content != null) { + try(InputStream is = new ByteArrayInputStream(content.getBytes())) { + properties.load(is); + } + } + + return AllocationResult.successful(properties); + } catch (IOException e) { + throw new DBAllocatorException(e); + } + } + + public ReleaseResult release(AllocationResult allocationResult) throws DBAllocatorException { + Objects.requireNonNull(allocationResult, "Previous allocation result must not be null"); + Objects.requireNonNull(allocationResult.getUUID(), "UUID must not be null"); + + Invocation.Builder target = restClient + .target(allocatorServletURI) + .queryParam("operation", "dealloc") + .queryParam("uuid", allocationResult.getUUID()) + .request(); + + retryPolicy.retryTillHttpOk(() -> target.get()); + + return ReleaseResult.successful(allocationResult.getUUID()); + } +} diff --git a/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/data/AllocationResult.java b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/data/AllocationResult.java new file mode 100644 index 0000000000..5d69fd9fb0 --- /dev/null +++ b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/data/AllocationResult.java @@ -0,0 +1,81 @@ +package org.keycloak.testsuite.dballocator.client.data; + +import java.util.Properties; + +public class AllocationResult { + + private final String uuid; + private final String driver; + private final String database; + private final String user; + private final String password; + private final String url; + + private AllocationResult(String uuid) { + this.uuid = uuid; + this.driver = null; + this.database = null; + this.user = null; + this.password = null; + this.url = null; + } + + private AllocationResult(String uuid, String driver, String database, String user, String password, String url) { + this.uuid = uuid; + this.driver = driver; + this.database = database; + this.user = user; + this.password = password; + this.url = url; + } + + public static AllocationResult forRelease(String uuid) { + return new AllocationResult(uuid); + } + + public static AllocationResult successful(Properties properties) { + return new AllocationResult( + properties.getProperty("uuid"), + properties.getProperty("db.jdbc_class"), + properties.getProperty("db.name"), + properties.getProperty("db.username"), + properties.getProperty("db.password"), + properties.getProperty("db.jdbc_url")); + } + + public String getDriver() { + return driver; + } + + public String getDatabase() { + return database; + } + + public String getUser() { + return user; + } + + public String getPassword() { + return password; + } + + public String getURL() { + return url; + } + + public String getUUID() { + return uuid; + } + + @Override + public String toString() { + return "AllocationResult{" + + "uuid='" + uuid + '\'' + + ", driver='" + driver + '\'' + + ", database='" + database + '\'' + + ", user='" + user + '\'' + + ", password='" + password + '\'' + + ", url='" + url + '\'' + + '}'; + } +} diff --git a/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/data/ReleaseResult.java b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/data/ReleaseResult.java new file mode 100644 index 0000000000..5fcce9e178 --- /dev/null +++ b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/data/ReleaseResult.java @@ -0,0 +1,25 @@ +package org.keycloak.testsuite.dballocator.client.data; + +public class ReleaseResult { + + private final String uuid; + + private ReleaseResult(String uuid) { + this.uuid = uuid; + } + + public static ReleaseResult successful(String uuid) { + return new ReleaseResult(uuid); + } + + public String getUUID() { + return uuid; + } + + @Override + public String toString() { + return "ReleaseResult{" + + "uuid='" + uuid + '\'' + + '}'; + } +} diff --git a/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/exceptions/DBAllocatorException.java b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/exceptions/DBAllocatorException.java new file mode 100644 index 0000000000..0443f69f4e --- /dev/null +++ b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/exceptions/DBAllocatorException.java @@ -0,0 +1,25 @@ +package org.keycloak.testsuite.dballocator.client.exceptions; + +import javax.ws.rs.core.Response; + +public class DBAllocatorException extends Exception { + + private Response errorResponse; + + public DBAllocatorException(Response errorResponse) { + this.errorResponse = errorResponse; + } + + public DBAllocatorException(Response errorResponse, Throwable throwable) { + super(throwable); + this.errorResponse = errorResponse; + } + + public DBAllocatorException(Throwable throwable) { + super(throwable); + } + + public Response getErrorResponse() { + return errorResponse; + } +} diff --git a/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/exceptions/DBAllocatorUnavailableException.java b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/exceptions/DBAllocatorUnavailableException.java new file mode 100644 index 0000000000..cafa5917ff --- /dev/null +++ b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/exceptions/DBAllocatorUnavailableException.java @@ -0,0 +1,11 @@ +package org.keycloak.testsuite.dballocator.client.exceptions; + + +import javax.ws.rs.core.Response; + +public class DBAllocatorUnavailableException extends DBAllocatorException { + + public DBAllocatorUnavailableException(Response errorResponse) { + super(errorResponse); + } +} diff --git a/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/retry/IncrementalBackoffRetryPolicy.java b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/retry/IncrementalBackoffRetryPolicy.java new file mode 100644 index 0000000000..8b236e4905 --- /dev/null +++ b/testsuite/db-allocator-plugin/src/main/java/org/keycloak/testsuite/dballocator/client/retry/IncrementalBackoffRetryPolicy.java @@ -0,0 +1,57 @@ +package org.keycloak.testsuite.dballocator.client.retry; + +import org.keycloak.testsuite.dballocator.client.BackoffRetryPolicy; +import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorUnavailableException; + +import javax.ws.rs.core.Response; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +public class IncrementalBackoffRetryPolicy implements BackoffRetryPolicy { + + public static final int DEFAULT_TOTAL_RETRIES = 3; + public static final int DEFAULT_BACKOFF_TIME = 10; + public static final TimeUnit DEFAULT_BACKOFF_TIME_UNIT = TimeUnit.SECONDS; + + private final int totalRetries; + private final int backoffTime; + private final TimeUnit backoffTimeUnit; + + public IncrementalBackoffRetryPolicy() { + this(DEFAULT_TOTAL_RETRIES, DEFAULT_BACKOFF_TIME, DEFAULT_BACKOFF_TIME_UNIT); + } + + public IncrementalBackoffRetryPolicy(int totalRetries, int backoffTime, TimeUnit backoffTimeUnit) { + this.backoffTime = backoffTime; + this.backoffTimeUnit = backoffTimeUnit; + this.totalRetries = totalRetries; + } + + @Override + public Response retryTillHttpOk(Callable callableSupplier) throws DBAllocatorUnavailableException { + return retryTillHttpOk(callableSupplier, totalRetries, backoffTime, backoffTimeUnit); + } + + public Response retryTillHttpOk(Callable callableSupplier, int totalRetries, int backoffTime, TimeUnit backoffTimeUnit) throws DBAllocatorUnavailableException { + int retryCount = 0; + Response response = null; + while(true) { + try { + response = callableSupplier.call(); + } catch (Exception e) { + response = null; + } + + if (response != null && response.getStatus() == 200) { + return response; + } + + if (++retryCount > totalRetries) { + throw new DBAllocatorUnavailableException(response); + } + + LockSupport.parkNanos(backoffTimeUnit.toNanos(backoffTime * retryCount)); + } + } +} diff --git a/testsuite/db-allocator-plugin/src/test/java/org/keycloak/testsuite/dballocator/client/DBAllocatorServiceClientTest.java b/testsuite/db-allocator-plugin/src/test/java/org/keycloak/testsuite/dballocator/client/DBAllocatorServiceClientTest.java new file mode 100644 index 0000000000..a6ede16b6c --- /dev/null +++ b/testsuite/db-allocator-plugin/src/test/java/org/keycloak/testsuite/dballocator/client/DBAllocatorServiceClientTest.java @@ -0,0 +1,67 @@ +package org.keycloak.testsuite.dballocator.client; + +import org.apache.commons.io.IOUtils; +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.testsuite.dballocator.client.data.AllocationResult; +import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorException; +import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorUnavailableException; +import org.keycloak.testsuite.dballocator.client.mock.MockResponse; + +import javax.ws.rs.core.Response; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.concurrent.TimeUnit; + + +public class DBAllocatorServiceClientTest { + + @Test + public void testSuccessfulAllocation() throws Exception { + //given + String mockURI = "http://localhost:8080/test"; + + String testProperties = null; + try(InputStream is = DBAllocatorServiceClientTest.class.getResourceAsStream("/db-allocator-response.properties")) { + testProperties = IOUtils.toString(is, Charset.defaultCharset()); + } + + Response successfulResponse = new MockResponse(200, testProperties); + BackoffRetryPolicy retryPolicyMock = callableSupplier -> successfulResponse; + + DBAllocatorServiceClient client = new DBAllocatorServiceClient(mockURI, retryPolicyMock); + + //when + AllocationResult allocationResult = client.allocate("user", "mariadb_galera_101", 1440, TimeUnit.SECONDS, "geo_RDU"); + + //then + Assert.assertEquals("d328bb0e-3dcc-42da-8ce1-83738a8dfede", allocationResult.getUUID()); + Assert.assertEquals("org.mariadb.jdbc.Driver", allocationResult.getDriver()); + Assert.assertEquals("dbname", allocationResult.getDatabase()); + Assert.assertEquals("username", allocationResult.getUser()); + Assert.assertEquals("password", allocationResult.getPassword()); + Assert.assertEquals("jdbc:mariadb://mariadb-101-galera.keycloak.org:3306", allocationResult.getURL()); + } + + @Test + public void testFailureAllocation() throws Exception { + //given + String mockURI = "http://localhost:8080/test"; + + Response serverErrorResponse = new MockResponse(500, null); + BackoffRetryPolicy retryPolicyMock = callableSupplier -> { + throw new DBAllocatorUnavailableException(serverErrorResponse); + }; + + DBAllocatorServiceClient client = new DBAllocatorServiceClient(mockURI, retryPolicyMock); + + //when + try { + client.allocate("user", "mariadb_galera_101", 1440, TimeUnit.SECONDS, "geo_RDU"); + Assert.fail(); + } catch (DBAllocatorException e) { + Assert.assertEquals(500, e.getErrorResponse().getStatus()); + } + } +} \ No newline at end of file diff --git a/testsuite/db-allocator-plugin/src/test/java/org/keycloak/testsuite/dballocator/client/mock/MockResponse.java b/testsuite/db-allocator-plugin/src/test/java/org/keycloak/testsuite/dballocator/client/mock/MockResponse.java new file mode 100644 index 0000000000..dd16c328cd --- /dev/null +++ b/testsuite/db-allocator-plugin/src/test/java/org/keycloak/testsuite/dballocator/client/mock/MockResponse.java @@ -0,0 +1,159 @@ +package org.keycloak.testsuite.dballocator.client.mock; + +import javax.ws.rs.core.EntityTag; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Link; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; +import java.lang.annotation.Annotation; +import java.net.URI; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +public class MockResponse extends Response { + + private final int status; + private final String entity; + + public MockResponse(int status, String entity) { + this.status = status; + this.entity = entity; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public StatusType getStatusInfo() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getEntity() { + return entity; + } + + @Override + public T readEntity(Class aClass) { + if (aClass.isAssignableFrom(String.class)) { + return (T) entity; + } + throw new UnsupportedOperationException(); + } + + @Override + public T readEntity(GenericType genericType) { + throw new UnsupportedOperationException(); + } + + @Override + public T readEntity(Class aClass, Annotation[] annotations) { + throw new UnsupportedOperationException(); + } + + @Override + public T readEntity(GenericType genericType, Annotation[] annotations) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasEntity() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean bufferEntity() { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + throw new UnsupportedOperationException(); + } + + @Override + public MediaType getMediaType() { + throw new UnsupportedOperationException(); + } + + @Override + public Locale getLanguage() { + throw new UnsupportedOperationException(); + } + + @Override + public int getLength() { + throw new UnsupportedOperationException(); + } + + @Override + public Set getAllowedMethods() { + throw new UnsupportedOperationException(); + } + + @Override + public Map getCookies() { + throw new UnsupportedOperationException(); + } + + @Override + public EntityTag getEntityTag() { + throw new UnsupportedOperationException(); + } + + @Override + public Date getDate() { + throw new UnsupportedOperationException(); + } + + @Override + public Date getLastModified() { + throw new UnsupportedOperationException(); + } + + @Override + public URI getLocation() { + throw new UnsupportedOperationException(); + } + + @Override + public Set getLinks() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasLink(String s) { + throw new UnsupportedOperationException(); + } + + @Override + public Link getLink(String s) { + throw new UnsupportedOperationException(); + } + + @Override + public Link.Builder getLinkBuilder(String s) { + throw new UnsupportedOperationException(); + } + + @Override + public MultivaluedMap getMetadata() { + throw new UnsupportedOperationException(); + } + + @Override + public MultivaluedMap getStringHeaders() { + throw new UnsupportedOperationException(); + } + + @Override + public String getHeaderString(String s) { + throw new UnsupportedOperationException(); + } +} diff --git a/testsuite/db-allocator-plugin/src/test/java/org/keycloak/testsuite/dballocator/client/retry/IncrementalBackoffRetryPolicyTest.java b/testsuite/db-allocator-plugin/src/test/java/org/keycloak/testsuite/dballocator/client/retry/IncrementalBackoffRetryPolicyTest.java new file mode 100644 index 0000000000..c936170340 --- /dev/null +++ b/testsuite/db-allocator-plugin/src/test/java/org/keycloak/testsuite/dballocator/client/retry/IncrementalBackoffRetryPolicyTest.java @@ -0,0 +1,68 @@ +package org.keycloak.testsuite.dballocator.client.retry; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.testsuite.dballocator.client.exceptions.DBAllocatorUnavailableException; +import org.keycloak.testsuite.dballocator.client.retry.IncrementalBackoffRetryPolicy; + +import javax.ws.rs.core.Response; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.LongAdder; + + +public class IncrementalBackoffRetryPolicyTest { + + static class BackoffCounter implements Callable { + + LongAdder adder = new LongAdder(); + Response responseToReport; + + public BackoffCounter(Response responseToReport) { + this.responseToReport = responseToReport; + } + + @Override + public Response call() throws Exception { + adder.add(1); + return responseToReport; + } + + public Long getCounter() { + return adder.longValue(); + } + } + + @Test + public void testBackoffLoop() { + //given + long expectedNumberOfRetries = 2; + long expectedNumberOfInvocations = expectedNumberOfRetries + 1; + BackoffCounter counter = new BackoffCounter(Response.serverError().build()); + IncrementalBackoffRetryPolicy backoffRetryPolicy = new IncrementalBackoffRetryPolicy((int) expectedNumberOfRetries, 0, TimeUnit.NANOSECONDS); + + //when + try { + backoffRetryPolicy.retryTillHttpOk(counter); + Assert.fail(); + } catch (DBAllocatorUnavailableException e) { + //then + Assert.assertEquals(expectedNumberOfInvocations, counter.getCounter().longValue()); + } + } + + @Test + public void testIgnoringBackoffWhenGettingSuccessfulResponse() throws Exception { + //given + BackoffCounter counter = new BackoffCounter(Response.ok().build()); + IncrementalBackoffRetryPolicy backoffRetryPolicy = new IncrementalBackoffRetryPolicy(3, 0, TimeUnit.NANOSECONDS); + + //when + Response response = backoffRetryPolicy.retryTillHttpOk(counter); + + //then + Assert.assertEquals(1, counter.getCounter().longValue()); + Assert.assertEquals(200, response.getStatus()); + } + +} \ No newline at end of file diff --git a/testsuite/db-allocator-plugin/src/test/resources/db-allocator-response.properties b/testsuite/db-allocator-plugin/src/test/resources/db-allocator-response.properties new file mode 100644 index 0000000000..036c1d0e2d --- /dev/null +++ b/testsuite/db-allocator-plugin/src/test/resources/db-allocator-response.properties @@ -0,0 +1,25 @@ +#Generated by DBAllocator +#Mon Mar 18 12:49:24 UTC 2019 +db.password=password +hibernate.connection.password=password +hibernate41.dialect=org.hibernate.dialect.MySQL5InnoDBDialect +broken=false +db.username=username +server_geo=RDU +dballoc.db_type=clustered +db.name=dbname +db.jdbc_url=jdbc\:mariadb\://mariadb-101-galera.keycloak.org\:3306 +datasource.class.xa=org.mariadb.jdbc.MySQLDataSource +server_uid=RDU_mariadb_galera_101 +hibernate33.dialect=org.hibernate.dialect.MySQL5InnoDBDialect +hibernate.connection.username=username +server_labels=mariadb_galera_101 +db.jdbc_class=org.mariadb.jdbc.Driver +db.schema=dballo01 +hibernate.connection.driver_class=org.mariadb.jdbc.Driver +uuid=d328bb0e-3dcc-42da-8ce1-83738a8dfede +db.primary_label=mariadb_galera_101 +server_label_primary=mariadb_galera_101 +hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect +hibernate.connection.url=jdbc\:mariadb\://mariadb-101-galera-01.keycloak.org\:3306 +hibernate.connection.schema=dballo01 diff --git a/testsuite/db-allocator-plugin/src/test/resources/log4j.properties b/testsuite/db-allocator-plugin/src/test/resources/log4j.properties new file mode 100755 index 0000000000..0032431e6f --- /dev/null +++ b/testsuite/db-allocator-plugin/src/test/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=info, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p %t [%c] %m%n \ No newline at end of file diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md index 27d923b216..e5713f39f7 100644 --- a/testsuite/integration-arquillian/HOW-TO-RUN.md +++ b/testsuite/integration-arquillian/HOW-TO-RUN.md @@ -717,6 +717,7 @@ The exact steps to configure Docker depend on the operating system. By default, the test will run against Undertow based embedded Keycloak Server, thus no distribution build is required beforehand. The exact command line arguments depend on the operating system. + ### General guidelines If docker daemon doesn't run locally, or if you're not running on Linux, you may need diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml index 1e64db8a03..69d2618ea5 100644 --- a/testsuite/integration-arquillian/pom.xml +++ b/testsuite/integration-arquillian/pom.xml @@ -94,6 +94,8 @@ jdbc:h2:mem:test;MVCC=TRUE;DB_CLOSE_DELAY=-1 DEFAULT + + true @@ -244,6 +246,18 @@ + + org.keycloak + db-allocator-plugin + ${project.version} + + keycloak.connectionsJpa.database + keycloak.connectionsJpa.driver + keycloak.connectionsJpa.url + keycloak.connectionsJpa.user + keycloak.connectionsJpa.password + + maven-surefire-plugin 2.19.1 @@ -272,6 +286,27 @@ + + + org.keycloak + db-allocator-plugin + false + + + allocate-db + + allocate + + + + release-db + + release + + + + + @@ -379,6 +414,16 @@ (?si)Ready for start up.*ready [^\n]{0,30}connections + + db-allocator-db-mysql + + mysql + mysql-connector-java + ${mysql.version} + mysql57 + false + + db-postgres @@ -396,6 +441,16 @@ (?si)Ready for start up.*ready [^\n]{0,30}connections + + db-allocator-db-postgres + + org.postgresql + postgresql + ${postgresql.version} + postgresql96 + false + + db-mariadb @@ -413,6 +468,16 @@ (?si)Ready for start up.*ready [^\n]{0,30}connections + + db-allocator-db-mariadb + + org.mariadb.jdbc + mariadb-java-client + ${mariadb.version} + mariadb_galera_101 + false + + db-mssql2017 @@ -431,6 +496,16 @@ ${mssql.version} + + db-allocator-db-mssql2016 + + com.microsoft.sqlserver + mssql-jdbc + ${mssql.version} + mssql2016 + false + + db-oracle11g @@ -450,6 +525,16 @@ 12.1.0 + + db-allocator-db-oracle11g + + com.oracle + ojdbc7 + 12.1.0 + oracle11gR1 + false + + diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/ant/configure.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/common/ant/configure.xml index 3a1c70c8a8..8136c2df8c 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/ant/configure.xml +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/ant/configure.xml @@ -103,6 +103,9 @@ + + diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml index 4270d8e9ff..358876654a 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml @@ -63,6 +63,10 @@ scenario-standalone + + 1 + 1 + 1 @@ -216,16 +220,16 @@ - - - - - ${auth.server.worker.io-threads} - ${auth.server.worker.task-max-threads} - - ${session.cache.owners} - ${offline.session.cache.owners} - ${login.failure.cache.owners} + + + + + @@ -415,18 +419,7 @@ - - - - - ${jdbc.driver.tmp.dir} - ${jdbc.mvn.artifactId} - ${jdbc.mvn.version} - ${keycloak.connectionsJpa.url} - ${keycloak.connectionsJpa.user} - ${keycloak.connectionsJpa.password} - ${keycloak.connectionsJpa.schema} - + @@ -574,11 +567,6 @@ auth-server-cluster scenario-cluster - - 1 - 1 - 1 - simple