/amps-maven-plugin/src/main/java/com/atlassian/maven/plugins/amps/product/BitbucketProductHandler.java
Java | 660 lines | 577 code | 57 blank | 26 comment | 24 complexity | d49a62ab43e5fb49757055ad4788b51f MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause
- package com.atlassian.maven.plugins.amps.product;
- import com.atlassian.maven.plugins.amps.MavenContext;
- import com.atlassian.maven.plugins.amps.MavenGoals;
- import com.atlassian.maven.plugins.amps.Node;
- import com.atlassian.maven.plugins.amps.Product;
- import com.atlassian.maven.plugins.amps.ProductArtifact;
- import com.atlassian.maven.plugins.amps.product.manager.WebAppManager;
- import com.atlassian.maven.plugins.amps.util.JvmArgsFix;
- import com.atlassian.maven.plugins.amps.util.MavenProjectLoader;
- import com.atlassian.maven.plugins.amps.util.PropertyUtils;
- import com.atlassian.maven.plugins.amps.util.ant.AntJavaExecutorThread;
- import com.atlassian.maven.plugins.amps.util.ant.JavaTaskFactory;
- import com.google.common.annotations.VisibleForTesting;
- import com.google.common.collect.ImmutableMap;
- import org.apache.maven.artifact.Artifact;
- import org.apache.maven.artifact.resolver.ArtifactResolver;
- import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
- import org.apache.maven.model.Dependency;
- import org.apache.maven.plugin.MojoExecutionException;
- import org.apache.maven.project.ProjectBuilder;
- import org.apache.maven.repository.RepositorySystem;
- import org.apache.tools.ant.taskdefs.Java;
- import javax.annotation.Nonnull;
- import javax.management.AttributeNotFoundException;
- import javax.management.InstanceNotFoundException;
- import javax.management.MBeanServerConnection;
- import javax.management.ObjectName;
- import javax.management.ReflectionException;
- import javax.management.remote.JMXConnector;
- import javax.management.remote.JMXConnectorFactory;
- import javax.management.remote.JMXServiceURL;
- import java.io.BufferedReader;
- import java.io.File;
- import java.io.FileNotFoundException;
- import java.io.IOException;
- import java.net.ConnectException;
- import java.net.InetAddress;
- import java.net.UnknownHostException;
- import java.nio.charset.StandardCharsets;
- import java.nio.file.Files;
- import java.nio.file.NoSuchFileException;
- import java.nio.file.Path;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Optional;
- import java.util.Properties;
- import java.util.UUID;
- import static com.atlassian.maven.plugins.amps.util.ProductHandlerUtil.pickFreePort;
- import static java.lang.Boolean.TRUE;
- import static java.lang.String.format;
- import static java.util.Collections.emptyList;
- import static java.util.Objects.requireNonNull;
- import static java.util.Optional.empty;
- import static java.util.Optional.of;
- import static java.util.stream.Collectors.joining;
- import static org.apache.commons.io.FileUtils.deleteQuietly;
- import static org.apache.commons.lang3.StringUtils.defaultString;
- import static org.apache.commons.lang3.StringUtils.isBlank;
- import static org.apache.commons.lang3.StringUtils.isNotBlank;
- import static org.apache.maven.artifact.Artifact.LATEST_VERSION;
- import static org.apache.maven.artifact.Artifact.RELEASE_VERSION;
- /**
- * @since 6.1.0
- */
- public class BitbucketProductHandler extends AbstractProductHandler {
- @VisibleForTesting
- static final String EMBEDDED_ELASTICSEARCH_HTTP_PORT = "plugin.embedded-elasticsearch.http.port";
- @VisibleForTesting
- static final String EMBEDDED_ELASTICSEARCH_TCP_PORT = "plugin.embedded-elasticsearch.transport.tcp.port";
- @VisibleForTesting
- static final String SSH_PORT = "plugin.ssh.port";
- @VisibleForTesting
- static final String HAZELCAST_GROUP = "hazelcast.group.name";
- @VisibleForTesting
- static final String HAZELCAST_PORT = "hazelcast.port";
- private static final String ADMIN_OBJECT_NAME = "org.springframework.boot:type=Admin,name=SpringApplication";
- private static final int DEFAULT_JMX_PORT = 7995;
- // Note: Versions require an "-a0" qualifier so that milestone, rc and snapshot releases are evaluated as
- // being 'later' (see org.apache.maven.artifact.versioning.ComparableVersion.StringItem)
- private static final DefaultArtifactVersion FIRST_SEARCH_VERSION = new DefaultArtifactVersion("4.6.0-a0");
- private static final DefaultArtifactVersion FIRST_SPRING_BOOT_VERSION = new DefaultArtifactVersion("5.0.0-a0");
- private static final String JMX_PORT_FILE = "jmx-port";
- private static final String JMX_URL_FORMAT = "service:jmx:rmi:///jndi/rmi://127.0.0.1:%1$d/jmxrmi";
- private static final String SEARCH_GROUP_ID = "com.atlassian.bitbucket.search";
- private static final String SERVER_GROUP_ID = "com.atlassian.bitbucket.server";
- private static final String ELASTICSEARCH_BASEURL = "plugin.search.elasticsearch.baseurl";
- private static final String HAZELCAST_NETWORK_TCPIP = "hazelcast.network.tcpip";
- private static final String HAZELCAST_NETWORK_TCPIP_MEMBERS = "hazelcast.network.tcpip.members";
- private static final String HAZELCAST_PASSWORD = "hazelcast.group.password";
- private final MavenProjectLoader projectLoader;
- private final ProjectBuilder projectBuilder;
- private final JavaTaskFactory taskFactory;
- private final WebAppManager webAppManager;
- public BitbucketProductHandler(final MavenContext context,
- final MavenGoals goals,
- final RepositorySystem repositorySystem,
- final MavenProjectLoader projectLoader,
- final ProjectBuilder projectBuilder,
- final ArtifactResolver artifactResolver,
- final WebAppManager webAppManager) {
- super(context, goals, new BitbucketPluginProvider(), repositorySystem, artifactResolver);
- this.projectLoader = requireNonNull(projectLoader);
- this.projectBuilder = requireNonNull(projectBuilder);
- this.webAppManager = requireNonNull(webAppManager);
- this.taskFactory = new JavaTaskFactory(log);
- }
- @Override
- protected void cleanupProductHomeForZip(@Nonnull final Product product, @Nonnull final File snapshotDir)
- throws MojoExecutionException, IOException {
- super.cleanupProductHomeForZip(product, snapshotDir);
- deleteQuietly(new File(snapshotDir, "log/atlassian-bitbucket.log"));
- deleteQuietly(new File(snapshotDir, ".osgi-cache"));
- }
- @Override
- @Nonnull
- public String getId() {
- return ProductHandlerFactory.BITBUCKET;
- }
- @Override
- @Nonnull
- public List<ProductArtifact> getAdditionalPlugins(final Product bitbucket) throws MojoExecutionException {
- final List<ProductArtifact> additionalPlugins = new ArrayList<>();
- if (usesElasticSearch(bitbucket)) {
- // Add the embedded ES plugin, at the same version as the production search plugin
- getSearchPluginDependency(bitbucket)
- .ifPresent(dependency -> additionalPlugins.add(new ProductArtifact(SEARCH_GROUP_ID,
- "embedded-elasticsearch-plugin", dependency.getVersion())));
- }
- return additionalPlugins;
- }
- private boolean usesElasticSearch(final Product bitbucket) {
- return new DefaultArtifactVersion(bitbucket.getVersion()).compareTo(FIRST_SEARCH_VERSION) >= 0;
- }
- private Optional<Dependency> getSearchPluginDependency(final Product bitbucket) throws MojoExecutionException {
- final Artifact bitbucketParentPom = repositorySystem.createProjectArtifact(
- SERVER_GROUP_ID, "bitbucket-parent", bitbucket.getVersion());
- return projectLoader.loadMavenProject(context.getProject(), bitbucketParentPom, projectBuilder)
- .flatMap(mavenProject -> Optional.ofNullable(mavenProject.getDependencyManagement())
- .flatMap(dependencyManagement -> dependencyManagement.getDependencies().stream()
- .filter(dep -> SEARCH_GROUP_ID.equals(dep.getGroupId()))
- .findFirst()));
- }
- @Nonnull
- @Override
- public ProductArtifact getArtifact() {
- return new ProductArtifact(SERVER_GROUP_ID, "bitbucket-webapp");
- }
- @Override
- @Nonnull
- public File getBundledPluginPath(final Product product, final File productDir) {
- // Starting from 4.8, bundled plugins are no longer a zip file. Instead, they're unpacked in the
- // webapp itself. This way, first run doesn't need to pay the I/O cost to unpack them
- final File bundledPluginsDir = new File(productDir, "WEB-INF/atlassian-bundled-plugins");
- if (bundledPluginsDir.isDirectory()) {
- return bundledPluginsDir;
- }
- // If the atlassian-bundled-plugins directory doesn't exist, assume we're using an older version
- // of Bitbucket Server where bundled plugins are still zipped up
- return new File(productDir, "WEB-INF/classes/bitbucket-bundled-plugins.zip");
- }
- @Override
- @Nonnull
- public String getDefaultContainerId() {
- return "tomcat8x";
- }
- @Override
- public int getDefaultHttpPort() {
- return 7990;
- }
- @Override
- public int getDefaultHttpsPort() {
- return 8447;
- }
- @Nonnull
- @Override
- public Map<String, String> getSystemProperties(final Product product, final int nodeIndex) {
- final ImmutableMap.Builder<String, String> builder = ImmutableMap.<String, String>builder()
- .put("johnson.spring.lifecycle.synchronousStartup", TRUE.toString());
- defaultSyspropsFromFile(product);
- final File homeDirectory = getHomeDirectories(product).get(nodeIndex);
- final String baseUrl = product.getBaseUrlForNode(nodeIndex);
- builder.put("baseurl", baseUrl);
- builder.put("baseurl.display", baseUrl);
- builder.put("bitbucket.home", fixSlashes(homeDirectory.getPath()));
- if (product.isMultiNode()) {
- configureNodeForElasticsearch(product, nodeIndex);
- setUpSsh(product, nodeIndex);
- setUpHazelcast(product, nodeIndex);
- builder.put("cluster.node.name", product.getInstanceId() + "-" + nodeIndex);
- }
- return builder.build();
- }
- private static void configureNodeForElasticsearch(final Product product,
- final int nodeIndex) {
- final Node node = product.getNodes().get(nodeIndex);
- // We intentionally set ES ports on each node for now to prevent them
- // from conflicting. This won't be necessary when the embedded ES is
- // cluster-aware.
- // TODO: BSP-3059 Make this a product-level setting when embedded ES is updated.
- node.defaultSystemProperty(EMBEDDED_ELASTICSEARCH_HTTP_PORT, () -> String.valueOf(pickFreePort(0)));
- node.defaultSystemProperty(EMBEDDED_ELASTICSEARCH_TCP_PORT, () -> String.valueOf(pickFreePort(0)));
- final String nodeZeroElasticsearchHttpPort = product.getNodes().get(0)
- .getSystemProperties()
- .get(EMBEDDED_ELASTICSEARCH_HTTP_PORT);
- final String nodeZeroElasticsearchTcpPort = product.getNodes().get(0)
- .getSystemProperties()
- .get(EMBEDDED_ELASTICSEARCH_TCP_PORT);
- if (isBlank(nodeZeroElasticsearchHttpPort) || isBlank(nodeZeroElasticsearchTcpPort)) {
- throw new IllegalStateException(
- format("First node's Elasticsearch HTTP or TCP port is blank: '%s', '%s'",
- nodeZeroElasticsearchHttpPort,
- nodeZeroElasticsearchTcpPort));
- }
- product.defaultSystemProperty(ELASTICSEARCH_BASEURL, () ->
- format("http://localhost:%s", nodeZeroElasticsearchHttpPort));
- }
- private static void setUpSsh(final Product product,
- final int nodeIndex) {
- final Node node = product.getNodes().get(nodeIndex);
- node.defaultSystemProperty(SSH_PORT, () -> String.valueOf(pickFreePort(0)));
- }
- private static void setUpHazelcast(final Product product,
- final int nodeIndex) {
- if (nodeIndex == 0) {
- // Set up all hazelcast ports ahead of time so config can be appropriately managed
- product.getNodes().forEach(nodeToDefault ->
- nodeToDefault.defaultSystemProperty(HAZELCAST_PORT, () -> String.valueOf(pickFreePort(0)))
- );
- product.defaultSystemProperty(HAZELCAST_NETWORK_TCPIP, () -> String.valueOf(true));
- product.defaultSystemProperty(HAZELCAST_PASSWORD, () -> "admin");
- product.defaultSystemProperty(HAZELCAST_NETWORK_TCPIP_MEMBERS, () -> getHazelcastNetworkTcpMembers(product));
- product.defaultSystemProperty(HAZELCAST_GROUP, () -> String.valueOf(UUID.randomUUID()));
- }
- }
- private static String getHazelcastNetworkTcpMembers(final Product product) {
- return product.getNodes().stream()
- .map(Node::getSystemProperties)
- .map(p -> "127.0.0.1:" + p.get(HAZELCAST_PORT))
- .collect(joining(","));
- }
- @Nonnull
- @Override
- public Optional<ProductArtifact> getTestResourcesArtifact() {
- return Optional.of(new ProductArtifact(SERVER_GROUP_ID, "bitbucket-it-resources"));
- }
- @Override
- @Nonnull
- public Optional<File> getUserInstalledPluginsDirectory(Product product, File webappDir, File homeDir) {
- File baseDir = homeDir;
- File sharedHomeDir = new File(homeDir, "shared");
- if (sharedHomeDir.exists()) {
- baseDir = sharedHomeDir;
- }
- return Optional.of(new File(new File(baseDir, "plugins"), "installed-plugins"));
- }
- @Override
- protected void customiseInstance(final Product product, final File homeDir, final File explodedWarDir) {
- if (product.isMultiNode()) {
- configureCluster(homeDir, product);
- }
- }
- private void configureCluster(final File homeDir, final Product product) {
- product.defaultSystemProperty("bitbucket.shared.home",
- () -> getSharedHome(product).getPath());
- }
- private void defaultSyspropsFromFile(final Product product) {
- getBitbucketProperties(product).ifPresent(props -> {
- for (Map.Entry<Object, Object> property : props.entrySet()) {
- product.defaultSystemProperty(String.valueOf(property.getKey()),
- () -> String.valueOf(property.getValue()));
- }
- });
- }
- private Optional<Properties> getBitbucketProperties(final Product product) {
- File propertiesFile = new File(getSharedHome(product), "bitbucket.properties");
- try {
- return Optional.of(PropertyUtils.load(propertiesFile));
- } catch (MojoExecutionException e) {
- return Optional.empty();
- }
- }
- private File getSharedHome(final Product product) {
- if (isNotBlank(product.getSharedHome())) {
- return new File(product.getSharedHome());
- }
- // Otherwise, use the shared-home found in the default Bitbucket home ZIP
- return new File(getHomeDirectories(product).get(0), "shared");
- }
- @Override
- public void stop(@Nonnull final Product product) throws MojoExecutionException {
- if (isSpringBoot(product)) {
- int jmxPort = readJmxPort(product);
- boolean connected = false;
- try (JMXConnector connector = createConnector(jmxPort)) {
- MBeanServerConnection connection = connector.getMBeanServerConnection();
- connected = true; // Connected successfully, so an exception most likely means success
- connection.invoke(new ObjectName(ADMIN_OBJECT_NAME), "shutdown", null, null);
- } catch (InstanceNotFoundException e) {
- throw new MojoExecutionException("Spring Boot administration is not available; " +
- "Bitbucket Server will need to be stopped manually", e);
- } catch (Exception e) {
- // Invoking shutdown will never receive a response because the application stops as part
- // of processing the call. The "connected" flag is used to differentiate errors
- if (connected) {
- log.debug("Bitbucket Server has stopped");
- } else {
- log.warn("There was an error attempting to stop Bitbucket Server", e);
- }
- }
- } else {
- webAppManager.stopWebapp(product, context);
- }
- }
- @Override
- @Nonnull
- protected File extractApplication(final Product product) throws MojoExecutionException {
- final ProductArtifact artifact = getArtifact(product);
- // check for a stable version if needed
- if (RELEASE_VERSION.equals(artifact.getVersion()) || LATEST_VERSION.equals(artifact.getVersion())) {
- setLatestStableVersion(product, artifact);
- }
- final File baseDir = getBaseDirectory(product);
- if (isSpringBoot(product)) {
- // Use the maven-dependency-plugin to unpack the WAR file, rather than copying it over
- final File appDir = new File(baseDir, "app");
- goals.unpackWebappWar(appDir, artifact);
- return appDir;
- }
- return goals.copyWebappWar(artifact, baseDir, product.getId());
- }
- @Override
- protected void fixJvmArgs(Product product) {
- // Don't use JvmArgsFix.defaults(); it applies -XX:MaxPermSize which just triggers warnings
- // on Java 8 (which is the only Java version Bitbucket Server ever allowed)
- final String jvmArgs = JvmArgsFix.empty()
- .with("-Xmx", "1g") // Use a 1g max heap by default instead of 512m
- .withAddOpens(ADD_OPENS_FOR_TOMCAT)
- .withAddOpens(ADD_OPENS_FOR_FELIX)
- .apply(product.getJvmArgs());
- product.setJvmArgs(jvmArgs);
- }
- @Override
- @Nonnull
- protected List<Node> startProduct(
- final Product product, final File productFile, final List<Map<String, String>> systemProperties)
- throws MojoExecutionException {
- if (isSpringBoot(product)) {
- startNodes(product, productFile, systemProperties);
- return product.getNodes();
- }
- // For Bitbucket Server 4.x, deploy the webapp to Tomcat using Cargo
- return webAppManager.startWebapp(productFile, systemProperties, emptyList(), emptyList(), product, context);
- }
- @Override
- protected boolean supportsStaticPlugins() {
- return true;
- }
- @Nonnull
- private void startNodes(final Product product,
- final File app,
- final List<Map<String, String>> systemProperties) throws MojoExecutionException {
- final List<Node> nodes = product.getNodes();
- for (int nodeIndex = 0; nodeIndex < nodes.size(); nodeIndex++) {
- final Node node = nodes.get(nodeIndex);
- int connectorPort = node.getWebPort();
- int jmxPort = pickJmxPort(product, connectorPort);
- final Map<String, String> finalSystemProperties = addJmxProperties(systemProperties.get(nodeIndex), jmxPort);
- startNode(product, node, app, finalSystemProperties, jmxPort);
- }
- }
- @Nonnull
- private void startNode(final Product product,
- final Node node,
- final File app,
- final Map<String, String> systemProperties,
- final int jmxPort) throws MojoExecutionException {
- AntJavaExecutorThread thread = startJavaThread(product, node, app, systemProperties);
- waitUntilReady(thread, jmxPort, product.getStartupTimeout());
- }
- private static Map<String, String> addJmxProperties(Map<String, String> properties, int jmxPort) {
- Map<String, String> updatedProperties = new HashMap<>(properties);
- updatedProperties.put("com.sun.management.jmxremote.authenticate", "false");
- updatedProperties.put("com.sun.management.jmxremote.port", String.valueOf(jmxPort));
- updatedProperties.put("com.sun.management.jmxremote.ssl", "false");
- // On Java 8u102 and newer, setting jmxremote.host will cause JMX to only listen on localhost. In
- // addition, rmi.server.hostname is explicitly set to localhost to match. Without both connecting
- // to JMX via TCP/IP fails in some environments.
- updatedProperties.put("com.sun.management.jmxremote.host", "127.0.0.1");
- updatedProperties.put("java.rmi.server.hostname", "127.0.0.1");
- return updatedProperties;
- }
- private static String fixSlashes(String path) {
- return path.replaceAll("\\\\", "/");
- }
- /**
- * Gets the local loopback address.
- * <p>
- * This method exists because {@link InetAddress#getLoopbackAddress()} may return an IPv6-style loopback address
- * on systems which support IPv6. Since {@link #addJmxProperties} explicitly configures RMI to run on 127.0.0.1,
- * and {@link #JMX_URL_FORMAT} is hard-coded for the same, we always want an IPv4-style address. If that address
- * fails for any reason, we fall back on {@link InetAddress#getLoopbackAddress()}.
- *
- * @return the loopback address
- * @since 6.3.4
- */
- private static InetAddress getLoopbackAddress() {
- try {
- return InetAddress.getByAddress("localhost", new byte[]{0x7f, 0x00, 0x00, 0x01});
- } catch (UnknownHostException e) {
- return InetAddress.getLoopbackAddress();
- }
- }
- private static boolean isSpringBoot(Product product) {
- return new DefaultArtifactVersion(product.getVersion()).compareTo(FIRST_SPRING_BOOT_VERSION) >= 0;
- }
- /**
- * Normalizes Tomcat's range of supported {@code CertificateVerification} settings to their Spring Boot
- * {@code Ssl} equivalents.
- * <p>
- * Note: Spring Boot's {@code ClientAuth} enumeration does not support Tomcat's {@code OPTIONAL_NO_CA}
- * setting. If that is the requested value, client auth will not be enabled.
- *
- * @param value the Tomcat value to map
- * @return the mapped Spring Boot value, which may be {@code empty()} if client auth should not be configured
- */
- private static Optional<String> normalizeClientAuth(String value) {
- switch (defaultString(value)) {
- case "need": // Tomcat doesn't support this. It's allowed because Spring Boot does
- case "require":
- case "required":
- case "true":
- case "yes":
- return of("need");
- case "optional":
- case "want":
- return of("want");
- default:
- return empty();
- }
- }
- private JMXConnector createConnector(int jmxPort) throws IOException {
- JMXServiceURL serviceURL = new JMXServiceURL(format(JMX_URL_FORMAT, jmxPort));
- return JMXConnectorFactory.connect(serviceURL);
- }
- private int pickJmxPort(Product product, int connectorPort) throws MojoExecutionException {
- // If the configured HTTP port is the default JMX port, skip the default and select a random port.
- // Checking if the port is available will likely succeed, but startup would still fail because JMX
- // would take the port before the HTTP connector was opened
- int jmxPort = pickFreePort(DEFAULT_JMX_PORT == connectorPort ? 0 : DEFAULT_JMX_PORT, getLoopbackAddress());
- if (jmxPort != DEFAULT_JMX_PORT) {
- // If the default JMX port wasn't available, write the randomly-selected port to a file in the
- // product's base directory. This makes it available later when the product is stopped
- Path jmxFile = getBaseDirectory(product).toPath().resolve(JMX_PORT_FILE);
- try {
- Files.write(jmxFile, String.valueOf(jmxPort).getBytes(StandardCharsets.UTF_8));
- } catch (IOException e) {
- // If the port file cannot be written, it won't be possible to shut the product down later
- // if it's started, so fail instead
- throw new MojoExecutionException("JMX port " + DEFAULT_JMX_PORT + " is not available, and the " +
- "automatically-selected replacement could not be written to " + jmxFile.toAbsolutePath(), e);
- }
- }
- return jmxPort;
- }
- private int readJmxPort(Product product) throws MojoExecutionException {
- Path jmxFile = getBaseDirectory(product).toPath().resolve(JMX_PORT_FILE);
- try (BufferedReader reader = Files.newBufferedReader(jmxFile, StandardCharsets.UTF_8)) {
- return Integer.parseInt(reader.readLine());
- } catch (FileNotFoundException | NoSuchFileException e) {
- // If the JMX port was not written to a file, assume the default is in use
- return DEFAULT_JMX_PORT;
- } catch (IOException e) {
- throw new MojoExecutionException("The JMX port could not be read from " + jmxFile.toAbsolutePath(), e);
- } catch (NumberFormatException e) {
- throw new MojoExecutionException("The JMX port in " + jmxFile.toAbsolutePath() + " is not valid", e);
- }
- }
- private AntJavaExecutorThread startJavaThread(
- final Product product,
- final Node node,
- final File app,
- final Map<String, String> properties) {
- Java java = taskFactory.newJavaTask(JavaTaskFactory.output(product.getOutput()).systemProperties(properties));
- // Set the unpacked application directory as the classpath. This will allow Java to find the
- // WarLauncher, which in turn will use the manifest to assemble the real classpath
- java.createClasspath().createPathElement().setLocation(app);
- java.createJvmarg().setLine(product.getJvmArgs());
- java.createJvmarg().setLine(node.getDebugArgs()); // If debug args aren't set, nothing happens
- java.createArg().setValue("--server.port=" + node.getWebPort());
- if (product.isHttps()) {
- // Configure SSL
- java.createArg().setValue("--server.ssl.enabled=true");
- java.createArg().setValue("--server.ssl.key-alias=" + product.getHttpsKeyAlias());
- java.createArg().setValue("--server.ssl.key-password=" + product.getHttpsKeystorePass());
- java.createArg().setValue("--server.ssl.key-store=" + product.getHttpsKeystoreFile());
- java.createArg().setValue("--server.ssl.key-store-password=" + product.getHttpsKeystorePass());
- java.createArg().setValue("--server.ssl.protocol=" + product.getHttpsSSLProtocol());
- // Client auth requires special handling, to map from the various values Tomcat accepts to
- // their equivalent Spring Boot value. Tomcat's native values aren't supported
- normalizeClientAuth(product.getHttpsClientAuth())
- .ifPresent(clientAuth -> java.createArg().setValue("--server.ssl.client-auth=" + clientAuth));
- }
- // Set the context path for the application
- java.createArg().setValue("--server.contextPath=" + product.getContextPath());
- // Enable Spring Boot's admin JMX endpoints, which can be used to wait for the application
- // to start and to shut it down gracefully later
- java.createArg().setValue("--spring.application.admin.enabled=true");
- java.createArg().setValue("--spring.application.admin.jmx-name=" + ADMIN_OBJECT_NAME);
- java.setClassname("org.springframework.boot.loader.WarLauncher");
- AntJavaExecutorThread javaThread = new AntJavaExecutorThread(java);
- javaThread.start();
- return javaThread;
- }
- private void waitUntilReady(AntJavaExecutorThread javaThread, int jmxPort, int wait) throws MojoExecutionException {
- long timeout = System.currentTimeMillis() + wait;
- while (System.currentTimeMillis() < timeout) {
- if (javaThread.isFinished()) {
- throw new MojoExecutionException("Bitbucket Server failed to start", javaThread.getBuildException());
- }
- try (JMXConnector connector = createConnector(jmxPort)) {
- MBeanServerConnection connection = connector.getMBeanServerConnection();
- Boolean ready = (Boolean) connection.getAttribute(new ObjectName(ADMIN_OBJECT_NAME), "Ready");
- if (TRUE.equals(ready)) {
- return;
- }
- } catch (AttributeNotFoundException e) {
- // Unexpected change to Spring Boot's "Admin" MXBean?
- throw new MojoExecutionException(ADMIN_OBJECT_NAME + " has no \"Ready\" attribute", e);
- } catch (InstanceNotFoundException e) {
- log.debug("Spring Boot administration for Bitbucket Server is not available yet");
- } catch (ReflectionException e) {
- throw new MojoExecutionException("Failed to retrieve \"Ready\" attribute", e);
- } catch (IOException e) {
- boolean rethrow = true;
- Throwable t = e;
- while (t != null) {
- if (t instanceof ConnectException) {
- log.debug("Bitbucket Server's MBeanServer is not available yet");
- rethrow = false;
- t = null;
- } else {
- t = t.getCause();
- }
- }
- if (rethrow) {
- throw new MojoExecutionException("Could not be connect to Bitbucket Server via JMX", e);
- }
- } catch (Exception e) {
- throw new MojoExecutionException(e.getMessage(), e);
- }
- try {
- log.debug("Waiting to retry");
- Thread.sleep(500L);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new IllegalStateException("Interrupted while waiting for Bitbucket Server to start");
- }
- }
- // If we make it here, startup timed out. Try to interrupt the process's thread, to trigger the
- // application to be shutdown, and then throw
- javaThread.interrupt();
- throw new MojoExecutionException("Timed out waiting for Bitbucket Server to start");
- }
- private static class BitbucketPluginProvider extends AbstractPluginProvider {
- @Override
- protected Collection<ProductArtifact> getSalArtifacts(String salVersion) {
- return emptyList();
- }
- }
- }