Added CF callable context to better handle client errors

Also added junits to commons-cf that test the error handling.
This commit is contained in:
nsingh
2017-01-31 15:52:04 -08:00
parent 039c954df1
commit de5226ff61
14 changed files with 400 additions and 49 deletions

View File

@@ -37,6 +37,13 @@
<groupId>io.projectreactor.ipc</groupId>
<artifactId>reactor-netty</artifactId>
<version>${reactor-netty}</version>
</dependency>
<!-- Test harness -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,66 @@
/*******************************************************************************
* Copyright (c) 2017 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.vscode.commons.cloudfoundry.client.cftarget;
import java.net.UnknownHostException;
import java.util.concurrent.Callable;
import org.cloudfoundry.uaa.UaaException;
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import reactor.ipc.netty.channel.AbortedException;
public class CFCallableContext {
public static final String UNAUTHORIZED_ERROR = "unauthorized";
private final CFParamsProviderMessages paramsProviderMessages;
private Throwable lastConnectionError;
public CFCallableContext(CFParamsProviderMessages paramsProviderMessages) {
this.paramsProviderMessages = paramsProviderMessages;
}
public <T> T checkConnection(Callable<T> callable) throws Exception {
this.lastConnectionError = null;
try {
return callable.call();
} catch (Exception e) {
throw convertToCfVscodeError(e);
}
}
private Exception convertToCfVscodeError(Exception e) {
Throwable deepestCause = ExceptionUtil.getDeepestCause(e);
if (deepestCause instanceof UaaException) {
String error = ((UaaException) deepestCause).getError();
if (error != null && error.contains(UNAUTHORIZED_ERROR)) {
this.lastConnectionError = deepestCause;
return new UnauthorizedException(this.paramsProviderMessages.unauthorised());
}
} else if (deepestCause instanceof AbortedException) {
// This one is odd. It is thrown when a wrong token is specified, but
// instead of getting an expected UaaException, this AbortedException is thrown instead by reactor netty.
this.lastConnectionError = deepestCause;
return new UnauthorizedException(this.paramsProviderMessages.unauthorised());
} else if (deepestCause instanceof UnknownHostException) {
this.lastConnectionError = deepestCause;
String message = ExceptionUtil.getMessage(deepestCause);
return new ConnectionException(this.paramsProviderMessages.noNetworkConnection() + " " + message);
}
return e;
}
public boolean hasConnectionError() {
return this.lastConnectionError != null;
}
}

View File

@@ -0,0 +1,26 @@
/*******************************************************************************
* Copyright (c) 2017 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.vscode.commons.cloudfoundry.client.cftarget;
/**
* Messages that are specific to a particular parameter provider
*
*/
public interface CFParamsProviderMessages {
String noTargetsFound();
String unauthorised();
String noNetworkConnection();
}

View File

