/* * 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.local; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import java.util.stream.Collectors; import org.hamcrest.BaseMatcher; import org.hamcrest.CoreMatchers; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.local.LocalAppDeployerIntegrationTests.Config; import org.springframework.cloud.deployer.spi.test.AbstractAppDeployerIntegrationTests; import org.springframework.cloud.deployer.spi.test.AbstractIntegrationTests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.util.FileSystemUtils; import org.springframework.web.client.RestTemplate; import static org.hamcrest.CoreMatchers.anyOf; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.springframework.cloud.deployer.spi.app.DeploymentState.deployed; import static org.springframework.cloud.deployer.spi.app.DeploymentState.unknown; import static org.springframework.cloud.deployer.spi.test.EventuallyMatcher.eventually; /** * Integration tests for {@link LocalAppDeployer}. * * Now supports running with Docker images for tests, just set this env var: * * SPRING_CLOUD_DEPLOYER_SPI_TEST_USE_DOCKER=true * * @author Eric Bottard * @author Mark Fisher * @author Oleg Zhurakousky * @author Janne Valkealahti * @author Ilayaperumal Gopinathan */ @SpringBootTest(classes = {Config.class, AbstractIntegrationTests.Config.class}, value = { "maven.remoteRepositories.springRepo.url=https://repo.spring.io/libs-snapshot" }) public class LocalAppDeployerIntegrationTests extends AbstractAppDeployerIntegrationTests { private static final String TESTAPP_DOCKER_IMAGE_NAME = "springcloud/spring-cloud-deployer-spi-test-app:latest"; @Autowired private AppDeployer appDeployer; @Value("${spring-cloud-deployer-spi-test-use-docker:false}") private boolean useDocker; @Override protected AppDeployer provideAppDeployer() { return appDeployer; } @Override protected Resource testApplication() { if (useDocker) { log.info("Using Docker image for tests"); return new DockerResource(TESTAPP_DOCKER_IMAGE_NAME); } return super.testApplication(); } @Override protected String randomName() { if (LocalDeployerUtils.isWindows()) { // tweak random dir name on win to be shorter String uuid = UUID.randomUUID().toString(); long l = ByteBuffer.wrap(uuid.toString().getBytes()).getLong(); return name.getMethodName() + Long.toString(l, Character.MAX_RADIX); } else { return super.randomName(); } } @Test public void testEnvVariablesInheritedViaEnvEndpoint() { if (useDocker) { // would not expect to be able to check anything on docker return; } Map<String, String> properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); 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)); Map<String, AppInstanceStatus> instances = appDeployer().status(deploymentId).getInstances(); String url = null; if (instances.size() == 1) { url = instances.entrySet().iterator().next().getValue().getAttributes().get("url"); } String env = null; if (url != null) { RestTemplate template = new RestTemplate(); env = template.getForObject(url + "/actuator/env", String.class); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); assertThat(url, notNullValue()); if (LocalDeployerUtils.isWindows()) { // windows is weird, we may still get Path or PATH assertThat(env, anyOf(containsString("\"Path\""), containsString("\"PATH\""))); } else { assertThat(env, containsString("\"PATH\"")); // we're defaulting to SAJ so it's i.e. // instance.index not INSTANCE_INDEX assertThat(env, containsString("\"instance.index\"")); assertThat(env, containsString("\"spring.application.index\"")); assertThat(env, containsString("\"spring.cloud.application.guid\"")); assertThat(env, containsString("\"spring.cloud.stream.instanceIndex\"")); } } @Test public void testAppLogRetrieval() { Map<String, String> properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); 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)); String logContent = appDeployer().getLog(deploymentId); assertThat(logContent, containsString("Starting DeployerIntegrationTestApplication")); } // TODO: remove when these two are forced in tck tests @Test public void testScale() { doTestScale(false); } @Test public void testScaleWithIndex() { doTestScale(true); } @Test public void testFailureToCallShutdownOnUndeploy() { Map<String, String> properties = new HashMap<>(); // disable shutdown endpoint properties.put("endpoints.shutdown.enabled", "false"); AppDefinition definition = new AppDefinition(randomName(), properties); 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)); } @Test // was triggered by GH-50 and subsequently GH-55 public void testNoStdoutStderrOnInheritLoggingAndNoNPEOnGetAttributes() { Map<String, String> properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap(LocalDeployerProperties.INHERIT_LOGGING, "true")); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); AppStatus appStatus = deployer.status(deploymentId); assertTrue(appStatus.getInstances().size() > 0); for (Entry<String, AppInstanceStatus> instanceStatusEntry : appStatus.getInstances().entrySet()) { Map<String, String> attributes = instanceStatusEntry.getValue().getAttributes(); assertFalse(attributes.containsKey("stdout")); assertFalse(attributes.containsKey("stderr")); } deployer.undeploy(deploymentId); } @Test public void testInDebugModeWithSuspended() throws Exception { Map<String, String> properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); Map<String, String> deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.DEBUG_PORT, "9999"); deploymentProperties.put(LocalDeployerProperties.DEBUG_SUSPEND, "y"); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); Thread.sleep(5000); AppStatus appStatus = deployer.status(deploymentId); if (resource instanceof DockerResource) { try { String containerId = getCommandOutput("docker ps -q --filter ancestor="+ TESTAPP_DOCKER_IMAGE_NAME); String logOutput = getCommandOutput("docker logs "+ containerId); assertTrue(logOutput.contains("Listening for transport dt_socket at address: 9999")); } catch (IOException e) { } } else { assertEquals("deploying", appStatus.toString()); } deployer.undeploy(deploymentId); } @Test public void testInDebugModeWithSuspendedUseCamelCase() throws Exception { Map<String, String> properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); Map<String, String> deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".debugPort", "8888"); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".debugSuspend", "y"); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".inheritLogging", "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); Thread.sleep(5000); AppStatus appStatus = deployer.status(deploymentId); if (resource instanceof DockerResource) { try { String containerId = getCommandOutput("docker ps -q --filter ancestor="+ TESTAPP_DOCKER_IMAGE_NAME); String logOutput = getCommandOutput("docker logs "+ containerId); assertTrue(logOutput.contains("Listening for transport dt_socket at address: 8888")); } catch (IOException e) { } } else { assertEquals("deploying", appStatus.toString()); } deployer.undeploy(deploymentId); } @Test public void testUseDefaultDeployerProperties() throws IOException { LocalDeployerProperties localDeployerProperties = new LocalDeployerProperties(); Path tmpPath = new File(System.getProperty("java.io.tmpdir")).toPath(); Path customWorkDirRoot = tmpPath.resolve("test-default-directory"); localDeployerProperties.setWorkingDirectoriesRoot(customWorkDirRoot.toFile().getAbsolutePath()); // Create a new LocalAppDeployer using a working directory that is different from the default value. AppDeployer appDeployer = new LocalAppDeployer(localDeployerProperties); List<Path> beforeDirs = getBeforePaths(customWorkDirRoot); Map<String, String> properties = new HashMap<>(); properties.put("server.port", "0"); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); // Deploy String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); assertThat(deploymentId, eventually(hasStatusThat( appDeployer, Matchers.<AppStatus>hasProperty("state", is(deployed))), timeout.maxAttempts, timeout.pause)); timeout = undeploymentTimeout(); // Undeploy appDeployer.undeploy(deploymentId); assertThat(deploymentId, eventually(hasStatusThat( appDeployer, Matchers.<AppStatus>hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause)); List<Path> afterDirs = getAfterPaths(customWorkDirRoot); assertThat("Additional working directory not created", afterDirs.size(), CoreMatchers.is(beforeDirs.size()+1)); } protected Matcher<String> hasStatusThat(final AppDeployer appDeployer, final Matcher<AppStatus> statusMatcher) { return new BaseMatcher<String>() { private AppStatus status; public boolean matches(Object item) { this.status = appDeployer.status((String)item); return statusMatcher.matches(this.status); } public void describeMismatch(Object item, Description mismatchDescription) { mismatchDescription.appendText("status of ").appendValue(item).appendText(" "); statusMatcher.describeMismatch(this.status, mismatchDescription); } public void describeTo(Description description) { statusMatcher.describeTo(description); } }; } @Test public void testZeroPortReportsDeployed() throws IOException { Map<String, String> properties = new HashMap<>(); properties.put("server.port", "0"); Path tmpPath = new File(System.getProperty("java.io.tmpdir")).toPath(); Path customWorkDirRoot = tmpPath.resolve("spring-cloud-deployer-app-workdir"); Map<String, String> deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".working-directories-root", customWorkDirRoot.toFile().getAbsolutePath()); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); List<Path> beforeDirs = getBeforePaths(customWorkDirRoot); 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)); List<Path> afterDirs = getAfterPaths(customWorkDirRoot); assertThat("Additional working directory not created", afterDirs.size(), CoreMatchers.is(beforeDirs.size()+1)); } private List<Path> getAfterPaths(Path customWorkDirRoot) throws IOException { if (!Files.exists(customWorkDirRoot)) { return new ArrayList<>(); } return Files.walk(customWorkDirRoot, 1) .filter(path -> Files.isDirectory(path)) .filter(path -> !path.getFileName().toString().startsWith(".")) .collect(Collectors.toList()); } private List<Path> getBeforePaths(Path customWorkDirRoot) throws IOException { List<Path> beforeDirs = new ArrayList<>(); beforeDirs.add(customWorkDirRoot); if (Files.exists(customWorkDirRoot)) { beforeDirs = Files.walk(customWorkDirRoot, 1) .filter(path -> Files.isDirectory(path)) .collect(Collectors.toList()); } return beforeDirs; } private String getCommandOutput(String cmd) throws IOException { Process process = Runtime.getRuntime().exec(cmd); BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); return stdInput.lines().findFirst().get(); } @Configuration @EnableConfigurationProperties(LocalDeployerProperties.class) public static class Config { @Bean public AppDeployer appDeployer(LocalDeployerProperties properties) { return new LocalAppDeployer(properties); } } }