diff --git a/src/main/java/com/marklogic/appdeployer/mgmt/AdminConfig.java b/src/main/java/com/marklogic/appdeployer/mgmt/AdminConfig.java new file mode 100644 index 00000000..aec2a76e --- /dev/null +++ b/src/main/java/com/marklogic/appdeployer/mgmt/AdminConfig.java @@ -0,0 +1,22 @@ +package com.marklogic.appdeployer.mgmt; + +import com.marklogic.appdeployer.util.RestConfig; + +/** + * Defines the configuration data for talking to the Admin REST API. + */ +public class AdminConfig extends RestConfig { + + public AdminConfig() { + super("localhost", 8001, "admin", "admin"); + } + + public AdminConfig(String host, String password) { + super(host, 8001, "admin", password); + } + + public AdminConfig(String host, int port, String username, String password) { + super(host, port, username, password); + } + +} diff --git a/src/main/java/com/marklogic/appdeployer/mgmt/ConfigManager.java b/src/main/java/com/marklogic/appdeployer/mgmt/ConfigManager.java index 4915d37c..73bb5af5 100644 --- a/src/main/java/com/marklogic/appdeployer/mgmt/ConfigManager.java +++ b/src/main/java/com/marklogic/appdeployer/mgmt/ConfigManager.java @@ -3,15 +3,8 @@ import java.io.File; import java.io.IOException; -import org.springframework.core.io.ClassPathResource; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.util.ClassUtils; import org.springframework.util.FileCopyUtils; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import com.marklogic.appdeployer.AppConfig; @@ -26,7 +19,8 @@ public class ConfigManager extends LoggingObject { private ManageClient client; - private AppServicesConfig appServicesConfig; + private AdminConfig adminConfig; + private int waitForRestartCheckInterval = 500; public ConfigManager(ManageClient client) { this.client = client; @@ -74,36 +68,58 @@ protected String replaceTestRestApiTokens(String input, AppConfig config) { return input; } - public void uninstallApp(AppConfig config) { - if (appServicesConfig == null) { - throw new IllegalStateException("Cannot uninstall an app without an instance of AppServicesConfig set"); + public void deleteRestApiAndWaitForRestart(AppConfig config, boolean includeModules, boolean includeContent) { + String timestamp = getLastRestartTimestamp(); + logger.info("About to delete REST API, will then wait for MarkLogic to restart"); + deleteRestApi(config, includeModules, includeContent); + waitForRestart(timestamp); + } + + public void deleteRestApi(AppConfig config, boolean includeModules, boolean includeContent) { + String path = client.getBaseUrl() + "/v1/rest-apis/" + config.getName() + "?"; + if (includeModules) { + path += "include=modules&"; } - String xquery = loadStringFromClassPath("uninstall-app.xqy"); - xquery = xquery.replace("%%APP_NAME%%", config.getName()); + if (includeContent) { + path += "include=content"; + } + logger.info("Deleting app, path: " + path); + client.getRestTemplate().exchange(path, HttpMethod.DELETE, null, String.class); + logger.info("Finished deleting app"); + } - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - MultiValueMap map = new LinkedMultiValueMap(); - map.add("xquery", xquery); + public void waitForRestart(String lastRestartTimestamp) { + logger.info("Waiting for MarkLogic to restart, last restart timestamp: " + lastRestartTimestamp); + logger.info("Ignore any HTTP client logging about socket exceptions and retries, those are expected while waiting for MarkLogic to restart"); + while (true) { + sleepUntilNextRestartCheck(); + String restart = getLastRestartTimestamp(); + if (restart != null && !restart.equals(lastRestartTimestamp)) { + logger.info(String + .format("MarkLogic has successfully restarted; new restart timestamp [%s] is greater than last restart timestamp [%s]", + restart, lastRestartTimestamp)); + break; + } + } + } - logger.info("Uninstalling app with name: " + config.getName()); + protected void sleepUntilNextRestartCheck() { try { - RestTemplate rt = RestTemplateUtil.newRestTemplate(appServicesConfig); - rt.exchange("http://localhost:8000/v1/eval", HttpMethod.POST, - new HttpEntity>(map, headers), String.class); + Thread.sleep(getWaitForRestartCheckInterval()); } catch (Exception e) { - logger.warn("Could not uninstall app; it may not be installed yet? Cause: " + e.getMessage()); + // ignore } } - protected String loadStringFromClassPath(String path) { - path = ClassUtils.addResourcePathToPackagePath(getClass(), path); - try { - return new String(FileCopyUtils.copyToByteArray(new ClassPathResource(path).getInputStream())); - } catch (IOException ie) { - throw new RuntimeException("Unable to load string from classpath resource at: " + path + "; cause: " - + ie.getMessage(), ie); + /** + * TODO May want to extract this into an AdminManager that depends on AdminConfig. + */ + public String getLastRestartTimestamp() { + if (adminConfig == null) { + throw new IllegalStateException("Cannot access admin app, no admin config provided"); } + RestTemplate t = RestTemplateUtil.newRestTemplate(adminConfig); + return t.getForEntity(adminConfig.getBaseUrl() + "/admin/v1/timestamp", String.class).getBody(); } protected String copyFileToString(File f) { @@ -115,7 +131,19 @@ protected String copyFileToString(File f) { } } - public void setAppServicesConfig(AppServicesConfig appServicesConfig) { - this.appServicesConfig = appServicesConfig; + public AdminConfig getAdminConfig() { + return adminConfig; + } + + public void setAdminConfig(AdminConfig adminConfig) { + this.adminConfig = adminConfig; + } + + public int getWaitForRestartCheckInterval() { + return waitForRestartCheckInterval; + } + + public void setWaitForRestartCheckInterval(int waitForRestartCheckInterval) { + this.waitForRestartCheckInterval = waitForRestartCheckInterval; } } diff --git a/src/main/java/com/marklogic/appdeployer/mgmt/uninstall-app.xqy b/src/main/java/com/marklogic/appdeployer/mgmt/uninstall-app.xqy deleted file mode 100644 index 71493f4f..00000000 --- a/src/main/java/com/marklogic/appdeployer/mgmt/uninstall-app.xqy +++ /dev/null @@ -1,142 +0,0 @@ -xquery version "1.0-ml"; - -import module namespace admin = "http://marklogic.com/xdmp/admin" at "/MarkLogic/admin.xqy"; -import module namespace sec = "http://marklogic.com/xdmp/security" at "/MarkLogic/security.xqy"; - -declare function local:find-appservers($config, $app-name) -{ - for $id in admin:get-appserver-ids($config) - let $name := admin:appserver-get-name($config, $id) - where $name = $app-name or fn:starts-with($name, fn:concat($app-name, "-")) - return $id -}; - -(: -Switching the app servers to the Documents database first (necessary so we can delete the underlying databases and -forests before we delete the XDBC server we talk to) seems to remove the possibility of the content forest not being -deleted (see the comment below). -:) - -let $app-name := "%%APP_NAME%%" -let $config := admin:get-configuration() - -let $appserver-ids := local:find-appservers($config, $app-name) - -let $documents-id := admin:database-get-id($config, "Documents") - -let $_ := - for $id in $appserver-ids - let $config := admin:get-configuration() - let $config := admin:appserver-set-database($config, $id, $documents-id) - let $config := admin:appserver-set-modules-database($config, $id, $documents-id) - return admin:save-configuration($config) - -return () -; - - - -xquery version "1.0-ml"; - -import module namespace admin = "http://marklogic.com/xdmp/admin" at "/MarkLogic/admin.xqy"; -import module namespace sec = "http://marklogic.com/xdmp/security" at "/MarkLogic/security.xqy"; - -declare function local:find-appservers($config, $app-name) -{ - for $id in admin:get-appserver-ids($config) - let $name := admin:appserver-get-name($config, $id) - where $name = $app-name or fn:starts-with($name, fn:concat($app-name, "-")) - return $id -}; - -declare function local:find-databases($config, $app-name) -{ - for $name in (fn:concat($app-name, "-content"), fn:concat($app-name, "-modules"), fn:concat($app-name, "-test-content"), fn:concat($app-name, "-triggers")) - where admin:database-exists($config, $name) - return admin:database-get-id($config, $name) -}; - -declare function local:find-forests($config, $app-name, $database-ids) -{ - for $forest-id in admin:get-forest-ids($config) - let $name := admin:forest-get-name($config, $forest-id) - where admin:forest-get-database($config, $forest-id) = $database-ids - return $forest-id -}; - -declare function local:delete-forests($config, $forest-ids) -{ - for $id in $forest-ids - let $db-id := admin:forest-get-database($config, $id) - let $config := admin:get-configuration() - let $config := admin:database-detach-forest($config, $db-id, $id) - let $_ := admin:save-configuration($config) - let $config := admin:get-configuration() - let $message := text{"Deleting forest", admin:forest-get-name($config, $id)} - let $_ := xdmp:log($message) - let $config := admin:forest-delete($config, $id, fn:true()) - return (admin:save-configuration($config), $message) -}; - -declare function local:delete-databases($config, $database-ids) -{ - for $id in $database-ids - let $config := admin:get-configuration() - let $message := text{"Deleting database", admin:database-get-name($config, $id)} - let $_ := xdmp:log($message) - let $config := admin:database-delete($config, $id) - return (admin:save-configuration($config), $message) -}; - -declare function local:delete-appservers($config, $appserver-ids) -{ - for $id at $index in $appserver-ids - let $config := admin:get-configuration() - let $message := text{"Deleting appserver", admin:appserver-get-name($config, $id)} - let $_ := xdmp:log($message) - let $config := admin:appserver-delete($config, $id) - return - if ($index = fn:count($appserver-ids)) then - let $_ := admin:save-configuration-without-restart($config) - return $message - else (admin:save-configuration($config), $message) -}; - -let $app-name := "%%APP_NAME%%" -let $config := admin:get-configuration() - -let $appserver-ids := local:find-appservers($config, $app-name) -let $database-ids := local:find-databases($config, $app-name) -let $forest-ids := local:find-forests($config, $app-name, $database-ids) - -let $database-names := - for $id in $database-ids - return admin:database-get-name($config, $id) -let $_ := xdmp:log(text{"Found database names", $database-names}) - -let $forest-names := - for $id in $forest-ids - return admin:forest-get-name($config, $id) -let $_ := xdmp:log(text{"Found forest names", $forest-names}) - -(: -The content forest is consistently not being found, but it's still deleted. The logs show the forest somehow being -deleted during the find-forests call. There's also a warning logged: "Warning: XDMP-FORESTNOT: Forest -(app name)-content-1 not available: unmounted". Right after that, the forest is deleted. But again, this oddly occurs -during the find-forests function call, which means it's not returned as a deleted forest in the delete-forests call -below. Very odd - seems like a bug. So the text returned by this module won't list the content database as having -been deleted, but it in fact has been. -:) - -let $forest-messages := local:delete-forests($config, $forest-ids) -let $database-messages := local:delete-databases($config, $database-ids) -let $appserver-messages := local:delete-appservers($config, $appserver-ids) - -return ( - text{"Found databases", $database-names}, - text{"Found forests", $forest-names}, - $database-messages, - $forest-messages, - $appserver-messages -) - diff --git a/src/test/java/com/marklogic/appdeployer/mgmt/AbstractMgmtTest.java b/src/test/java/com/marklogic/appdeployer/mgmt/AbstractMgmtTest.java new file mode 100644 index 00000000..a6cacdf4 --- /dev/null +++ b/src/test/java/com/marklogic/appdeployer/mgmt/AbstractMgmtTest.java @@ -0,0 +1,51 @@ +package com.marklogic.appdeployer.mgmt; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.marklogic.appdeployer.AppConfig; + +/** + * Base class for tests that run against the new management API in ML8. Main purpose is to provide convenience methods + * for quickly creating and deleting a sample application. + */ +public abstract class AbstractMgmtTest extends Assert { + + public final static String SAMPLE_APP_NAME = "sample-app"; + + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + protected ManageConfig manageConfig; + protected ManageClient manageClient; + + protected ConfigDir configDir; + protected ConfigManager configMgr; + + protected AppConfig appConfig; + + @Before + public void initialize() { + manageClient = new ManageClient(new ManageConfig()); + configDir = new ConfigDir(new File("src/test/resources/sample-app/src/main/ml-config")); + configMgr = new ConfigManager(manageClient); + initializeAppConfig(); + } + + protected void createSampleApp() { + configMgr.createRestApi(configDir, appConfig); + } + + protected void initializeAppConfig() { + appConfig = new AppConfig(); + appConfig.setName("sample-app"); + appConfig.setRestPort(8540); + } + + protected void deleteSampleApp() { + configMgr.deleteRestApiAndWaitForRestart(appConfig, true, true); + } +} diff --git a/src/test/java/com/marklogic/appdeployer/mgmt/DeleteRestApiTest.java b/src/test/java/com/marklogic/appdeployer/mgmt/DeleteRestApiTest.java new file mode 100644 index 00000000..513f203e --- /dev/null +++ b/src/test/java/com/marklogic/appdeployer/mgmt/DeleteRestApiTest.java @@ -0,0 +1,24 @@ +package com.marklogic.appdeployer.mgmt; + +import org.junit.Test; + +import com.marklogic.appdeployer.mgmt.services.ServiceManager; + +/** + * This test ensures that the convenience methods for creating and deleting a sample application work properly, and thus + * they can be used in other tests that depend on having an app in place. + */ +public class DeleteRestApiTest extends AbstractMgmtTest { + + @Test + public void createAndDelete() { + ServiceManager mgr = new ServiceManager(manageClient); + + createSampleApp(); + assertTrue("The REST API server should exist", mgr.restApiServerExists(SAMPLE_APP_NAME)); + + configMgr.setAdminConfig(new AdminConfig()); + deleteSampleApp(); + assertFalse("The REST API server should have been deleted", mgr.restApiServerExists(SAMPLE_APP_NAME)); + } +} diff --git a/src/test/java/com/marklogic/appdeployer/mgmt/TestClient.java b/src/test/java/com/marklogic/appdeployer/mgmt/TestClient.java deleted file mode 100644 index 706f247d..00000000 --- a/src/test/java/com/marklogic/appdeployer/mgmt/TestClient.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.marklogic.appdeployer.mgmt; - -import java.io.File; - -import com.marklogic.appdeployer.AppConfig; - -public class TestClient { - - public static void main(String[] args) throws Exception { - - // Define how to connect to the ML manage API - this defaults to localhost/8002/admin/admin - ManageConfig manageConfig = new ManageConfig(); - - // Build a client for talking to the ML manage API - this wraps an instance of Spring RestTemplate with some - // convenience methods for talking to the manage API - ManageClient client = new ManageClient(manageConfig); - - // Define where configuration files for the ML app are - defaults to src/main/ml-config - ConfigDir configDir = new ConfigDir(new File("src/test/resources/sample-app/src/main/ml-config")); - - // Build a ConfigManager with all the fun methods - ConfigManager configMgr = new ConfigManager(client); - - // Define app configuration - AppConfig appConfig = new AppConfig(); - appConfig.setName("sample-app"); - appConfig.setRestPort(8100); - appConfig.setTestRestPort(8101); - - // Now start calling fun methods that get things done - configMgr.createRestApi(configDir, appConfig); - - // In order to uninstall, need to define how to talk to 8000/v1/eval - // AppServicesConfig defaults to localhost/8000/admin/admin - configMgr.setAppServicesConfig(new AppServicesConfig()); - - //configMgr.uninstallApp(appConfig); - - System.out.println("All done!"); - } -}