@@ -39,12 +39,13 @@ public class CFTarget {
*/
private final LoadingCache<String, List<CFBuildpack>> buildpacksCache;
private final LoadingCache<String, List<CFServiceInstance>> servicesCache;
private Throwable lastCFFailure;
private CFCallableContext callableContext;
public CFTarget(String targetName, CFClientParams params, ClientRequests requests) {
public CFTarget(String targetName, CFClientParams params, ClientRequests requests, CFCallableContext callableContext) {
this.params = params;
this.requests = requests;
this.targetName = targetName;
this.callableContext = callableContext;
CacheLoader<String, List<CFServiceInstance>> servicesLoader = new CacheLoader<String, List<CFServiceInstance>>() {
@Override
@@ -73,24 +74,11 @@ public class CFTarget {
}
protected <T> T runAndCheckForFailure(Callable<T> callable) throws Exception {
this.lastCFFailure = null;
try {
return callable.call();
} catch (Exception e) {
if (isAcceptableCFFailure(e)) {
this.lastCFFailure = e;
}
throw e;
}
return callableContext.checkConnection(callable);
}
public boolean hasCFFailure() {
return this.lastCFFailure != null;
}
protected boolean isAcceptableCFFailure(Throwable e) {
// TODO: this is too broad. Be more specific: e.g. look for IOException, etc..
return e != null;
public boolean hasConnectionError() {
return callableContext.hasConnectionError();
}
public CFClientParams getParams() {

View File

@@ -28,9 +28,11 @@ public class CFTargetCache {
private final CloudFoundryClientFactory clientFactory;
private final ClientTimeouts timeouts;
private final LoadingCache<ClientParamsCacheKey, CFTarget> cache;
private final CFCallableContext callableContext;
public static final long SERVICES_EXPIRATION = 10;
public static final long TARGET_EXPIRATION = 1;
public CFTargetCache(ClientParamsProvider paramsProvider, CloudFoundryClientFactory clientFactory,
ClientTimeouts timeouts) {
@@ -39,6 +41,7 @@ public class CFTargetCache {
this.paramsProvider = paramsProvider;
this.clientFactory = clientFactory;
this.timeouts = timeouts;
this.callableContext = new CFCallableContext(paramsProvider.getMessages());
CacheLoader<ClientParamsCacheKey, CFTarget> loader = new CacheLoader<ClientParamsCacheKey, CFTarget>() {
@Override
@@ -58,6 +61,10 @@ public class CFTargetCache {
* for any other error encountered
*/
public synchronized List<CFTarget> getOrCreate() throws NoTargetsException, Exception {
return callableContext.checkConnection(() -> doGetOrCreate());
}
protected synchronized List<CFTarget> doGetOrCreate() throws NoTargetsException, Exception {
List<CFClientParams> allParams = paramsProvider.getParams();
List<CFTarget> targets = new ArrayList<>();
@@ -67,7 +74,7 @@ public class CFTargetCache {
CFTarget target = cache.get(key);
if (target != null) {
// If any CF errors occurred in the target, refresh once
if (target.hasCFFailure()) {
if (target.hasConnectionError()) {
cache.refresh(key);
target = cache.get(key);
}
@@ -80,7 +87,7 @@ public class CFTargetCache {
}
protected CFTarget create(CFClientParams params) throws Exception {
return new CFTarget(getTargetName(params), params, clientFactory.getClient(params, timeouts));
return new CFTarget(getTargetName(params), params, clientFactory.getClient(params, timeouts), callableContext);
}
protected static String getTargetName(CFClientParams params) {

View File

@@ -29,11 +29,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
*/
public class CfCliParamsProvider implements ClientParamsProvider {
/*
* Important: be sure to use ':' to separate the initial part of the message with a longer portion. The vscode
* content assist will parse around the first ':' and the second segment will appear as a doc string that can be longer
*/
public static final String NO_CLI_TARGETS_FOUND_MESSAGE = "No Cloud Foundry targets found: Use CF CLI to login";
public static final String TARGET = "Target";
public static final String REFRESH_TOKEN = "RefreshToken";
@@ -41,6 +36,8 @@ public class CfCliParamsProvider implements ClientParamsProvider {
public static final String SPACE_FIELDS = "SpaceFields";
public static final String NAME = "Name";
public static final String SSL_DISABLED = "SSLDisabled";
private CfCliProviderMessages cfCliProviderMessages = new CfCliProviderMessages();
/*
* (non-Javadoc)
@@ -79,8 +76,7 @@ public class CfCliParamsProvider implements ClientParamsProvider {
}
if (params.isEmpty()) {
throw new NoTargetsException(
NO_CLI_TARGETS_FOUND_MESSAGE);
throw new NoTargetsException(getMessages().noTargetsFound());
} else {
return params;
}
@@ -109,4 +105,9 @@ public class CfCliParamsProvider implements ClientParamsProvider {
return System.getProperty("user.home");
}
@Override
public CFParamsProviderMessages getMessages() {
return cfCliProviderMessages;
}
}

View File

@@ -0,0 +1,39 @@
/*******************************************************************************
* Copyright (c) 2017 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.vscode.commons.cloudfoundry.client.cftarget;
public final class CfCliProviderMessages implements CFParamsProviderMessages {
/*
* Important: be sure to use ':' to separate the initial part of the message
* with a longer portion. The vscode content assist will parse around the
* first ':' and the second segment will appear as a doc string that can be
* longer
*/
public static final String NO_CLI_TARGETS_FOUND_MESSAGE = "No Cloud Foundry targets found: Use cf CLI to login";
public static final String UNAUTHORISED_MESSAGE = "Unauthorized access: Use cf CLI to login";
public static final String NO_NETWORK_CONNECTION = "Unable to connect to network: Verify network connection";
@Override
public String noTargetsFound() {
return NO_CLI_TARGETS_FOUND_MESSAGE;
}
@Override
public String unauthorised() {
return UNAUTHORISED_MESSAGE;
}
@Override
public String noNetworkConnection() {
return NO_NETWORK_CONNECTION;
}
}

View File

@@ -22,4 +22,6 @@ public interface ClientParamsProvider {
* @throws ExecutionException if failure occurs while resolving params
*/
List<CFClientParams> getParams() throws NoTargetsException, ExecutionException;
CFParamsProviderMessages getMessages();
}

View File

@@ -0,0 +1,24 @@
/*******************************************************************************
* Copyright (c) 2017 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.vscode.commons.cloudfoundry.client.cftarget;
public class ConnectionException extends Exception {
/**
*
*/
private static final long serialVersionUID = 1L;
public ConnectionException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,23 @@
/*******************************************************************************
* Copyright (c) 2017 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.vscode.commons.cloudfoundry.client.cftarget;
public class UnauthorizedException extends Exception {
/**
*
*/
private static final long serialVersionUID = 1L;
public UnauthorizedException(String message) {
super(message);
}
}

View File

@@ -10,39 +10,114 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.cloudfoundry.client;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import java.util.List;
import java.net.UnknownHostException;
import java.util.concurrent.Callable;
import org.junit.Ignore;
import org.cloudfoundry.uaa.UaaException;
import org.junit.Before;
import org.junit.Test;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFParamsProviderMessages;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFTarget;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfCliParamsProvider;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.ClientParamsProvider;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFTargetCache;
import org.springframework.ide.vscode.commons.cloudfoundry.client.v2.DefaultCloudFoundryClientFactoryV2;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfCliProviderMessages;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.ConnectionException;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.NoTargetsException;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.UnauthorizedException;
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import reactor.ipc.netty.channel.AbortedException;
public class CFClientTest {
/*
* Not meant to be run in a build yet as there is no CF harness to read CF
* params. Just keeping this tests for local development.
*/
@Ignore
MockCfCli cloudfoundry = new MockCfCli();
ClientTimeouts timeouts = new ClientTimeouts();
CFTargetCache targetCache;
CFParamsProviderMessages expectedMessages = new CfCliProviderMessages();
@Before
public void setup() throws Exception {
targetCache = new CFTargetCache(cloudfoundry.paramsProvider, cloudfoundry.factory, timeouts);
}
@Test
public void testFromCliParamsTarget() throws Exception {
public void testNoTarget() throws Exception {
ClientParamsProvider cliProvider = new CfCliParamsProvider();
CloudFoundryClientFactory clientFactory = DefaultCloudFoundryClientFactoryV2.INSTANCE;
ClientTimeouts timeouts = ClientTimeouts.DEFAULT_TIMEOUTS;
when(cloudfoundry.paramsProvider.getParams())
.thenThrow(new NoTargetsException(expectedMessages.noTargetsFound()));
assertError(() -> targetCache.getOrCreate(), NoTargetsException.class, expectedMessages.noTargetsFound());
}
CFTargetCache targetCache = new CFTargetCache(cliProvider, clientFactory, timeouts);
@Test
public void testOneTarget() throws Exception {
CFTarget target = targetCache.getOrCreate().get(0);
assertNotNull(target);
assertEquals(MockCfCli.DEFAULT_PARAMS, target.getParams());
}
List<CFBuildpack> buildPacks = target.getBuildpacks();
assertTrue(!buildPacks.isEmpty());
List<CFServiceInstance> services = target.getServices();
assertTrue(!services.isEmpty());
@Test
public void testUnknownHostServices() throws Exception {
ClientRequests client = cloudfoundry.client;
when(client.getServices()).thenThrow(new UnknownHostException("api.run.pivotal.io"));
CFTarget target = targetCache.getOrCreate().get(0);
assertError(() -> target.getServices(), ConnectionException.class, expectedMessages.noNetworkConnection());
}
@Test
public void testUnknownHostBuildpacks() throws Exception {
ClientRequests client = cloudfoundry.client;
when(client.getBuildpacks()).thenThrow(new UnknownHostException("api.run.pivotal.io"));
CFTarget target = targetCache.getOrCreate().get(0);
assertError(() -> target.getBuildpacks(), ConnectionException.class, expectedMessages.noNetworkConnection());
}
@Test
public void testAbortedExceptionServices() throws Exception {
ClientRequests client = cloudfoundry.client;
when(client.getServices()).thenThrow(new AbortedException("connection aborted"));
CFTarget target = targetCache.getOrCreate().get(0);
assertError(() -> target.getServices(), UnauthorizedException.class, expectedMessages.unauthorised());
}
@Test
public void testAbortedExceptionBuildpacks() throws Exception {
ClientRequests client = cloudfoundry.client;
when(client.getBuildpacks()).thenThrow(new AbortedException("connection aborted"));
CFTarget target = targetCache.getOrCreate().get(0);
assertError(() -> target.getBuildpacks(), UnauthorizedException.class, expectedMessages.unauthorised());
}
@Test
public void testUaaExceptionServices() throws Exception {
ClientRequests client = cloudfoundry.client;
when(client.getServices()).thenThrow(new UaaException(401, "unauthorized", "Bad credentials"));
CFTarget target = targetCache.getOrCreate().get(0);
assertError(() -> target.getServices(), UnauthorizedException.class, expectedMessages.unauthorised());
}
@Test
public void testUaaExceptionBuildpacks() throws Exception {
ClientRequests client = cloudfoundry.client;
when(client.getBuildpacks()).thenThrow(new UaaException(401, "unauthorized", "Bad credentials"));
CFTarget target = targetCache.getOrCreate().get(0);
assertError(() -> target.getBuildpacks(), UnauthorizedException.class, expectedMessages.unauthorised());
}
protected void assertError(Callable<?> callable, Class<? extends Throwable> expected, String expectedMessage)
throws Exception {
Throwable error = null;
try {
callable.call();
} catch (Exception e) {
error = ExceptionUtil.getDeepestCause(e);
}
assertEquals(expected, error.getClass());
assertTrue(ExceptionUtil.getMessageNoAppendedInformation(error).contains(expectedMessage));
}
}

View File

@@ -0,0 +1,61 @@
/*******************************************************************************
* Copyright (c) 2017 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.vscode.commons.cloudfoundry.client;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.mockito.Mockito;
import org.springframework.ide.vscode.commons.cloudfoundry.client.ClientRequests;
import org.springframework.ide.vscode.commons.cloudfoundry.client.CloudFoundryClientFactory;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFClientParams;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFCredentials;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfCliProviderMessages;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.ClientParamsProvider;
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import com.google.common.collect.ImmutableList;
public class MockCfCli {
public final static CFClientParams DEFAULT_PARAMS = new CFClientParams("test.io", "testuser",
CFCredentials.fromRefreshToken("refreshtoken"), false);
public final CloudFoundryClientFactory factory = mock(CloudFoundryClientFactory.class);
public final ClientRequests client = mock(ClientRequests.class);
public final ClientParamsProvider paramsProvider = mock(ClientParamsProvider.class);
public final CfCliProviderMessages actualCfCliMessages = new CfCliProviderMessages();
public MockCfCli() {
try {
//program some default behavior into mocks... most tests will use this.
//other tests should 'reset' the mocks and reprogram them as needed.
when(factory.getClient(any(), any())).thenReturn(client);
when(paramsProvider.getParams()).thenReturn(ImmutableList.of(DEFAULT_PARAMS));
when(paramsProvider.getMessages()).thenReturn(actualCfCliMessages);
} catch (Exception e) {
throw ExceptionUtil.unchecked(e);
}
}
/**
* Reset the mocks. Use this if the default's programmed into the mocks don't suite your test case.
* <p>
* Note: you may also choose to call {@link Mockito}.mock directly if you do not want to
* reset all of the mocks.
*/
public void reset() throws Exception {
Mockito.reset(factory, client, paramsProvider);
}
}

View File

@@ -13,6 +13,7 @@ package org.springframework.ide.vscode.commons.util;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
@@ -56,6 +57,22 @@ public class ExceptionUtil {
}
return null;
}
/**
* Given an exception, find if any of the exception types to look for is contained in the given exception
* @param e
* @param toLookFor non-null list of exception types to look for
* @return exception of specified type, if found, or null if not found
*/
public static Throwable findThrowable(Throwable e, List<Class<? extends Throwable>> toLookFor) {
for (Class<? extends Throwable> klass : toLookFor) {
Throwable found = getThrowable(e, klass);
if (found != null) {
return found;
}
}
return null;
}
public static String getMessage(Throwable e) {
// The message of nested exception is usually more interesting than the

View File

@@ -19,11 +19,15 @@ import java.util.logging.Logger;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFTarget;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFTargetCache;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.NoTargetsException;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.UnauthorizedException;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.ConnectionException;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import org.springframework.ide.vscode.commons.util.ValueParseException;
import org.springframework.ide.vscode.commons.yaml.schema.YValueHint;
import com.google.common.collect.ImmutableList;
public abstract class AbstractCFHintsProvider implements Callable<Collection<YValueHint>> {
public static final String EMPTY_VALUE = "";
@@ -59,12 +63,12 @@ public abstract class AbstractCFHintsProvider implements Callable<Collection<YVa
// resolving the error message. Instead, log the full error, and
// only throw a
// new exception with a "nicer" message
Throwable noTargetsError = ExceptionUtil.getThrowable(e, NoTargetsException.class);
if (noTargetsError != null) {
Throwable errorNoAppending = getErrorNoAppending(e);
if (errorNoAppending != null) {
// Do not log the no-targets exception as it may be encountered
// frequently
// if a user does not have a CF client installed
throw new ValueParseException(ExceptionUtil.getMessageNoAppendedInformation(noTargetsError));
throw new ValueParseException(ExceptionUtil.getMessageNoAppendedInformation(errorNoAppending));
} else {
// Log any other error
logger.log(Level.SEVERE, ExceptionUtil.getMessage(e), e);
@@ -74,6 +78,17 @@ public abstract class AbstractCFHintsProvider implements Callable<Collection<YVa
}
}
/**
*
* @param e
* @return an error that requires no additional information when showing its
* message, or null if no such error is found
*/
protected Throwable getErrorNoAppending(Throwable e) {
return ExceptionUtil.findThrowable(e,
ImmutableList.of(NoTargetsException.class, ConnectionException.class, UnauthorizedException.class));
}
/**
*
* @return non-null list of hints. Return empty if no hints available