/* * Copyright 2016-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.cloud.deployer.spi.test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.test.app.DeployerIntegrationTestProperties; import org.springframework.core.io.Resource; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.everyItem; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.springframework.cloud.deployer.spi.app.DeploymentState.deployed; import static org.springframework.cloud.deployer.spi.app.DeploymentState.deploying; import static org.springframework.cloud.deployer.spi.app.DeploymentState.failed; import static org.springframework.cloud.deployer.spi.app.DeploymentState.partial; import static org.springframework.cloud.deployer.spi.app.DeploymentState.unknown; import static org.springframework.cloud.deployer.spi.test.EventuallyMatcher.eventually; /** * Abstract base class for integration tests of * {@link org.springframework.cloud.deployer.spi.app.AppDeployer} * implementations. * <p> * Inheritors should setup an environment with a newly created * {@link org.springframework.cloud.deployer.spi.app.AppDeployer}. * Tests in this class are independent and leave the * deployer in a clean state after they successfully run. * </p> * <p> * As deploying an application is often quite time consuming, some tests assert * various aspects of deployment in a row, to avoid re-deploying apps over and * over again. * </p> * * @author Eric Bottard * @author Mark Fisher * @author Greg Turnquist * @author David Turanski */ public abstract class AbstractAppDeployerIntegrationTests extends AbstractIntegrationTests { private AppDeployerWrapper deployerWrapper; /** * To be implemented by subclasses, which should return the instance of AppDeployer that needs * to be tested. If subclasses decide to add additional implementation-specific tests, they should * interact with the deployer through {@link #appDeployer()}, and not directly via a field or a call * to this method. */ protected abstract AppDeployer provideAppDeployer(); /** * Subclasses should call this method to interact with the AppDeployer under test. * Returns a wrapper around the deployer returned by {@link #provideAppDeployer()}, that keeps * track of which apps have been deployed and undeployed. */ protected AppDeployer appDeployer() { return deployerWrapper; } @Before public void wrapDeployer() { deployerWrapper = new AppDeployerWrapper(provideAppDeployer()); } @After public void cleanupLingeringApps() { for (String id : deployerWrapper.deployments) { try { log.warn("Test named {} left behind an app for deploymentId '{}', trying to cleanup", name.getMethodName(), id); deployerWrapper.wrapped.undeploy(id); } catch (Exception e) { log.warn("Exception caught while trying to cleanup '{}'. Moving on...", id); } } } @Test public void testUnknownDeployment() { String unknownId = randomName(); AppStatus status = appDeployer().status(unknownId); assertThat(status.getDeploymentId(), is(unknownId)); assertThat("The map was not empty: " + status.getInstances(), status.getInstances().isEmpty(), is(true)); assertThat(status.getState(), is(unknown)); } /** * Tests a simple deploy-undeploy cycle. */ @Test public void testSimpleDeployment() { AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(deployed))), timeout.maxAttempts, timeout.pause)); log.info("Deploying {} again...", request.getDefinition().getName()); try { appDeployer().deploy(request); fail("Should have thrown an IllegalStateException"); } catch (IllegalStateException ok) { } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); try { appDeployer().undeploy(deploymentId); fail("Should have thrown an IllegalStateException"); } catch (IllegalStateException ok) { } } /** * An app deployer should be able to re-deploy an application after it has been un-deployed. * This test makes sure the deployer does not leave things lying around for example. */ @Test public void testRedeploy() { AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(deployed))), timeout.maxAttempts, timeout.pause)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); // Optionally pause before re-using request try { Thread.sleep(redeploymentPause()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } log.info("Deploying {} again...", request.getDefinition().getName()); // Attempt re-deploy of SAME request deploymentId = appDeployer().deploy(request); timeout = deploymentTimeout(); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(deployed))), timeout.maxAttempts, timeout.pause)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); } /** * Tests that an app which takes a long time to deploy is correctly reported as deploying. * Test that such an app can be undeployed. */ @Test public void testDeployingStateCalculationAndCancel() { Map<String, String> properties = new HashMap<>(); properties.put("initDelay", "" + 1000 * 60 * 60); // 1hr AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, properties); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(deploying))), timeout.maxAttempts, timeout.pause)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); } @Test public void testFailedDeployment() { Map<String, String> properties = new HashMap<>(); properties.put("killDelay", "0"); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, properties); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(failed))), timeout.maxAttempts, timeout.pause)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); } /** * Tests that properties (key-value mappings) can be passed to a deployed app, * including values that typically require special handling. */ @Test public void testApplicationPropertiesPassing() { Map<String, String> properties = new HashMap<>(); properties.put("parameterThatMayNeedEscaping", DeployerIntegrationTestProperties.FUNNY_CHARACTERS); AppDefinition definition = new AppDefinition(randomName(), properties); Map<String, String> deploymentProperties = new HashMap<>(); // This makes sure that deploymentProperties are not passed to the deployed app itself deploymentProperties.put("killDelay", "0"); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), deploymentProperties); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(deployed))), timeout.maxAttempts, timeout.pause)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); // This second pass makes sure that properties are indeed passed properties.put("parameterThatMayNeedEscaping", "notWhatIsExpected"); definition = new AppDefinition(randomName(), properties); request = new AppDeploymentRequest(definition, testApplication(), deploymentProperties); log.info("Deploying {}, expecting it to fail...", request.getDefinition().getName()); deploymentId = appDeployer().deploy(request); timeout = deploymentTimeout(); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(failed))), timeout.maxAttempts, timeout.pause)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); } /** * Tests that command line arguments (ordered strings) can be passed to a deployed app, * including values that typically require special handling. */ @Test public void testCommandLineArgumentsPassing() { Map<String, String> properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Map<String, String> deploymentProperties = new HashMap<>(); List<String> cmdLineArgs = Arrays.asList("--commandLineArgValueThatMayNeedEscaping=" + DeployerIntegrationTestProperties.FUNNY_CHARACTERS); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), deploymentProperties, cmdLineArgs); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(deployed))), timeout.maxAttempts, timeout.pause)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); // This second pass makes sure that commandLine args are indeed understood properties = new HashMap<>(); definition = new AppDefinition(randomName(), properties); deploymentProperties = new HashMap<>(); cmdLineArgs = Arrays.asList("--commandLineArgValueThatMayNeedEscaping=" + "notWhatIsExpected"); request = new AppDeploymentRequest(definition, testApplication(), deploymentProperties, cmdLineArgs); log.info("Deploying {}, expecting it to fail...", request.getDefinition().getName()); deploymentId = appDeployer().deploy(request); timeout = deploymentTimeout(); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(failed))), timeout.maxAttempts, timeout.pause)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); } /** * Tests support for instance count support and individual instance status report. */ @Test public void testMultipleInstancesDeploymentAndPartialState() { Map<String, String> appProperties = new HashMap<>(); appProperties.put("matchInstances", "1"); // Only instance n°1 will kill itself appProperties.put("killDelay", "0"); AppDefinition definition = new AppDefinition(randomName(), appProperties); Resource resource = testApplication(); Map<String, String> deploymentProperties = new HashMap<>(); deploymentProperties.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); deploymentProperties.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(partial))), timeout.maxAttempts, timeout.pause)); // Assert individual instance state // Note we can't rely on instances order, neither on their id indicating their ordinal number List<DeploymentState> individualStates = new ArrayList<>(); for (AppInstanceStatus status : appDeployer().status(deploymentId).getInstances().values()) { individualStates.add(status.getState()); } assertThat(individualStates, containsInAnyOrder( is(deployed), is(deployed), is(failed) )); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); } /** * Tests support for DeployerEnvironmentInfo is implemented. */ @Test public void testEnvironmentInfo() { RuntimeEnvironmentInfo info = appDeployer().environmentInfo(); assertNotNull(info.getImplementationVersion()); assertNotNull(info.getPlatformType()); assertNotNull(info.getPlatformClientVersion()); assertNotNull(info.getPlatformHostVersion()); } @Test @Ignore("Disabled pending the implementation of this feature.") public void testScale() { doTestScale(false); } @Test @Ignore("Disabled pending the implementation of this feature.") public void testScaleWithIndex() { doTestScale(true); } protected void doTestScale(Boolean indexed) { final int DESIRED_COUNT = 3; Map<String, String> deploymentProperties = Collections.singletonMap(AppDeployer.INDEXED_PROPERTY_KEY, indexed.toString()); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); log.info("Deploying {} index={}...", request.getDefinition().getName(), indexed); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(deployed))), timeout.maxAttempts, timeout.pause)); log.info("Scaling {} to {} instances...", request.getDefinition().getName(), DESIRED_COUNT); appDeployer().scale(new AppScaleRequest(deploymentId, DESIRED_COUNT)); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(deployed))), timeout.maxAttempts, timeout.pause)); assertThat(deploymentId, eventually(appInstanceCount(is(DESIRED_COUNT)), timeout.maxAttempts, timeout.pause)); List<DeploymentState> individualStates = new ArrayList<>(); for (AppInstanceStatus status : appDeployer().status(deploymentId).getInstances().values()) { individualStates.add(status.getState()); } assertThat(individualStates, everyItem(is(deployed))); log.info("Scaling {} from {} to 1 instance...", request.getDefinition().getName(), DESIRED_COUNT); appDeployer().scale(new AppScaleRequest(deploymentId, 1)); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(deployed))), timeout.maxAttempts, timeout.pause)); assertThat(deploymentId, eventually(appInstanceCount(is(1)), timeout.maxAttempts, timeout.pause)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); } /** * A Hamcrest Matcher that queries the number of app instances for some app id. * @param countMatcher number of app instances to match. */ protected Matcher<String> appInstanceCount(final Matcher<Integer> countMatcher) { return AppCountMatcher.hasAppCount(countMatcher, appDeployer()); } /** * A Hamcrest Matcher that queries the deployment status for some app id. * * @author Eric Bottard */ protected Matcher<String> hasStatusThat(final Matcher<AppStatus> statusMatcher) { return new BaseMatcher<String>() { private AppStatus status; @Override public boolean matches(Object item) { status = appDeployer().status((String) item); return statusMatcher.matches(status); } @Override public void describeMismatch(Object item, Description mismatchDescription) { mismatchDescription.appendText("status of ").appendValue(item).appendText(" "); statusMatcher.describeMismatch(status, mismatchDescription); } @Override public void describeTo(Description description) { statusMatcher.describeTo(description); } }; } /** * A decorator for AppDeployer that keeps track of deployed/undeployed apps. * * @author Eric Bottard */ protected static class AppDeployerWrapper implements AppDeployer { private final AppDeployer wrapped; private final Set<String> deployments = new LinkedHashSet<>(); public AppDeployerWrapper(AppDeployer wrapped) { this.wrapped = wrapped; } @Override public String deploy(AppDeploymentRequest request) { String deploymentId = wrapped.deploy(request); deployments.add(deploymentId); return deploymentId; } @Override public void undeploy(String id) { wrapped.undeploy(id); deployments.remove(id); } @Override public AppStatus status(String id) { return wrapped.status(id); } @Override public RuntimeEnvironmentInfo environmentInfo() { return wrapped.environmentInfo(); } @Override public String getLog(String id) { return wrapped.getLog(id); } @Override public void scale(AppScaleRequest appScaleRequest) { wrapped.scale(appScaleRequest); } } }