org.springframework.ide.eclipse.boot.dash.test.mocks.MockCloudFoundryClientFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.ide.eclipse.boot.dash.test.mocks.MockCloudFoundryClientFactory.java

Source

/*******************************************************************************
 * 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
 * https://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.IOException;
import java.nio.file.Files;
import java.util.Collection;
import java.util.Collections;
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 java.util.stream.Collectors;

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.CFDomainType;
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.SshHost;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v2.CFCloudDomainData;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v2.CFDomainStatus;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v2.CFPushArguments;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.console.IApplicationLogConsole;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.routes.ParsedUri;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.routes.RouteBinding;
import org.springframework.ide.eclipse.boot.dash.test.CfTestTargetParams;
import org.springframework.ide.eclipse.boot.dash.test.util.LiveExpToFlux;
import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken;
import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

import junit.framework.Assert;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public class MockCloudFoundryClientFactory extends CloudFoundryClientFactory {

    private static void debug(String string) {
        System.out.println(string);
    }

    private final Set<MockClient> instances = Collections.synchronizedSet(new HashSet<>());

    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, CFCloudDomainData> 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 CFCloudDomain defDomain(String name) {
        CFCloudDomainData it = new CFCloudDomainData(name);
        domainsByName.put(name, it);
        return it;
    }

    public CFCloudDomain defDomain(String name, CFDomainType type, CFDomainStatus status) {
        CFCloudDomainData it = new CFCloudDomainData(name, type, status);
        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 String validPassword = FAKE_PASSWORD;

    /**
     * Change the current password this mock client will accept when trying to
     * use password-based authentication.
     */
    public void setPassword(String newPassword) {
        this.validPassword = newPassword;
    }

    private class MockClient implements ClientRequests {

        private class MockSshClientSupport implements SshClientSupport {

            @Override
            public SshHost getSshHost() throws Exception {
                return new SshHost("ssh.host.somewhere", 2222, "some-ssh-fingerprint");
            }

            @Override
            public String getSshUser(String appName, int instance) throws Exception {
                MockCFApplication app = getSpace().getApplication(appName);
                if (app == null) {
                    throw new IOException("App not found");
                }
                UUID guid = app.getGuid();
                Assert.assertNotNull(guid);
                return getSshUser(guid, instance);
            }

            @Override
            public String getSshCode() throws Exception {
                return "an-ssh-code";
            }

            @Override
            public String getSshUser(UUID appGuid, int instance) throws Exception {
                return appGuid + "/" + instance;
            }

        }

        private int nextPort = 63000;

        private synchronized int choosePort() {
            return nextPort++;
        }

        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;
            instances.add(this);
            debug("created Mock CF Client: " + instances.size());
            refreshToken.addListener((e, v) -> debug("refreshToken <- " + v));
            refreshToken.onDispose(d -> debug("refreshToken DISPOSED"));
        }

        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 Disposable 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 dispose() {
            connected = false;
            refreshToken.dispose();
            instances.remove(this);
            debug("Mock CF Client disposed: " + instances.size());
        }

        @Override
        public SshClientSupport getSshClientSupport() throws Exception {
            return new MockSshClientSupport();
        }

        @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(validPassword)) {
                    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.setRoutes(buildRoutes(args));

            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.setHealthCheckHttpEndpoint(args.getHealthCheckHttpEndpoint());
            app.setBits(() -> {
                try {
                    return Files.readAllBytes(args.getApplicationDataAsFile().toPath());
                } catch (IOException e) {
                    return new byte[0];
                }
            });
            space.put(app);
            space.getPushCount(app.getName()).increment();

            app.start(cancelationToken);
        }

        private Collection<RouteBinding> buildRoutes(CFPushArguments args) {
            List<String> desiredUris = args.getRoutes();
            if (desiredUris != null) {
                return desiredUris.stream().map(uri -> buildRoute(uri, args)).collect(Collectors.toList());
            }
            return ImmutableList.of();
        }

        private RouteBinding buildRoute(String _uri, CFPushArguments args) {
            ParsedUri uri = new ParsedUri(_uri);
            boolean randomRoute = args.getRandomRoute();
            CFCloudDomainData bestDomain = domainsByName.values().stream()
                    .filter(domain -> domainCanBeUsedFor(domain, uri))
                    .max((d1, d2) -> Integer.compare(d1.getName().length(), d2.getName().length())).orElse(null);
            if (bestDomain == null) {
                throw new IllegalStateException("No domain matching the given uri '" + _uri + "' could be found");
            }
            RouteBinding route = new RouteBinding();
            route.setDomain(bestDomain.getName());
            route.setHost(bestDomain.splitHost(uri.getHostAndDomain()));
            route.setPath(uri.getPath());
            route.setPort(uri.getPort());
            if (randomRoute) {
                if (bestDomain.getType() == CFDomainType.TCP) {
                    if (route.getPort() == null) {
                        route.setPort(choosePort());
                    }
                } else if (bestDomain.getType() == CFDomainType.HTTP) {
                    if (route.getHost() == null) {
                        route.setHost(chooseHost());
                    }
                }
            }
            return route;
        }

        private String chooseHost() {
            return RandomStringUtils.randomAlphabetic(8).toLowerCase();
        }

        private boolean domainCanBeUsedFor(CFCloudDomainData domainData, ParsedUri uri) {
            String domain = domainData.getName();
            String hostAndDomain = uri.getHostAndDomain();
            String host;
            if (!hostAndDomain.endsWith(domain)) {
                return false;
            }
            if (domain.length() == hostAndDomain.length()) {
                //The uri matches domain precisely
                host = null;
            } else if (hostAndDomain.charAt(hostAndDomain.length() - domain.length() - 1) == '.') {
                //THe uri matches as ${host}.${domain}
                host = hostAndDomain.substring(0, hostAndDomain.length() - domain.length() - 1);
            } else {
                //Couldn't match this domain to uri
                return false;
            }
            if (domainData.getType() == CFDomainType.TCP) {
                return host == null; //TCP routes don't allow setting a host, only a port
            } else if (domainData.getType() == CFDomainType.HTTP) {
                return uri.getPort() == null; //HTTP routes don't allow setting a port only a host
            } else {
                throw new IllegalStateException("Unknown domain type: " + domainData.getType());
            }
        }

        @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();
        }

        @Override
        public Flux<String> getRefreshTokens() {
            return LiveExpToFlux.toFlux(refreshToken);
        }
    }

    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);
    }

    public int instanceCount() {
        return instances.size();
    }

    public void changeRefrestToken(String newToken) {
        for (MockClient client : instances) {
            client.refreshToken.setValue(newToken);
        }
    }

}