/*******************************************************************************
* Copyright (c) 2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.dash.test.mocks;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.RandomStringUtils;
import org.cloudfoundry.client.CloudFoundryClient;
import org.osgi.framework.Version;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFApplication;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFApplicationDetail;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFBuildpack;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFClientParams;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFCloudDomain;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFCredentials;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFCredentials.CFCredentialType;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFOrganization;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFServiceInstance;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFSpace;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFStack;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.ClientRequests;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CloudFoundryClientFactory;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.SshClientSupport;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v2.CFPushArguments;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v2.ReactorUtils;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.console.IApplicationLogConsole;
import org.springframework.ide.eclipse.boot.dash.test.CfTestTargetParams;
import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken;
import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil;
import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import reactor.core.Cancellation;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class MockCloudFoundryClientFactory extends CloudFoundryClientFactory {
public static final String FAKE_REFRESH_TOKEN = "fakeRefreshToken";
public static final String FAKE_PASSWORD = CfTestTargetParams.fromEnv("CF_TEST_PASSWORD");
private Version supportedApiVersion = new Version(CloudFoundryClient.SUPPORTED_API_VERSION);
private Version apiVersion = supportedApiVersion;
private Map<String, CFOrganization> orgsByName = new LinkedHashMap<>();
private Map<String, MockCFSpace> spacesByName = new LinkedHashMap<>();
private Map<String, MockCFDomain> domainsByName = new LinkedHashMap<>();
private Map<String, MockCFBuildpack> buildpacksByName = new LinkedHashMap<>();
private Map<String, MockCFStack> stacksByName = new LinkedHashMap<>();
private Set<String> ssoTokens = new HashSet<>();
/**
* Becomes non-null if notImplementedStub is called, used to check that the tests
* only use parts of the mocking harness that are actually implemented.
*/
private Exception notImplementedStubCalled = null;
private long startDelay = 0;
public MockCloudFoundryClientFactory() {
defDomain("cfmockapps.io"); //Lost of functionality may assume there's at least one domain so make sure we have one.
defBuildpacks("java-buildpack", "ruby-buildpack", "funky-buildpack", "another-buildpack");
defStacks("cflinuxfs2", "windows2012R2");
}
synchronized public String getSsoToken() {
String token = RandomStringUtils.randomAlphabetic(8);
ssoTokens.add(token);
return token;
}
/**
* Verfies the validity of a sso token. Sso token can only be used once
* so this check implicitly invalidates the token.
*
* @return Whether the token was valid prior to the call to this method.
*/
private synchronized boolean checkSsoToken(String token) {
return ssoTokens.remove(token);
}
public void defStacks(String... names) {
for (String n : names) {
defStack(n);
}
}
public MockCFStack defStack(String name) {
MockCFStack stack = new MockCFStack(name);
stacksByName.put(name, stack);
return stack;
}
@Override
public ClientRequests getClient(CFClientParams params) {
return new MockClient(params);
}
public MockCFDomain defDomain(String name) {
MockCFDomain it = new MockCFDomain(name);
domainsByName.put(name, it);
return it;
}
public String getDefaultDomain() {
return domainsByName.keySet().iterator().next();
}
public MockCFSpace defSpace(String orgName, String spaceName) {
String key = orgName+"/"+spaceName;
MockCFSpace existing = spacesByName.get(key);
if (existing==null) {
CFOrganization org = defOrg(orgName);
spacesByName.put(key, existing= new MockCFSpace(this,
spaceName,
UUID.randomUUID(),
org
));
}
return existing;
}
public CFOrganization defOrg(String orgName) {
CFOrganization existing = orgsByName.get(orgName);
if (existing==null) {
orgsByName.put(orgName, existing = new CFOrganizationData(
orgName,
UUID.randomUUID()
));
}
return existing;
}
public void assertOnlyImplementedStubsCalled() throws Exception {
if (notImplementedStubCalled!=null) {
throw notImplementedStubCalled;
}
}
private class MockClient implements ClientRequests {
private CFClientParams params;
private boolean connected = true;
private Boolean validCredentials = null;
private final LiveVariable<String> refreshToken = new LiveVariable<>();
public MockClient(CFClientParams params) {
this.params = params;
}
private void notImplementedStub() {
IllegalStateException e = new IllegalStateException("CF Client Stub Not Yet Implemented");
if (notImplementedStubCalled==null) {
notImplementedStubCalled = e;
}
throw e;
}
@Override
public Flux<CFApplicationDetail> getApplicationDetails(List<CFApplication> appsToLookUp) throws Exception {
checkConnection();
MockCFSpace space = getSpace();
return Flux.fromIterable(appsToLookUp)
.flatMap((app) -> {
return Mono.justOrEmpty(space.getApplication(app.getGuid()).getDetailedInfo());
});
}
@Override
public Cancellation streamLogs(String appName, IApplicationLogConsole logConsole) throws Exception {
checkConnection();
//TODO: This 'log streamer' is a total dummy for now. It doesn't stream any data and canceling it does nothing.
return Flux.empty().subscribe();
}
@Override
public void stopApplication(String appName) throws Exception {
checkConnection();
MockCFApplication app = getSpace().getApplication(appName);
if (app==null) {
throw errorAppNotFound(appName);
}
app.stop();
}
@Override
public void restartApplication(String appName, CancelationToken cancelationToken) throws Exception {
checkConnection();
MockCFApplication app = getSpace().getApplication(appName);
if (app==null) {
throw errorAppNotFound(appName);
}
app.restart(cancelationToken);
}
@Override
public void logout() {
connected = false;
}
@Override
public SshClientSupport getSshClientSupport() throws Exception {
notImplementedStub();
return null;
}
@SuppressWarnings("unchecked")
@Override
public List<CFSpace> getSpaces() throws Exception {
checkConnection();
@SuppressWarnings("rawtypes")
List hack = ImmutableList.copyOf(spacesByName.values());
return hack;
}
@Override
public List<CFServiceInstance> getServices() throws Exception {
checkConnection();
return getSpace().getServices();
}
private MockCFSpace getSpace() throws IOException {
if (params.getOrgName()==null) {
throw errorNoOrgSelected();
}
if (params.getSpaceName()==null) {
throw errorNoSpaceSelected();
}
MockCFSpace space = spacesByName.get(params.getOrgName()+"/"+params.getSpaceName());
if (space==null) {
throw errorSpaceNotFound(params.getOrgName()+"/"+params.getSpaceName());
}
return space;
}
@Override
public List<CFCloudDomain> getDomains() throws Exception {
checkConnection();
return ImmutableList.<CFCloudDomain>copyOf(domainsByName.values());
}
@Override
public List<CFBuildpack> getBuildpacks() throws Exception {
checkConnection();
return ImmutableList.<CFBuildpack>copyOf(buildpacksByName.values());
}
@Override
public List<CFApplication> getApplicationsWithBasicInfo() throws Exception {
checkConnection();
return getSpace().getApplicationsWithBasicInfo();
}
@Override
public CFApplicationDetail getApplication(String appName) throws Exception {
checkConnection();
MockCFApplication app = getSpace().getApplication(appName);
if (app!=null) {
return app.getDetailedInfo();
}
return null;
}
@Override
public Version getApiVersion() {
return apiVersion;
}
@Override
public Version getSupportedApiVersion() {
return supportedApiVersion;
}
@Override
public void deleteApplication(String name) throws Exception {
checkConnection();
if (!getSpace().removeApp(name)) {
throw errorAppNotFound(name);
}
}
@Override
public String getHealthCheck(UUID appGuid) throws Exception {
checkConnection();
MockCFApplication app = getApplication(appGuid);
if (app == null) {
throw errorAppNotFound("GUID: "+appGuid.toString());
} else {
return app.getHealthCheckType();
}
}
private MockCFApplication getApplication(UUID appGuid) throws IOException {
return getSpace().getApplication(appGuid);
}
/**
* Each mock operation that does something requires access to CF should call this
* to ensure that it implicitly check whether the connection is valid.
* <p>
* Operations on 'invalid' connection are expected to throw Exceptions.
* Calling this method makes the operations behave as expected. For example,
* fail when logged out, or when connection was created with invalid credentials.
*/
private void checkConnection() throws Exception {
if (!connected) {
throw errorClientNotConnected();
}
if (validCredentials==null) {
validCredentials = isValidCredentials(params.getUsername(), params.getCredentials());
}
if (!validCredentials) {
throw errorInvalidCredentials();
}
}
private boolean isValidCredentials(String username, CFCredentials credentials) throws Exception {
CFCredentialType type = credentials.getType();
String secret = credentials.getSecret();
if (type==CFCredentialType.PASSWORD) {
if (!credentials.getSecret().equals(FAKE_PASSWORD)) {
return false;
}
} else if (type==CFCredentialType.REFRESH_TOKEN) {
if (!secret.equals(FAKE_REFRESH_TOKEN)) {
return false;
}
} else if (type==CFCredentialType.TEMPORARY_CODE) {
if (!checkSsoToken(secret)) {
return false;
}
} else {
return false;
}
//Validation of credentials is expected to update refresh token.
refreshToken.setValue(FAKE_REFRESH_TOKEN);
return true;
}
@Override
public void setHealthCheck(UUID guid, String hcType) throws Exception {
checkConnection();
notImplementedStub();
}
@Override
public List<CFStack> getStacks() throws Exception {
checkConnection();
return ImmutableList.<CFStack>copyOf(stacksByName.values());
}
@Override
public boolean applicationExists(String appName) throws Exception {
checkConnection();
return getSpace().getApplication(appName) !=null;
}
@Override
public void push(CFPushArguments args, CancelationToken cancelationToken) throws Exception {
checkConnection();
System.out.println("Pushing: "+args);
//TODO: should check services exist and raise an error because non-existant services cannot be bound.
MockCFSpace space = getSpace();
MockCFApplication app = new MockCFApplication(MockCloudFoundryClientFactory.this, space, args.getAppName());
app.setBuildpackUrlMaybe(args.getBuildpack());
app.setUris(args.getRoutes());
app.setCommandMaybe(args.getCommand());
app.setDiskQuotaMaybe(args.getDiskQuota());
app.setEnvMaybe(args.getEnv());
app.setMemoryMaybe(args.getMemory());
app.setServicesMaybe(args.getServices());
app.setStackMaybe(args.getStack());
app.setTimeoutMaybe(args.getTimeout());
app.setHealthCheckTypeMaybe(args.getHealthCheckType());
app.setBits(IOUtil.toBytes(new FileInputStream(args.getApplicationData().getName())));
space.put(app);
space.getPushCount(app.getName()).increment();
app.start(cancelationToken);
}
@Override
public Map<String, String> getApplicationEnvironment(String appName) throws Exception {
checkConnection();
MockCFApplication app = getSpace().getApplication(appName);
if (app==null) {
throw errorAppNotFound(appName);
}
return ImmutableMap.copyOf(app.getEnv());
}
@Override
public Mono<Void> deleteServiceAsync(String serviceName) {
return Mono.defer(() -> {
try {
checkConnection();
getSpace().deleteService(serviceName);
return Mono.empty();
} catch (Exception e) {
return Mono.error(e);
}
});
}
@Override
public Mono<String> getUserName() {
return Mono.defer(() -> {
try {
checkConnection();
return Mono.just(params.getUsername());
} catch (Exception e) {
return Mono.error(e);
}
});
}
@Override
public String getRefreshToken() {
return refreshToken.getValue();
}
}
public void defBuildpacks(String... names) {
for (String n : names) {
defBuildpack(n);
}
}
public MockCFBuildpack defBuildpack(String n) {
MockCFBuildpack it = new MockCFBuildpack(n);
buildpacksByName.put(n, it);
return it;
}
//////////////////////////////////////////////////
// Exception creation methods
protected IOException errorAppNotFound(String detailMessage) throws IOException {
return new IOException("App not found: "+detailMessage);
}
protected IOException errorClientNotConnected() {
return new IOException("CF Client not Connected");
}
protected IOException errorNoOrgSelected() {
return new IOException("No org selected");
}
protected IOException errorNoSpaceSelected() {
return new IOException("No space selected");
}
protected IOException errorSpaceNotFound(String detail) {
return new IOException("Space not found: "+detail);
}
protected IOException errorAppAlreadyExists(String detail) {
return new IOException("App already exists: "+detail);
}
protected Exception errorInvalidCredentials() {
return new Exception("Cannot connect to CF. Invalid credentials.");
}
public void setAppStartDelay(TimeUnit timeUnit, int howMany) {
startDelay = timeUnit.toMillis(howMany);
}
/**
* @return The delay that a simulated 'start' of an app should take before returning. Given in milliseconds.
*/
public long getStartDelay() {
return startDelay;
}
public void setApiVersion(String string) {
apiVersion = new Version(string);
}
public void setSupportedApiVersion(String string) {
supportedApiVersion = new Version(string);
}
}