|
| 1 | +package com.marklogic.appdeployer.command; |
| 2 | + |
| 3 | +import com.fasterxml.jackson.databind.JsonNode; |
| 4 | +import com.fasterxml.jackson.databind.node.ArrayNode; |
| 5 | +import com.marklogic.appdeployer.AppConfig; |
| 6 | +import com.marklogic.client.DatabaseClient; |
| 7 | +import com.marklogic.client.ext.SecurityContextType; |
| 8 | +import com.marklogic.mgmt.ManageClient; |
| 9 | +import com.marklogic.mgmt.admin.AdminManager; |
| 10 | +import com.marklogic.mgmt.cma.ConfigurationManager; |
| 11 | +import com.marklogic.mgmt.resource.clusters.ClusterManager; |
| 12 | +import com.marklogic.rest.util.RestConfig; |
| 13 | +import org.springframework.web.client.DefaultResponseErrorHandler; |
| 14 | +import org.springframework.web.client.HttpClientErrorException; |
| 15 | +import org.springframework.web.client.ResponseErrorHandler; |
| 16 | + |
| 17 | +import javax.net.ssl.SSLContext; |
| 18 | +import java.util.ArrayList; |
| 19 | +import java.util.List; |
| 20 | +import java.util.function.Supplier; |
| 21 | +import java.util.stream.Stream; |
| 22 | + |
| 23 | +/** |
| 24 | + * Command for testing each of the connections that can be made to MarkLogic based on the configuration in a |
| 25 | + * {@code CommandContext}. |
| 26 | + * |
| 27 | + * @since 4.6.0 |
| 28 | + */ |
| 29 | +public class TestConnectionsCommand extends AbstractCommand { |
| 30 | + |
| 31 | + /** |
| 32 | + * If run in a deployment process, this should run immediately so as to fail fast. |
| 33 | + */ |
| 34 | + public TestConnectionsCommand() { |
| 35 | + setExecuteSortOrder(0); |
| 36 | + } |
| 37 | + |
| 38 | + /** |
| 39 | + * Can be included in a deployment process so that the deployment fails if any of the connections fail. |
| 40 | + * |
| 41 | + * @param context |
| 42 | + */ |
| 43 | + @Override |
| 44 | + public void execute(CommandContext context) { |
| 45 | + TestResults results = testConnections(context); |
| 46 | + if (results.anyTestFailed()) { |
| 47 | + throw new RuntimeException(results.toString()); |
| 48 | + } |
| 49 | + logger.info(results.toString()); |
| 50 | + } |
| 51 | + |
| 52 | + /** |
| 53 | + * Intended for execution outside a deployment process, where the client wants access to the test results and |
| 54 | + * will choose how to present those to a user. |
| 55 | + * |
| 56 | + * @param context |
| 57 | + * @return |
| 58 | + */ |
| 59 | + public TestResults testConnections(CommandContext context) { |
| 60 | + try { |
| 61 | + TestResult manageResult = testManageAppServer(context.getManageClient()); |
| 62 | + TestResult adminResult = testAdminAppServer(context.getAdminManager()); |
| 63 | + |
| 64 | + TestResult appServicesResult = null; |
| 65 | + TestResult restResult = null; |
| 66 | + TestResult testRestResult = null; |
| 67 | + if (manageResult.isSucceeded()) { |
| 68 | + List<Integer> serverPorts = getAppServerPorts(context.getManageClient()); |
| 69 | + appServicesResult = testAppServicesAppServer(context.getAppConfig(), serverPorts); |
| 70 | + restResult = testRestAppServer(context.getAppConfig(), serverPorts); |
| 71 | + testRestResult = testTestRestAppServer(context.getAppConfig(), serverPorts); |
| 72 | + } |
| 73 | + |
| 74 | + return new TestResults(manageResult, adminResult, appServicesResult, restResult, testRestResult); |
| 75 | + } catch (Exception ex) { |
| 76 | + // We don't expect any exceptions above, as each connection test has its own try/catch block. |
| 77 | + // This is simply to pretty up the error a bit. |
| 78 | + throw new RuntimeException("Unable to test connections; cause: " + ex.getMessage(), ex); |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + private List<Integer> getAppServerPorts(ManageClient manageClient) { |
| 83 | + JsonNode json = new ConfigurationManager(manageClient).getResourcesAsJson("server").getBody(); |
| 84 | + ArrayNode servers = (ArrayNode) json.get("config").get(0).get("server"); |
| 85 | + List<Integer> ports = new ArrayList<>(); |
| 86 | + servers.forEach(server -> { |
| 87 | + if (server.has("port")) { |
| 88 | + ports.add(server.get("port").asInt()); |
| 89 | + } |
| 90 | + }); |
| 91 | + return ports; |
| 92 | + } |
| 93 | + |
| 94 | + private TestResult testAppServicesAppServer(AppConfig appConfig, List<Integer> serverPorts) { |
| 95 | + if (appConfig.getAppServicesPort() != null && serverPorts.contains(appConfig.getAppServicesPort())) { |
| 96 | + return testWithDatabaseClient(appConfig.getHost(), appConfig.getAppServicesPort(), |
| 97 | + appConfig.getAppServicesSslContext(), appConfig.getAppServicesSecurityContextType(), |
| 98 | + appConfig.getAppServicesUsername(), () -> appConfig.newAppServicesDatabaseClient(null)); |
| 99 | + } |
| 100 | + return null; |
| 101 | + } |
| 102 | + |
| 103 | + private TestResult testRestAppServer(AppConfig appConfig, List<Integer> serverPorts) { |
| 104 | + if (appConfig.getRestPort() != null && serverPorts.contains(appConfig.getRestPort())) { |
| 105 | + return testWithDatabaseClient(appConfig.getHost(), appConfig.getRestPort(), |
| 106 | + appConfig.getRestSslContext(), appConfig.getRestSecurityContextType(), |
| 107 | + appConfig.getRestAdminUsername(), appConfig::newDatabaseClient); |
| 108 | + } |
| 109 | + return null; |
| 110 | + } |
| 111 | + |
| 112 | + private TestResult testTestRestAppServer(AppConfig appConfig, List<Integer> serverPorts) { |
| 113 | + if (appConfig.getTestRestPort() != null && serverPorts.contains(appConfig.getTestRestPort())) { |
| 114 | + return testWithDatabaseClient(appConfig.getHost(), appConfig.getTestRestPort(), |
| 115 | + appConfig.getRestSslContext(), appConfig.getRestSecurityContextType(), |
| 116 | + appConfig.getRestAdminUsername(), appConfig::newTestDatabaseClient); |
| 117 | + } |
| 118 | + return null; |
| 119 | + } |
| 120 | + |
| 121 | + public static class TestResults { |
| 122 | + private TestResult manageTestResult; |
| 123 | + private TestResult adminTestResult; |
| 124 | + private TestResult appServicesTestResult; |
| 125 | + private TestResult restServerTestResult; |
| 126 | + private TestResult testRestServerTestResult; |
| 127 | + |
| 128 | + public TestResults(TestResult manageTestResult, TestResult adminTestResult, |
| 129 | + TestResult appServicesTestResult, TestResult restServerTestResult, TestResult testRestServerTestResult) { |
| 130 | + this.manageTestResult = manageTestResult; |
| 131 | + this.adminTestResult = adminTestResult; |
| 132 | + this.appServicesTestResult = appServicesTestResult; |
| 133 | + this.restServerTestResult = restServerTestResult; |
| 134 | + this.testRestServerTestResult = testRestServerTestResult; |
| 135 | + } |
| 136 | + |
| 137 | + public boolean anyTestFailed() { |
| 138 | + return Stream.of(manageTestResult, adminTestResult, appServicesTestResult, restServerTestResult, testRestServerTestResult) |
| 139 | + .anyMatch(test -> test != null && !test.isSucceeded()); |
| 140 | + } |
| 141 | + |
| 142 | + public TestResult getManageTestResult() { |
| 143 | + return manageTestResult; |
| 144 | + } |
| 145 | + |
| 146 | + public TestResult getAdminTestResult() { |
| 147 | + return adminTestResult; |
| 148 | + } |
| 149 | + |
| 150 | + public TestResult getAppServicesTestResult() { |
| 151 | + return appServicesTestResult; |
| 152 | + } |
| 153 | + |
| 154 | + public TestResult getRestServerTestResult() { |
| 155 | + return restServerTestResult; |
| 156 | + } |
| 157 | + |
| 158 | + public TestResult getTestRestServerTestResult() { |
| 159 | + return testRestServerTestResult; |
| 160 | + } |
| 161 | + |
| 162 | + /** |
| 163 | + * @return a multi-line summary of all the non-null test results. This is intended to provide a simple |
| 164 | + * rendering of the test result data, suitable for use in ml-gradle. |
| 165 | + */ |
| 166 | + @Override |
| 167 | + public String toString() { |
| 168 | + StringBuilder sb = new StringBuilder(); |
| 169 | + sb.append("Manage App Server\n").append(getManageTestResult()) |
| 170 | + .append("\n\nAdmin App Server\n").append(getAdminTestResult()); |
| 171 | + if (getManageTestResult().isSucceeded()) { |
| 172 | + if (getAppServicesTestResult() != null) { |
| 173 | + sb.append("\n\nApp-Services App Server\n").append(getAppServicesTestResult()); |
| 174 | + } else { |
| 175 | + sb.append("\n\nNo test run for the App-Services App Server as either a port is not configured for it or it has not been deployed yet"); |
| 176 | + } |
| 177 | + if (getRestServerTestResult() != null) { |
| 178 | + sb.append("\n\nREST API App Server\n").append(getRestServerTestResult()); |
| 179 | + } else { |
| 180 | + sb.append("\n\nNo test run for a REST API App Server as either a port is not configured for it or it has not been deployed yet."); |
| 181 | + } |
| 182 | + if (getTestRestServerTestResult() != null) { |
| 183 | + sb.append("\n\nTest REST API App Server\n").append(getTestRestServerTestResult()); |
| 184 | + } else { |
| 185 | + sb.append("\n\nNo test run for a Test REST API App Server as either a port is not configured for it or it has not been deployed yet."); |
| 186 | + } |
| 187 | + } else { |
| 188 | + sb.append("\n\nCould not test connections against the App-Services or REST API App Servers " + |
| 189 | + "due to the Manage App Server connection failing."); |
| 190 | + } |
| 191 | + return sb.toString(); |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + public static class TestResult { |
| 196 | + private String host; |
| 197 | + private int port; |
| 198 | + private String scheme; |
| 199 | + private String authType; |
| 200 | + private String username; |
| 201 | + private boolean succeeded; |
| 202 | + private String message; |
| 203 | + |
| 204 | + public TestResult(RestConfig restConfig, boolean succeeded, String message) { |
| 205 | + this(restConfig.getHost(), restConfig.getPort(), restConfig.getScheme(), restConfig.getAuthType(), |
| 206 | + restConfig.getUsername(), succeeded, message); |
| 207 | + } |
| 208 | + |
| 209 | + public TestResult(RestConfig restConfig, Exception ex) { |
| 210 | + this(restConfig, false, ex.getMessage()); |
| 211 | + } |
| 212 | + |
| 213 | + public TestResult(String host, int port, String scheme, String authType, String username, DatabaseClient.ConnectionResult result) { |
| 214 | + this.host = host; |
| 215 | + this.port = port; |
| 216 | + this.scheme = scheme; |
| 217 | + this.authType = authType; |
| 218 | + this.username = username; |
| 219 | + this.succeeded = result.isConnected(); |
| 220 | + if (!result.isConnected()) { |
| 221 | + this.message = String.format("Received %d: %s", result.getStatusCode(), result.getErrorMessage()); |
| 222 | + } |
| 223 | + } |
| 224 | + |
| 225 | + public TestResult(String host, int port, String scheme, String authType, String username, boolean succeeded, String message) { |
| 226 | + this.host = host; |
| 227 | + this.port = port; |
| 228 | + this.scheme = scheme; |
| 229 | + this.authType = authType; |
| 230 | + this.username = username; |
| 231 | + this.succeeded = succeeded; |
| 232 | + this.message = message; |
| 233 | + } |
| 234 | + |
| 235 | + public String getHost() { |
| 236 | + return host; |
| 237 | + } |
| 238 | + |
| 239 | + public int getPort() { |
| 240 | + return port; |
| 241 | + } |
| 242 | + |
| 243 | + public String getScheme() { |
| 244 | + return scheme; |
| 245 | + } |
| 246 | + |
| 247 | + public boolean isSucceeded() { |
| 248 | + return succeeded; |
| 249 | + } |
| 250 | + |
| 251 | + public String getMessage() { |
| 252 | + return message; |
| 253 | + } |
| 254 | + |
| 255 | + public String getAuthType() { |
| 256 | + return authType; |
| 257 | + } |
| 258 | + |
| 259 | + public String getUsername() { |
| 260 | + return username; |
| 261 | + } |
| 262 | + |
| 263 | + /** |
| 264 | + * @return a multi-line representation of the test result. This is intended to provide a simple |
| 265 | + * rendering of the test result data, suitable for use in ml-gradle. |
| 266 | + */ |
| 267 | + @Override |
| 268 | + public String toString() { |
| 269 | + String result = String.format("Configured to connect to %s://%s:%d using '%s' authentication", |
| 270 | + getScheme(), getHost(), getPort(), getAuthType()); |
| 271 | + if (getUsername() != null) { |
| 272 | + result += String.format(" and username of '%s'", getUsername()); |
| 273 | + } |
| 274 | + if (isSucceeded()) { |
| 275 | + result += "\nConnected successfully"; |
| 276 | + return getMessage() != null ? result + "; " + getMessage() : result; |
| 277 | + } |
| 278 | + return result + "\nFAILED TO CONNECT; cause: " + message; |
| 279 | + } |
| 280 | + } |
| 281 | + |
| 282 | + private TestResult testManageAppServer(ManageClient client) { |
| 283 | + ResponseErrorHandler originalErrorHandler = client.getRestTemplate().getErrorHandler(); |
| 284 | + client.getRestTemplate().setErrorHandler(new DefaultResponseErrorHandler()); |
| 285 | + try { |
| 286 | + String version = new ClusterManager(client).getVersion(); |
| 287 | + return new TestResult(client.getManageConfig(), true, "MarkLogic version: " + version); |
| 288 | + } catch (Exception ex) { |
| 289 | + if (ex instanceof HttpClientErrorException && ((HttpClientErrorException) ex).getRawStatusCode() == 404) { |
| 290 | + return new TestResult(client.getManageConfig(), false, |
| 291 | + "Unable to access /manage/v2; received 404; unexpected response: " + ex.getMessage()); |
| 292 | + } else { |
| 293 | + return new TestResult(client.getManageConfig(), ex); |
| 294 | + } |
| 295 | + } finally { |
| 296 | + client.getRestTemplate().setErrorHandler(originalErrorHandler); |
| 297 | + } |
| 298 | + } |
| 299 | + |
| 300 | + private TestResult testAdminAppServer(AdminManager adminManager) { |
| 301 | + try { |
| 302 | + String timestamp = adminManager.getServerTimestamp(); |
| 303 | + return new TestResult(adminManager.getAdminConfig(), true, "MarkLogic server timestamp: " + timestamp); |
| 304 | + } catch (Exception ex) { |
| 305 | + return new TestResult(adminManager.getAdminConfig(), ex); |
| 306 | + } |
| 307 | + } |
| 308 | + |
| 309 | + private TestResult testWithDatabaseClient(String host, Integer port, SSLContext sslContext, |
| 310 | + SecurityContextType securityContextType, String username, Supplier<DatabaseClient> supplier) { |
| 311 | + if (port == null) { |
| 312 | + return null; |
| 313 | + } |
| 314 | + final String scheme = sslContext != null ? "https" : "http"; |
| 315 | + final String authType = securityContextType != null ? securityContextType.name().toLowerCase() : "unknown"; |
| 316 | + DatabaseClient client = null; |
| 317 | + try { |
| 318 | + client = supplier.get(); |
| 319 | + return new TestResult(host, port, scheme, authType, username, client.checkConnection()); |
| 320 | + } catch (Exception ex) { |
| 321 | + return new TestResult(host, port, scheme, authType, username, false, ex.getMessage()); |
| 322 | + } finally { |
| 323 | + if (client != null) { |
| 324 | + client.release(); |
| 325 | + } |
| 326 | + } |
| 327 | + } |
| 328 | +} |
0 commit comments