/**
* Copyright (C) 2015 Orange
* 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
* http://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 com.francetelecom.clara.cloud.activation.plugin.cf.infrastructure;
import com.francetelecom.clara.cloud.commons.MavenReference;
import com.francetelecom.clara.cloud.commons.TechnicalException;
import com.francetelecom.clara.cloud.logicalmodel.samplecatalog.SampleAppProperties;
import com.francetelecom.clara.cloud.mvn.consumer.MvnRepoDao;
import com.francetelecom.clara.cloud.techmodel.cf.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.cloudfoundry.client.lib.CloudCredentials;
import org.cloudfoundry.client.lib.CloudFoundryException;
import org.cloudfoundry.client.lib.CloudFoundryOperations;
import org.cloudfoundry.client.lib.HttpProxyConfiguration;
import org.cloudfoundry.client.lib.domain.CloudApplication;
import org.cloudfoundry.client.lib.domain.CloudDomain;
import org.cloudfoundry.client.lib.domain.CloudRoute;
import org.cloudfoundry.client.lib.domain.CloudService;
import org.fest.assertions.Assertions;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import static org.fest.assertions.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/*
* Copyright 2009-2012 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
*
* http://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.
*/
/**
* Test class for V2 cloud controller.
*
* You need to create organization, space and user account via the portal and
* set these values using system properties.
*
* @author Thomas Risberg
*/
public abstract class AbstractCfAdapterIT {
public static final Logger logger = LoggerFactory.getLogger(AbstractCfAdapterIT.class);
@Autowired
private MvnRepoDao mvnRepoDao;
@Autowired
private SampleAppProperties sampleAppProperties;
@Autowired
@Qualifier("datacenter")
protected String datacenter;
protected String ccEmail;
protected CfAdapterImpl cfAdapter;
protected String cfSubdomain;
protected String cfDefaultSpace;
@Value("${cf.ccng.use_proxy}")
protected boolean isUsingHttpProxy;
@Value("${cf.ccng.proxyHost}")
protected String httpProxyHost;
@Value("${cf.ccng.proxyPort}")
protected int httpProxyPort;
private CloudFoundryOperations cfClient;
/**
* The default domain without owner, i.e. cfapps.io
*/
protected static String defaultDomainName = null;
protected static String defaultNamespace(String email) {
String name_without_domain = email.substring(0, email.indexOf('@')).replaceAll("\\.", "-").replaceAll("\\+", "-").replaceAll("_", "-");
// Keep it short, 5 chars max
name_without_domain = StringUtils.left(name_without_domain, 5);
return name_without_domain;
}
@Test
public void creates_the_requested_domain_if_missing() {
// given
String domainNameToCreate = "newdomain.tocreate.com";
try {
if (cfAdapter.domainExists(domainNameToCreate, cfDefaultSpace)) {
cfClient.deleteDomain(domainNameToCreate);
}
// when
cfAdapter.addDomain(domainNameToCreate, cfDefaultSpace);
// then
boolean domainWasCreated = cfAdapter.domainExists(domainNameToCreate, cfDefaultSpace);
assertThat(domainWasCreated).as(domainNameToCreate).isTrue();
} finally {
if (cfAdapter.domainExists(domainNameToCreate, cfDefaultSpace)) {
cfClient.deleteDomain(domainNameToCreate);
}
}
}
@Test
@Ignore("Re-enable if you need to debug jonas buildpack and compare against native buildpack")
public void provisions_starts_stops_deletes_a_small_war_in_tomcat() throws IOException {
MavenReference cfWicketJpa = sampleAppProperties.getMavenReference("cf-wicket-jpa", "war");
String testRequestPath = "/"; // default buildpacks "mount" any wars to
// ROOT
provisionStartStopDeletesApp(cfWicketJpa, getJavaBuildpackUrl(), getTestAppName() + "-" + "travel_test-" + "upload1", 512, testRequestPath);
}
@Test
@Ignore("Re-enable if you need to debug jeeprobe and compare against hello-env")
public void provisions_starts_stops_deletes_a_small_war_on_jonas() throws IOException {
MavenReference cfWicketJpa = sampleAppProperties.getMavenReference("cf-wicket-jpa", "war");
String testRequestPath = "app/"; // buildpacks "mount" any wars to
// app.war, and jonas exposes them
// as app
provisionStartStopDeletesApp(cfWicketJpa, getJonasBuildpackUrl(), getTestAppName() + "-" + "travel_test-" + "upload1", 512, testRequestPath);
}
@Test
public void provisions_starts_stops_deletes_jeeprobe_ear() throws IOException {
MavenReference jeeProbeMavenReference = sampleAppProperties.getMavenReference("jeeprobe","ear");
MavenReference jeeProbeEarRef = mvnRepoDao.resolveUrl(jeeProbeMavenReference);
String testRequestPath = "/jeeprobe/"; // JeeProbe EAR specified a
// context-root which is honored
// by Jonas
int ramMb = 1024; // temporary oversized to 1 Gb to workaround warden
// bug
provisionStartStopDeletesApp(jeeProbeEarRef, getJonasBuildpackUrl(), getTestAppName() + "-" + "jeeProbe", ramMb, testRequestPath);
}
@Test
public void should_create_then_delete_a_route() {
Space space = new Space();
Route route = new Route(new RouteUri("demo-elpaasofrontend13beta." + getTestDomainName()), "root1", space);
Assertions.assertThat(cfAdapter.routeExists(route, cfDefaultSpace)).isFalse();
cfAdapter.createRoute(route, cfDefaultSpace);
Assertions.assertThat(cfAdapter.routeExists(route, cfDefaultSpace)).isTrue();
cfAdapter.deleteRoute(route, cfDefaultSpace);
Assertions.assertThat(cfAdapter.routeExists(route, cfDefaultSpace)).isFalse();
}
@Test(expected=CloudFoundryException.class)
@Ignore
public void fail_to_create_2_routes_with_same_host_for_same_domain() {
final Space space = new Space();
Route route1 = new Route(new RouteUri("test-host-conflict." + getTestDomainName()), "root1", space);
Route route2 = new Route(new RouteUri("test-host-conflict." + getTestDomainName()), "root1", space);
cfAdapter.createRoute(route1, cfDefaultSpace);
cfAdapter.createRoute(route2, cfDefaultSpace);
//then it should fail
}
@Test
@Ignore("should be tested at upper level")
public void handles_uri_conflicts() {
MavenReference simpleProbe = mvnRepoDao.resolveUrl(sampleAppProperties.getMavenReference("simple-probe", "jar"));
Space space = new Space();
space.activate(new SpaceName(cfDefaultSpace));
final App cfApp = new App(space, getTestAppName(), simpleProbe, getJonasBuildpackUrl(), 512, 1);
Route route1 = new Route(new RouteUri("demo-elpaasofrontend13beta." + getTestDomainName()), "root1", space);
Route route2 = new Route(new RouteUri("demo-elpaasobackend13beta." + getTestDomainName()), "root2", space);
cfApp.mapRoute(route1);
cfApp.mapRoute(route2);
cfAdapter.createApp(cfApp, cfDefaultSpace);
CloudApplication app = cfClient.getApplication(cfApp.getAppName());
assertNotNull(app);
assertEquals(CloudApplication.AppState.STOPPED, app.getState());
for (String uri : cfApp.getRouteURIs()) {
assertThat(uri.startsWith("c")).isFalse(); // should not have uri
// conflicts on the
// first app.
}
// Try to provision a second app with with same params (name and uri)
final App secondApp = new App(space, getTestAppName() + "-2", cfApp.getAppBinaries(), cfApp.getBuildPackUrl(), cfApp.getRamMb(), 1);
secondApp.mapRoute(route1);
secondApp.mapRoute(route2);
cfAdapter.createApp(secondApp, cfDefaultSpace);
app = cfClient.getApplication(secondApp.getAppName());
assertNotNull(app);
assertEquals(CloudApplication.AppState.STOPPED, app.getState());
for (String uri : secondApp.getRouteURIs()) {
assertThat(uri).as("conflict-prefixed uri").startsWith("c");
}
}
private void provisionStartStopDeletesApp(MavenReference mavenReference, String buildpackUrl, String appName, int ramMb, String testRequestPath) throws IOException {
Space space = new Space();
App cfApp = new App(space, appName, mavenReference, buildpackUrl, ramMb, 1);
Route route = new Route(new RouteUri("webgui." + getTestDomainName()), testRequestPath, space);
cfApp.mapRoute(route);
// when
cfAdapter.createRoute(route, cfDefaultSpace);
// then route should have been created
assertThat(cfAdapter.routeExists(route, cfDefaultSpace)).isTrue();
// when
cfAdapter.createApp(cfApp, cfDefaultSpace);
// then app should have been created
assertThat(cfAdapter.appExists(appName, cfDefaultSpace)).isTrue();
// then app should be stopped
assertThat(cfAdapter.isAppStopped(appName, cfDefaultSpace)).isTrue();
cfAdapter.startApp(cfApp, cfDefaultSpace);
// because starting an app is async
pollAppStartStatus(cfApp);
// then app should be started
assertThat(cfAdapter.isAppStarted(appName, cfDefaultSpace)).isTrue();
String firstUri = cfApp.getRouteURIs().get(0);
try {
Map<String, String> logs = cfClient.getLogs(appName);
logger.info("logs for " + appName + "are:\n" + logs);
} catch (Exception e) {
logger.info("unable to get app log for " + appName, e);
}
testRemoteAppWebGui(firstUri, testRequestPath, appName);
// when
cfAdapter.stopApp(cfApp, cfDefaultSpace);
// then app should be stopped
assertThat(cfAdapter.isAppStopped(appName, cfDefaultSpace)).isTrue();
// when
cfAdapter.deleteApp(cfApp, cfDefaultSpace);
// then app should have been removed
assertThat(cfAdapter.appExists(appName, cfDefaultSpace)).isFalse();
// then domain should have been removed
assertThat(cfAdapter.domainExists(route.getDomain(), cfDefaultSpace)).isFalse();
// then route should have been removed
assertThat(cfAdapter.routeExists(route, cfDefaultSpace)).isFalse();
}
/**
*
* @param virtualHost
* @param testRequestPath
* @param appNameToDumpDiagnosticLogs
* the name of the app to display the logs of if the webgui is
* unreacheable, or null to no display such logs.
* @throws IOException
*/
protected void testRemoteAppWebGui(String virtualHost, String testRequestPath, String appNameToDumpDiagnosticLogs) throws IOException {
HttpClientConfig defaultProxyConfig = getHttpProxyConfigToQueryWebGuiRoutes();
int retry = 0;
int maxRetries = 10;
String testResponse = null;
StringBuffer testFailureDetails = new StringBuffer();
do {
logger.info("Querying " + getWebGuiURL(virtualHost, testRequestPath) + " using proxyConfig=" + defaultProxyConfig + " ...");
try {
testResponse = fetchRoutedContentAsString(getWebGuiURL(virtualHost, testRequestPath), defaultProxyConfig);
break;
} catch (HttpResponseException e) {
String msg = "Querying " + getWebGuiURL(virtualHost, testRequestPath) + " ... done. Caught: " + e;
logger.info(msg);
testFailureDetails.append(msg);
testFailureDetails.append("\n");
if (e.getStatusCode() == 404) {
logger.info("Sleeping for 10s before next retry (" + retry + "/" + maxRetries + ")");
if (appNameToDumpDiagnosticLogs != null) {
cfAdapter.logAppDiagnostics(appNameToDumpDiagnosticLogs, cfDefaultSpace);
}
try {
Thread.sleep(10 * 1000);
} catch (InterruptedException e1) {
// Ignore
}
retry++;
} else {
break; // no retries for unexpected errors
}
}
} while (retry < maxRetries);
logger.info("Querying " + getWebGuiURL(virtualHost, testRequestPath) + " ... done. Returned: " + StringUtils.abbreviate(testResponse, 50));
assertThat(testResponse).as("webGui response").isNotNull().isNotEmpty();
assertThat(retry).overridingErrorMessage("Expecting zero retries on webGui polling, got " + retry + " retries. Details:" + testFailureDetails).isLessThanOrEqualTo(1); // Expect
// the app to immediately return a valid response, retries are only here
// to help diagnostics
}
protected HttpClientConfig getHttpProxyConfigToQueryWebGuiRoutes() {
// By default consider routes are reacheable without proxies
HttpClientConfig defaultProxyConfig = new HttpClientConfig() {
@Override
public void applyConfig(DefaultHttpClient httpclient) {
}
@Override
public String toString() {
return "HttpClientConfig { direct connection, no proxy }";
}
};
return defaultProxyConfig;
}
interface HttpClientConfig {
void applyConfig(DefaultHttpClient httpclient);
}
private String fetchRoutedContentAsString(String uri, HttpClientConfig httpClientConfig) throws IOException {
String contentA;
DefaultHttpClient httpclient = new DefaultHttpClient();
if (httpClientConfig != null) {
httpClientConfig.applyConfig(httpclient);
}
try {
HttpGet httpget = new HttpGet(uri);
contentA = httpclient.execute(httpget, new BasicResponseHandler());
} finally {
httpclient.getConnectionManager().shutdown();
}
return contentA;
}
public String getTestAppName() {
return "AbstractCfAdapterIT-" + datacenter + "-" + defaultNamespace(ccEmail) + cfSubdomain;
}
public String getTestDomainName() {
return "cfconsummerit." + datacenter + "." + defaultNamespace(ccEmail) + "." + cfSubdomain; // lower
// case
}
protected String getWebGuiURL(String virtualHost, String testRequestPath) {
if (testRequestPath != null && testRequestPath.startsWith("/"))
return "http://" + virtualHost + testRequestPath;
else
return "http://" + virtualHost + "/" + testRequestPath;
}
public HttpProxyConfiguration httpProxyConfiguration() {
logger.info("cfAdapter Connection settings: isUsingHttpProxy=" + cfAdapter.isUsingHttpProxy + " httpProxyHost=" + cfAdapter.httpProxyHost + " httpProxyPort="
+ cfAdapter.httpProxyPort);
HttpProxyConfiguration httpProxyConfiguration;
if (cfAdapter.isUsingHttpProxy && (cfAdapter.httpProxyHost != null)) {
httpProxyConfiguration = new HttpProxyConfiguration(cfAdapter.httpProxyHost, cfAdapter.httpProxyPort);
} else {
httpProxyConfiguration = null;
}
return httpProxyConfiguration;
}
@Before
public void setUp() {
cfClient = CFClientFactory.login(new CloudCredentials(cfAdapter.getEmail(), cfAdapter.getPassword()), cfAdapter.getTarget(), cfAdapter.getSpace(), cfAdapter.getOrg(),
cfAdapter.getDomain(), cfAdapter.trustSelfSignedCerts, httpProxyConfiguration());
List<CloudApplication> applications = cfClient.getApplications();
for (CloudApplication application : applications) {
if (application.getName().contains(getTestAppName())) {
cfClient.deleteApplication(application.getName());
}
List<String> boundServices = application.getServices();
List<String> services = boundServices;
for (String service : services) {
cfClient.deleteService(service);
}
clearDomain(getTestDomainName(), true);
clearDomain(getDefaultDomain(), false);
}
cfAdapter.addDomain(getTestDomainName(), cfDefaultSpace);
}
private String getDefaultDomain() {
for (CloudDomain domain : cfClient.getDomainsForOrg()) {
if (domain.getOwner().getName().equals("none")) {
return domain.getName();
}
}
return null;
}
@After
public void tearDown() {
List<CloudApplication> cloudApps = cfClient.getApplications();
for (CloudApplication cloudApp : cloudApps) {
if (cloudApp.getName().contains(getTestAppName())) {
cfClient.deleteApplication(cloudApp.getName());
}
}
List<CloudService> cloudServices = cfClient.getServices();
for (CloudService cloudService : cloudServices) {
if (cloudService.getName().contains(getTestAppName())) {
cfClient.deleteService(cloudService.getName());
}
}
clearDomain(getTestDomainName(), true);
clearDomain(getDefaultDomain(), false);
cfClient.logout();
}
private void clearDomain(String domainToClear, boolean deleteDomain) {
for (CloudDomain domain : cfClient.getDomainsForOrg()) {
String domainName = domain.getName();
if (domainName.contains(domainToClear)) {
List<CloudRoute> routes = cfClient.getRoutes(domainName);
for (CloudRoute route : routes) {
cfClient.deleteRoute(route.getHost(), route.getDomain().getName());
}
if (deleteDomain) {
cfClient.deleteDomain(domainName);
}
}
}
}
public abstract String getJonasBuildpackUrl();
public abstract String getJavaBuildpackUrl();
private void pollAppStartStatus(App app) {
try {
int timeoutMs = 5 * 60 * 1000;
long start = System.currentTimeMillis();
boolean pass = false;
int i;
for (i = 0; i < 50 && pass == false; i++) {
int nbPass = cfAdapter.peekAppStartStatus(app.getInstanceCount(), app.getAppName(), cfDefaultSpace);
pass = (nbPass == app.getInstanceCount());
if (!pass) {
long elapsed = System.currentTimeMillis() - start;
if (elapsed > timeoutMs) {
logger.info("timeout waiting for app" + app.getAppName() + " to start: polled " + i + "times and waited " + elapsed + "ms (max is:" + timeoutMs + " ms)");
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// ignore
}
}
}
boolean startWasOk = pass;
if (!startWasOk) {
cfAdapter.logAppDiagnostics(app.getAppName(), cfDefaultSpace);
long elapsed = System.currentTimeMillis() - start;
throw new TechnicalException("Unable to successfully start " + app.getAppName() + ": polled " + i + " times and waited " + elapsed + " ms (max is:" + timeoutMs
+ " ms)");
} else {
logger.info("all " + app.getInstanceCount() + " instance(s) have properly started");
}
} catch (Exception e) {
throw new TechnicalException("unable to start app:" + app.getAppName(), e);
}
}
}