PT 156579665 - Improvements in handling CF targets in manifest LS

Changes to how CF targets are handled in manifest LS, including better
error handling and association with a source type, like cf CLI or boot
dashboard.
This commit is contained in:
nsingh
2018-06-25 20:07:13 -07:00
parent cc9d1858f3
commit 990c8aae26
14 changed files with 314 additions and 242 deletions

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 2018 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
@@ -15,6 +15,7 @@ import java.net.UnknownHostException;
import java.util.concurrent.Callable;
import org.cloudfoundry.uaa.UaaException;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfo.TargetDiagnosticMessages;
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import reactor.ipc.netty.channel.AbortedException;
@@ -25,15 +26,15 @@ import reactor.ipc.netty.channel.AbortedException;
*/
public class CFCallableContext {
private final CFParamsProviderMessages paramsProviderMessages;
private final TargetDiagnosticMessages diagnosticMessages;
private Exception lastConnectionError;
private long lastErrorTime = 0;
public CFCallableContext(CFParamsProviderMessages paramsProviderMessages) {
this.paramsProviderMessages = paramsProviderMessages;
public CFCallableContext(TargetDiagnosticMessages diagnosticMessages) {
this.diagnosticMessages = diagnosticMessages;
}
public <T> T checkConnection(Callable<T> callable) throws Exception {
public <T> T run(Callable<T> callable) throws Exception {
this.lastConnectionError = null;
try {
return callable.call();
@@ -56,9 +57,12 @@ public class CFCallableContext {
Throwable deepestCause = ExceptionUtil.getDeepestCause(e);
if (deepestCause instanceof UaaException || deepestCause instanceof AbortedException
|| deepestCause instanceof SocketException || deepestCause instanceof UnknownHostException
&& this.paramsProviderMessages != null) {
return new ConnectionException(this.paramsProviderMessages.noNetworkConnection());
|| deepestCause instanceof SocketException || deepestCause instanceof UnknownHostException) {
if (this.diagnosticMessages != null) {
return new ConnectionException(this.diagnosticMessages.getConnectionError());
} else {
return new ConnectionException(CfTargetsInfoProvder.DEFAULT_MESSAGES.getConnectionError());
}
}
return null;
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 2018 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
@@ -44,6 +44,7 @@ public class CFTarget {
private LoadingCache<String, List<CFDomain>> domainCache;
private LoadingCache<String, List<CFStack>> stacksCache;
private CFCallableContext callableContext;
public CFTarget(String targetName, CFClientParams params, ClientRequests requests,
CFCallableContext callableContext) {
this.params = params;
@@ -107,7 +108,7 @@ public class CFTarget {
}
protected <T> T runAndCheckForFailure(Callable<T> callable) throws Exception {
return callableContext.checkConnection(callable);
return callableContext.run(callable);
}
public boolean hasExpiredConnectionError() {

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 2018 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
@@ -12,57 +12,64 @@ package org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.cloudfoundry.client.ClientTimeouts;
import org.springframework.ide.vscode.commons.cloudfoundry.client.CloudFoundryClientFactory;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfo.TargetDiagnosticMessages;
import org.springframework.ide.vscode.commons.util.StringUtil;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
public class CFTargetCache {
private final CfClientConfig cfClientConfig;
private Logger logger = LoggerFactory.getLogger(CFTargetCache.class);
private final CloudFoundryClientFactory clientFactory;
private final ClientTimeouts timeouts;
private LoadingCache<ClientParamsCacheKey, CFTarget> cache;
private CFCallableContext cacheCallableContext;
private List<ClientParamsProvider> _providers;
public static final Duration SERVICES_EXPIRATION = Duration.ofSeconds(10);
public static final Duration TARGET_EXPIRATION = Duration.ofHours(1);
public static final Duration ERROR_EXPIRATION = Duration.ofSeconds(10);
public CFTargetCache(CfClientConfig cfClientConfig, CloudFoundryClientFactory clientFactory,
public CFTargetCache(List<ClientParamsProvider> providers, CloudFoundryClientFactory clientFactory,
ClientTimeouts timeouts) {
Assert.isLegal(cfClientConfig != null,
"A Cloud Foundry client parameters provider must be set when creating a target cache.");
this.cfClientConfig = cfClientConfig;
this.clientFactory = clientFactory;
this.timeouts = timeouts;
//TODO: I suspect that addClientParamsProviderChangedListener below is not necessary.
// I think it results in unnessary refreshes of the cache, any time the providers are
// changed. The cached results doesn't really depend on the providers, only on the targets. So I think,
// it shouldn't need to refresh when the providers are changed.
cfClientConfig.addClientParamsProviderChangedListener((newProvider, oldProvider) -> initCache());
this._providers = providers;
initCache();
}
private void initCache() {
CacheLoader<ClientParamsCacheKey, CFTarget> loader = new CacheLoader<ClientParamsCacheKey, CFTarget>() {
@Override
public CFTarget load(ClientParamsCacheKey params) throws Exception {
return create(params.fullParams);
public CFTarget load(ClientParamsCacheKey key) throws Exception {
return create(key.fullParams, key.getProvider());
}
};
cache = CacheBuilder.newBuilder()./*maximumSize(1).*/expireAfterAccess(TARGET_EXPIRATION.toMillis(), TimeUnit.MILLISECONDS)
.build(loader);
this.cacheCallableContext = new CFCallableContext(cfClientConfig.getClientParamsProvider().getMessages());
}
/**
*
* @param providers list of providers that will be called in order.
*/
public synchronized void setProviders(ClientParamsProvider... providers) {
this._providers = providers != null ? Arrays.asList(providers) : ImmutableList.of();
}
/**
@@ -73,39 +80,67 @@ public class CFTargetCache {
* for any other error encountered
*/
public synchronized List<CFTarget> getOrCreate() throws NoTargetsException, Exception {
return cacheCallableContext.checkConnection(() -> doGetOrCreate());
}
protected synchronized List<CFTarget> doGetOrCreate() throws NoTargetsException, Exception {
Collection<CFClientParams> allParams = cfClientConfig.getClientParamsProvider().getParams();
// Obtain an uptodate list of params from the providers and refresh the list of targets.
List<CFTarget> targets = new ArrayList<>();
if (allParams != null) {
for (CFClientParams params : allParams) {
ClientParamsCacheKey key = ClientParamsCacheKey.from(params);
CFTarget target = cache.get(key);
if (target != null) {
// If any CF errors occurred in the target, refresh once
if (target.hasExpiredConnectionError()) {
cache.refresh(key);
target = cache.get(key);
}
targets.add(target);
}
Exception lastError = null;
for (ClientParamsProvider provider : this._providers) {
// IMPORTANT: do not let errors stop iterating through all the providers.
// If one provider cannot provide targets, try the next one.
try {
Collection<CFClientParams> providerParams = provider.getParams();
getTargets(targets, provider, providerParams);
} catch (Exception e) {
lastError = e;
}
}
if (targets.isEmpty() && lastError != null) {
throw lastError;
}
return targets;
}
private void getTargets(List<CFTarget> targets, ClientParamsProvider provider,
Collection<CFClientParams> providerParams) throws ExecutionException {
for (CFClientParams params : providerParams) {
ClientParamsCacheKey key = ClientParamsCacheKey.from(params, provider);
CFTarget target = cache.get(key);
if (target != null) {
// If any CF errors occurred in the target, refresh once
if (target.hasExpiredConnectionError()) {
cache.refresh(key);
target = cache.get(key);
}
targets.add(target);
}
}
}
protected CFTarget create(CFClientParams params) throws Exception {
/*
* Must pass a NEW callable context. Cannot be
* the same as the target cache callable context, as
* contexts may contain error state
*/
return new CFTarget(getTargetName(params), params, clientFactory.getClient(params, timeouts),
new CFCallableContext(cfClientConfig.getClientParamsProvider().getMessages()));
public synchronized List<ClientParamsProvider> getParamsProviders() {
return this._providers;
}
protected CFTarget create(CFClientParams params, ClientParamsProvider provider) throws Exception {
TargetDiagnosticMessages messages = provider.getMessages();
CFCallableContext context = createCallingContext(provider);
CFTarget target = new CFTarget(getTargetName(params), params, clientFactory.getClient(params, timeouts),
context);
if (messages != null && StringUtil.hasText(messages.getTargetSource())) {
logger.info("Created CF target for [{}/{}], from {}", params.getOrgName(), params.getSpaceName(),
messages.getTargetSource());
} else {
logger.info("Created CF target for [{}/{}]", params.getOrgName(), params.getSpaceName());
}
return target;
}
private CFCallableContext createCallingContext(ClientParamsProvider provider) {
return new CFCallableContext(provider.getMessages());
}
protected static String getTargetName(CFClientParams params) {
@@ -121,8 +156,4 @@ public class CFTargetCache {
return cfApiUrl;
}
}
public CfClientConfig getCfClientConfig() {
return cfClientConfig;
}
}

View File

@@ -18,6 +18,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfo.TargetDiagnosticMessages;
import org.springframework.ide.vscode.commons.util.StringUtil;
import com.google.gson.Gson;
@@ -37,7 +38,32 @@ 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();
public static final TargetDiagnosticMessages CLI_PROVIDER_MESSAGES = new TargetDiagnosticMessages() {
@Override
public String getNoTargetsFound() {
// Make this a "generic" message, instead of using "cf CLI" prefix as it shows general instructions when there are not targets
return "No Cloud Foundry targets found: Use 'cf' CLI to login";
}
@Override
public String getConnectionError() {
return "cf CLI - Connection error: Verify connection or use 'cf' CLI to login again";
}
@Override
public String getNoOrgSpace() {
return "cf CLI - No org/space selected: Use 'cf' CLI to login";
}
@Override
public String getTargetSource() {
return "cf CLI";
}
};
private Gson gson = new GsonBuilder().disableHtmlEscaping().create();
private static CfCliParamsProvider instance;
@@ -81,7 +107,7 @@ public class CfCliParamsProvider implements ClientParamsProvider {
String orgName = (String) orgFields.get(NAME);
String spaceName = (String) spaceFields.get(NAME);
if (!StringUtil.hasText(orgName) || !StringUtil.hasText(spaceName)) {
throw new NoTargetsException(getMessages().noOrgSpace());
throw new NoTargetsException(getMessages().getNoOrgSpace());
}
params.add(new CFClientParams(target, null, credentials, orgName, spaceName, sslDisabled));
}
@@ -93,7 +119,7 @@ public class CfCliParamsProvider implements ClientParamsProvider {
}
if (params.isEmpty()) {
throw new NoTargetsException(getMessages().noTargetsFound());
throw new NoTargetsException(getMessages().getNoTargetsFound());
} else {
return params;
}
@@ -123,8 +149,7 @@ public class CfCliParamsProvider implements ClientParamsProvider {
}
@Override
public CFParamsProviderMessages getMessages() {
return cfCliProviderMessages;
public TargetDiagnosticMessages getMessages() {
return CLI_PROVIDER_MESSAGES;
}
}

View File

@@ -11,7 +11,6 @@
package org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget;
import java.util.List;
import java.util.Map;
/**
* JSON-friendly representation of Cloud Foundry targets, used for integrations that need
@@ -25,7 +24,7 @@ import java.util.Map;
public class CfTargetsInfo {
private List<Target> cfTargets;
private Map<String, String> cfDiagnosticMessages;
private TargetDiagnosticMessages diagnosticMessages;
public List<Target> getCfTargets() {
return cfTargets;
@@ -35,12 +34,61 @@ public class CfTargetsInfo {
this.cfTargets = cfTargets;
}
public Map<String, String> getCfDiagnosticMessages() {
return this.cfDiagnosticMessages;
public TargetDiagnosticMessages getDiagnosticMessages() {
return diagnosticMessages;
}
public void setCfDiagnosticMessages(Map<String, String> cfDiagnosticMessages) {
this.cfDiagnosticMessages = cfDiagnosticMessages;
public void setDiagnosticMessages(TargetDiagnosticMessages diagnosticMessages) {
this.diagnosticMessages = diagnosticMessages;
}
public static class TargetDiagnosticMessages {
private String noTargetsFound;
private String connectionError;
private String noOrgSpace;
private String targetSource;
/**
*
* @return only when there are no targets available (e.g. cf CLI is not connected or no boot dash targets)
*/
public String getNoTargetsFound() {
return noTargetsFound;
}
public void setNoTargetsFound(String noTargetsFound) {
this.noTargetsFound = noTargetsFound;
}
/**
*
* @return error if any existing target cannot connect.
*/
public String getConnectionError() {
return connectionError;
}
public void setConnectionError(String connectionError) {
this.connectionError = connectionError;
}
public String getNoOrgSpace() {
return noOrgSpace;
}
public void setNoOrgSpace(String noOrgSpace) {
this.noOrgSpace = noOrgSpace;
}
public String getTargetSource() {
return targetSource;
}
public void setTargetSource(String targetSource) {
this.targetSource = targetSource;
}
}
public static class Target {
@@ -66,7 +114,7 @@ public class CfTargetsInfo {
public void setOrg(String org) {
this.org = org;
}
public void setSpace(String space) {
this.space = space;
}
@@ -90,5 +138,10 @@ public class CfTargetsInfo {
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
@Override
public String toString() {
return "Target [api=" + api + ", org=" + org + ", space=" + space + "]";
}
}
}

View File

@@ -11,11 +11,11 @@
package org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfo.TargetDiagnosticMessages;
import org.springframework.ide.vscode.commons.util.StringUtil;
import com.google.common.base.Supplier;
@@ -33,19 +33,31 @@ import com.google.common.base.Suppliers;
public class CfTargetsInfoProvder implements ClientParamsProvider {
private static final String NO_TARGETS_FOUND_MESSAGE = "No targets found";
private static final String NO_NETWORK_CONNECTION = "No connection to Cloud Foundry";
public static final String NO_NETWORK_CONNECTION = "No connection to Cloud Foundry";
private static final String NO_ORG_SPACE = "No org/space selected";
private static final String PROP_NO_TARGETS_FOUND = "noTargetsFound";
private static final String PROP_UNAUTHORISED = "unauthorised";
private static final String PROP_NO_NETWORK_CONNECTION = "noNetworkConnection";
private static final String PROP_NO_ORG_SPACE = "noOrgSpace";
public static final TargetDiagnosticMessages DEFAULT_MESSAGES = new TargetDiagnosticMessages() {
@Override
public String getNoTargetsFound() {
return NO_TARGETS_FOUND_MESSAGE;
}
@Override
public String getConnectionError() {
return NO_NETWORK_CONNECTION;
}
@Override
public String getNoOrgSpace() {
return NO_ORG_SPACE;
}
};
private Supplier<Collection<CFClientParams>> paramsSupplier;
private Map<String, String> messages;
private TargetDiagnosticMessages messages;
public CfTargetsInfoProvder(CfTargetsInfo targetsInfo) {
this.messages = targetsInfo.getCfDiagnosticMessages();
this.messages = targetsInfo.getDiagnosticMessages();
this.paramsSupplier = Suppliers.memoize(() -> targetsInfo.getCfTargets()
.stream()
.map(t -> parseCfClientParams(t))
@@ -57,7 +69,7 @@ public class CfTargetsInfoProvder implements ClientParamsProvider {
public Collection<CFClientParams> getParams() throws NoTargetsException, ExecutionException {
Collection<CFClientParams> params = paramsSupplier.get();
if (params == null || params.isEmpty()) {
throw new NoTargetsException(getMessages().noTargetsFound());
throw new NoTargetsException(getMessages().getNoTargetsFound());
}
return params;
}
@@ -79,42 +91,7 @@ public class CfTargetsInfoProvder implements ClientParamsProvider {
}
@Override
public CFParamsProviderMessages getMessages() {
return new CFParamsProviderMessages() {
@Override
public String noTargetsFound() {
if (messages != null && messages.containsKey(PROP_NO_TARGETS_FOUND)) {
return messages.get(PROP_NO_TARGETS_FOUND);
}
return NO_TARGETS_FOUND_MESSAGE;
}
@Override
public String unauthorised() {
if (messages != null && messages.containsKey(PROP_UNAUTHORISED)) {
return messages.get(PROP_UNAUTHORISED);
}
return NO_NETWORK_CONNECTION;
}
@Override
public String noNetworkConnection() {
if (messages != null && messages.containsKey(PROP_NO_NETWORK_CONNECTION)) {
return messages.get(PROP_NO_NETWORK_CONNECTION);
}
return NO_NETWORK_CONNECTION;
}
@Override
public String noOrgSpace() {
if (messages != null && messages.containsKey(PROP_NO_ORG_SPACE)) {
return messages.get(PROP_NO_ORG_SPACE);
}
return NO_ORG_SPACE;
}
};
public TargetDiagnosticMessages getMessages() {
return messages != null ? messages : DEFAULT_MESSAGES;
}
}

View File

@@ -19,15 +19,23 @@ package org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget;
public class ClientParamsCacheKey {
public final CFClientParams fullParams;
private final ClientParamsProvider provider;
/**
* Use static API to create: {@link #from(CFClientParams)}
* @param fullParams
*/
private ClientParamsCacheKey(CFClientParams fullParams) {
private ClientParamsCacheKey(CFClientParams fullParams, ClientParamsProvider provider) {
this.fullParams = fullParams;
// Not used in evaluating key equality. It's passed into the key because when a
// target is created from this key, it requires a provider context. See the CFTargetCache
this.provider = provider;
}
public ClientParamsProvider getProvider() {
return this.provider;
}
// @Override
@@ -100,11 +108,9 @@ public class ClientParamsCacheKey {
return false;
return true;
}
public static ClientParamsCacheKey from(CFClientParams params) {
return new ClientParamsCacheKey(params);
public static ClientParamsCacheKey from(CFClientParams params, ClientParamsProvider provider) {
return new ClientParamsCacheKey(params, provider);
}
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 2018 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
@@ -13,6 +13,8 @@ package org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget;
import java.util.Collection;
import java.util.concurrent.ExecutionException;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfo.TargetDiagnosticMessages;
public interface ClientParamsProvider {
/**
@@ -23,6 +25,7 @@ public interface ClientParamsProvider {
*/
Collection<CFClientParams> getParams() throws NoTargetsException, ExecutionException;
CFParamsProviderMessages getMessages();
TargetDiagnosticMessages getMessages();
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 2018 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
@@ -18,13 +18,14 @@ import static org.mockito.Mockito.when;
import java.net.UnknownHostException;
import java.util.concurrent.Callable;
import org.cloudfoundry.uaa.UaaException;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
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.CFTargetCache;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfCliProviderMessages;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfCliParamsProvider;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfo.TargetDiagnosticMessages;
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.util.ExceptionUtil;
@@ -36,19 +37,19 @@ public class CFClientTest {
MockCfCli cloudfoundry = new MockCfCli();
ClientTimeouts timeouts = new ClientTimeouts();
CFTargetCache targetCache;
CFParamsProviderMessages expectedMessages = new CfCliProviderMessages();
TargetDiagnosticMessages expectedMessages = CfCliParamsProvider.CLI_PROVIDER_MESSAGES;
@Before
public void setup() throws Exception {
targetCache = new CFTargetCache(cloudfoundry.cfClientConfig, cloudfoundry.factory, timeouts);
targetCache = new CFTargetCache(ImmutableList.of(cloudfoundry.paramsProvider), cloudfoundry.factory, timeouts);
}
@Test
public void testNoTarget() throws Exception {
when(cloudfoundry.paramsProvider.getParams())
.thenThrow(new NoTargetsException(expectedMessages.noTargetsFound()));
assertError(() -> targetCache.getOrCreate(), NoTargetsException.class, expectedMessages.noTargetsFound());
.thenThrow(new NoTargetsException(expectedMessages.getNoTargetsFound()));
assertError(() -> targetCache.getOrCreate(), NoTargetsException.class, expectedMessages.getNoTargetsFound());
}
@Test
@@ -63,7 +64,7 @@ public class CFClientTest {
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());
assertError(() -> target.getServices(), ConnectionException.class, expectedMessages.getConnectionError());
}
@Test
@@ -71,7 +72,7 @@ public class CFClientTest {
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());
assertError(() -> target.getBuildpacks(), ConnectionException.class, expectedMessages.getConnectionError());
}
@Test
@@ -79,7 +80,16 @@ public class CFClientTest {
ClientRequests client = cloudfoundry.client;
when(client.getDomains()).thenThrow(new UnknownHostException("api.run.pivotal.io"));
CFTarget target = targetCache.getOrCreate().get(0);
assertError(() -> target.getDomains(), ConnectionException.class, expectedMessages.noNetworkConnection());
assertError(() -> target.getDomains(), ConnectionException.class, expectedMessages.getConnectionError());
}
@Test
public void testInvalidRefreshToken() throws Exception {
ClientRequests client = cloudfoundry.client;
String mockedError = "org.cloudfoundry.uaa.UaaException: invalid_token: Invalid refresh token expired at Wed Mar 28 18:58:20 UTC 2018";
when(client.getDomains()).thenThrow(new UaaException(1111, mockedError, mockedError));
CFTarget target = targetCache.getOrCreate().get(0);
assertError(() -> target.getDomains(), ConnectionException.class, expectedMessages.getConnectionError());
}
@Test

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 2018 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
@@ -15,12 +15,9 @@ 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.CfClientConfig;
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.util.ExceptionUtil;
@@ -33,18 +30,16 @@ public class MockCfCli {
public final CloudFoundryClientFactory factory = mock(CloudFoundryClientFactory.class);
public final ClientRequests client = mock(ClientRequests.class);
public final CfClientConfig cfClientConfig = CfClientConfig.createDefault();
public final ClientParamsProvider paramsProvider = mock(ClientParamsProvider.class);
public final CfCliProviderMessages actualCfCliMessages = new CfCliProviderMessages();
public MockCfCli() {
try {
cfClientConfig.setClientParamsProvider(paramsProvider);
//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);
when(paramsProvider.getMessages()).thenReturn(CfCliParamsProvider.CLI_PROVIDER_MESSAGES);
} catch (Exception e) {
throw ExceptionUtil.unchecked(e);

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 2018 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
@@ -15,6 +15,8 @@ import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.ConnectionException;
@@ -29,7 +31,10 @@ import com.google.common.collect.ImmutableList;
public abstract class AbstractCFHintsProvider implements Callable<Collection<YValueHint>> {
public static final String EMPTY_VALUE = "";
public static final String PROBLEM_RESOLVING_FROM_TARGETS = "Unable to resolve hints from: ";
protected final CFTargetCache targetCache;
private Logger logger = LoggerFactory.getLogger(AbstractCFHintsProvider.class);
public AbstractCFHintsProvider(CFTargetCache targetCache) {
Assert.isNotNull(targetCache);
@@ -59,12 +64,10 @@ 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 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(errorNoAppending));
Throwable targetErrors = getErrorOfType(e, ImmutableList.of(NoTargetsException.class, ConnectionException.class));
if (targetErrors != null) {
// This error may appear in the UI (e.g. hover pop-up) so only show the error message , not the full error with stack trace
throw new ValueParseException(ExceptionUtil.getMessageNoAppendedInformation(targetErrors));
} else {
// Log any other error
//logger.log(Level.SEVERE, ExceptionUtil.getMessage(e), e);
@@ -80,9 +83,9 @@ public abstract class AbstractCFHintsProvider implements Callable<Collection<YVa
* @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) {
protected Throwable getErrorOfType(Throwable e, List<Class<? extends Throwable>> toLookFor) {
return ExceptionUtil.findThrowable(e,
ImmutableList.of(NoTargetsException.class, ConnectionException.class));
toLookFor);
}
/**
@@ -97,20 +100,31 @@ public abstract class AbstractCFHintsProvider implements Callable<Collection<YVa
}
List<YValueHint> hints = new ArrayList<>();
boolean validTargetsPresent = false;
Exception lastErrorEncountered = null;
for (CFTarget cfTarget : targets) {
try {
// TODO: check if duplicate proposals can be the list of all hints. Duplicates don't seem to cause duplicate proposals. Verify this!
getHints(cfTarget).stream().filter(hint -> !hints.contains(hint)).forEach(hint -> hints.add(hint));
validTargetsPresent = true;
} catch (Exception e) {
// Drop individual target error
// PT 156579665 - Log the Connection exceptions, as it means there are existing targets that have connection errors
// and this information could be useful to the user,
// but don't log the "NoTarget" errors, as they may be logged frequently and dont necessarily indicate an issue (e.g. cf CLI is not installed).
Throwable connectionError = getErrorOfType(e, ImmutableList.of(ConnectionException.class));
if (connectionError != null) {
logger.error("{}", ExceptionUtil.getMessageNoAppendedInformation(connectionError));
}
lastErrorEncountered = e;
}
}
if (validTargetsPresent) {
return hints;
} else if (lastErrorEncountered != null){
throw lastErrorEncountered;
} else {
throw new ConnectionException(
targetCache.getCfClientConfig().getClientParamsProvider().getMessages().noNetworkConnection());
throw new ConnectionException(PROBLEM_RESOLVING_FROM_TARGETS + " " + targets.toString());
}
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016, 2017 Pivotal, Inc.
* Copyright (c) 2016, 2018 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
@@ -10,26 +10,18 @@
*******************************************************************************/
package org.springframework.ide.vscode.manifest.yaml;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.cloudfoundry.client.ClientTimeouts;
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.CFParamsProviderMessages;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFTargetCache;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfCliParamsProvider;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfClientConfig;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfoProvder;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfo;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfo.Target;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfoProvder;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.ClientParamsProvider;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.NoTargetsException;
import org.springframework.ide.vscode.commons.cloudfoundry.client.v2.DefaultCloudFoundryClientFactoryV2;
@@ -38,7 +30,6 @@ import org.springframework.ide.vscode.commons.languageserver.hover.HoverInfoProv
import org.springframework.ide.vscode.commons.languageserver.hover.VscodeHoverEngineAdapter;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.util.HoverHandler;
import org.springframework.ide.vscode.commons.languageserver.util.Settings;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleWorkspaceService;
@@ -61,9 +52,6 @@ import org.yaml.snakeyaml.Yaml;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
public class ManifestYamlLanguageServer extends SimpleLanguageServer {
@@ -71,9 +59,6 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer {
private CfJson cfJson = new CfJson();
private ManifestYmlSchema schema;
private CFTargetCache cfTargetCache;
private final CloudFoundryClientFactory cfClientFactory;
private final CfClientConfig cfClientConfig;
private final Logger log = LoggerFactory.getLogger(ManifestYamlLanguageServer.class);
private final ImmutableSet<LanguageId> FALLBACK_YML_IDS = ImmutableSet.of(LanguageId.of("yml"), LanguageId.of("yaml"));
final private ClientParamsProvider defaultClientParamsProvider;
@@ -84,9 +69,10 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer {
public ManifestYamlLanguageServer(CloudFoundryClientFactory cfClientFactory, ClientParamsProvider defaultClientParamsProvider) {
super("vscode-manifest-yaml");
this.cfClientFactory = cfClientFactory;
this.cfClientConfig=CfClientConfig.createDefault();
this.defaultClientParamsProvider = defaultClientParamsProvider;
this.cfTargetCache = new CFTargetCache(ImmutableList.of(this.defaultClientParamsProvider), cfClientFactory, new ClientTimeouts());
SimpleTextDocumentService documents = getTextDocumentService();
SimpleWorkspaceService workspace = getWorkspaceService();
@@ -144,52 +130,11 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer {
});
}
public CfClientConfig getCfClientConfig() {
return cfClientConfig;
}
@SuppressWarnings("unchecked")
private void applyCfLoginParameterSettings(CfTargetsInfo info) {
List<Target> cfTargets = info.getCfTargets();
if (cfTargets != null) {
CfTargetsInfoProvder cfClientParamsProvider = new CfTargetsInfoProvder(info);
cfClientConfig.setClientParamsProvider(new ClientParamsProvider() {
@Override
public Collection<CFClientParams> getParams() throws NoTargetsException, ExecutionException {
List<ClientParamsProvider> providers = ImmutableList.of(defaultClientParamsProvider, cfClientParamsProvider);
List<CFClientParams> params = new ArrayList<>();
for (ClientParamsProvider provider : providers) {
try {
params.addAll(provider.getParams());
} catch (Exception e) {
// ignore
}
}
if (params.isEmpty()) {
throw new NoTargetsException(getMessages().noTargetsFound());
}
return params;
}
@Override
public CFParamsProviderMessages getMessages() {
return cfTargets.isEmpty() ? defaultClientParamsProvider.getMessages()
: cfClientParamsProvider.getMessages();
}
});
}
// Refresh the list of providers
CfTargetsInfoProvder cfClientParamsProvider = new CfTargetsInfoProvder(info);
// set providers in the order that they should be called
cfTargetCache.setProviders(defaultClientParamsProvider, cfClientParamsProvider);
}
private void validateOnDocumentChange(IReconcileEngine engine, TextDocument doc) {
@@ -235,15 +180,11 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer {
}
private CFTargetCache getCfTargetCache() {
if (cfTargetCache == null) {
// Init CF client params provider if it's initilized
if (cfClientConfig.getClientParamsProvider() == null) {
cfClientConfig.setClientParamsProvider(defaultClientParamsProvider );
}
CloudFoundryClientFactory clientFactory = cfClientFactory;
cfTargetCache = new CFTargetCache(cfClientConfig, clientFactory, new ClientTimeouts());
}
return cfTargetCache;
return this.cfTargetCache;
}
public List<ClientParamsProvider> getParamsProvider() {
return cfTargetCache.getParamsProviders();
}
/**

View File

@@ -22,11 +22,12 @@ import org.springframework.ide.vscode.commons.cloudfoundry.client.ClientTimeouts
import org.springframework.ide.vscode.commons.cloudfoundry.client.CloudFoundryClientFactory;
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.CfClientConfig;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfo;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CfTargetsInfoProvder;
import org.springframework.ide.vscode.commons.cloudfoundry.client.v2.DefaultCloudFoundryClientFactoryV2;
import com.google.common.collect.ImmutableList;
public class ManifestYamlActualCfClientTest {
private CFTargetCache cfTargetCache;
@@ -37,9 +38,8 @@ public class ManifestYamlActualCfClientTest {
cfJson = new CfJson();
CfTargetsInfo info = getTargetsInfoFromEnv();
CfTargetsInfoProvder provider = new CfTargetsInfoProvder(info);
CfClientConfig cfClientConfig = CfClientConfig.createDefault(provider);
CloudFoundryClientFactory clientFactory = DefaultCloudFoundryClientFactoryV2.INSTANCE;
cfTargetCache = new CFTargetCache(cfClientConfig, clientFactory, new ClientTimeouts());
cfTargetCache = new CFTargetCache(ImmutableList.of(provider), clientFactory, new ClientTimeouts());
}
private CfTargetsInfo getTargetsInfoFromEnv() {

View File

@@ -18,19 +18,21 @@ import java.io.File;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.junit.Test;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.CFClientParams;
import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.ClientParamsProvider;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;
public class ManifestYamlLanguageServerTest {
@@ -99,20 +101,30 @@ public class ManifestYamlLanguageServerTest {
);
harness.intialize(null);
assertEquals(1, manifestYamlLanguageServer.getCfClientConfig().getClientParamsProvider().getParams().size());
// This is an initial target, for example from cf CLI
assertEquals(1, getAllParams(manifestYamlLanguageServer.getParamsProvider()).size());
assertEquals(Arrays.asList("test.io"), manifestYamlLanguageServer.getCfTargets());
// This tests a change in workspace (e.g. boot dash) that results in two more targets created.
DidChangeConfigurationParams params = new DidChangeConfigurationParams();
JsonParser parser = new JsonParser();
params.setSettings(parser.parse(new InputStreamReader(getClass().getResourceAsStream("/cf-targets1.json")))
);
params.setSettings(parser.parse(new InputStreamReader(getClass().getResourceAsStream("/cf-targets1.json"))));
manifestYamlLanguageServer.getWorkspaceService().didChangeConfiguration(params);
assertEquals(3, manifestYamlLanguageServer.getCfClientConfig().getClientParamsProvider().getParams().size());
assertEquals(3, getAllParams(manifestYamlLanguageServer.getParamsProvider()).size());
// End result should have the initial target as well as the two additional targets obtained on workspace change
assertEquals(Arrays.asList("test.io", "api.system.demo-gcp.springapps.io", "api.run.pivotal.io"), manifestYamlLanguageServer.getCfTargets());
}
private List<CFClientParams> getAllParams(List<ClientParamsProvider> providers) throws Exception {
List<CFClientParams> all = new ArrayList<>();
for (ClientParamsProvider clientParamsProvider : providers) {
Collection<CFClientParams> params = clientParamsProvider.getParams();
all.addAll(params);
}
return all;
}
}