Skip to content

Commit

Permalink
Add connection status for Vespa sidepanel
Browse files Browse the repository at this point in the history
  • Loading branch information
pehrs committed Apr 6, 2024
1 parent 48db1e0 commit 0fc7597
Show file tree
Hide file tree
Showing 18 changed files with 795 additions and 260 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ file [from disk](https://www.jetbrains.com/help/idea/managing-plugins.html#insta
* Onlys zips the dir and calls the prepare and activate function on the config endpoint
* No support for application code for now.
* Simple visualization of service.xml files
* Right click on a services.xml file and select "Show Service Overview"
* Right-click on a services.xml file and select "Show Service Overview"
* Show connection status in the Vespa dock
* [1.0.1] - Fix since-build for idea-version
* [1.0.0] - First version

Expand Down
113 changes: 113 additions & 0 deletions src/main/java/com/pehrs/vespa/yql/plugin/VespaClusterChecker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.pehrs.vespa.yql.plugin;

import com.fasterxml.jackson.databind.JsonNode;
import com.intellij.openapi.progress.ProgressIndicator;
import com.pehrs.vespa.yql.plugin.YqlResult.YqlResultListener;
import com.pehrs.vespa.yql.plugin.settings.VespaClusterConfig;
import com.pehrs.vespa.yql.plugin.settings.YqlAppSettingsState;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class VespaClusterChecker {

private static final Logger log = LoggerFactory.getLogger(VespaClusterChecker.class);

public static interface StatusListener {
void vespaClusterStatusUpdated();
}

static List<StatusListener> listeners = new ArrayList();

@Getter
static Map<VespaClusterConfig, String> queryEndpointStatus = new HashMap<>();
@Getter
static Map<VespaClusterConfig, String> configEndpointStatus = new HashMap<>();

public static void checkVespaClusters() {
checkVespaClusters(null);
}

public static void checkVespaClusters(ProgressIndicator indicator) {
YqlAppSettingsState settings = YqlAppSettingsState.getInstance();
if (settings != null) {

List<VespaClusterConfig> clusterConfigs = settings.clusterConfigs;
for (int i = 0; i < clusterConfigs.size(); i++) {
VespaClusterConfig clusterConfig = clusterConfigs.get(i);
if (indicator != null) {
indicator.setFraction(i / clusterConfigs.size());
indicator.setText("Checking Vespa cluster " + clusterConfig.name);
indicator.setText("Vespa: " + clusterConfig.queryEndpoint);
}
URI configHostUri = clusterConfig.getConfigUri().resolve("/");
String configCode = getHealthStatusCode(configHostUri.toString(), clusterConfig);
log.trace("[" + clusterConfig.name + "]" + clusterConfig.configEndpoint + ": " + configCode);
URI queryHostUri = clusterConfig.getQueryUri().resolve("/");
String queryCode = getHealthStatusCode(queryHostUri.toString(), clusterConfig);
log.trace("[" + clusterConfig.name + "]" + clusterConfig.queryEndpoint + ": " + queryCode);

queryEndpointStatus.put(clusterConfig, queryCode);
configEndpointStatus.put(clusterConfig, configCode);
}
notifyStatusListeners();
}
}

public final static String STATUS_UP = "up";
public final static String STATUS_DOWN = "down";
public final static String STATUS_INITIALIZING = "initializing";
public final static String STATUS_FAIL = "fail";

private static String getHealthStatusCode(String endpoint, VespaClusterConfig clusterConfig) {
String url = String.format("%s/state/v1/health", endpoint);
try {
JsonNode healthResponse =
VespaClusterConnection.jsonGet(clusterConfig, url);
if(healthResponse.has("status")) {
if(healthResponse.get("status").has("code")) {
String code = healthResponse.get("status").get("code").asText();
return code;
}
}
return STATUS_DOWN;
} catch (Exception ex) {
return STATUS_FAIL;
}
}


public static void addStatusListener(StatusListener listener) {
if(!listeners.contains(listener)) {
listeners.add(listener);
}
}

public void removeStatusListener(StatusListener listener) {
if(YqlResult.listeners.contains(listener)) {
YqlResult.listeners.remove(listener);
}
}
private static void notifyStatusListeners() {
synchronized (listeners) {
List<StatusListener> toBeRemoved = listeners.stream().flatMap(listener -> {
try {
listener.vespaClusterStatusUpdated();
} catch (Exception ex) {
ex.printStackTrace();
return Stream.of(listener);
}
return Stream.empty();
}).collect(Collectors.toList());
listeners.removeAll(toBeRemoved);
}
}

}
232 changes: 232 additions & 0 deletions src/main/java/com/pehrs/vespa/yql/plugin/VespaClusterConnection.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package com.pehrs.vespa.yql.plugin;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pehrs.vespa.yql.plugin.settings.VespaClusterConfig;
import com.pehrs.vespa.yql.plugin.settings.YqlAppSettingsState;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Optional;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;
import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.pem.util.PemUtils;
import nl.altindag.ssl.util.TrustManagerUtils;
import nl.altindag.ssl.util.internal.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class VespaClusterConnection {

private static final Logger log = LoggerFactory.getLogger(VespaClusterConnection.class);

final static ObjectMapper objectMapper = new ObjectMapper();

public static SSLFactory createSslFactory(String caPemFile, String clientCertFile,
String clientKeyFile) {
log.trace("createSslFactory(" + caPemFile + ", " + clientCertFile + ", " + clientKeyFile + ")");
X509ExtendedTrustManager trustManager;
trustManager = createTrustManager(caPemFile);

X509ExtendedKeyManager keyManager;
keyManager = createKeyManager(clientCertFile, clientKeyFile);

SSLFactory sslFactory = SSLFactory.builder()
.withTrustMaterial(trustManager)
.withIdentityMaterial(keyManager)
.build();
return sslFactory;
}

static X509ExtendedKeyManager createKeyManager(String clientCertFile,
String clientKeyFile) {
X509ExtendedKeyManager keyManager;
try (FileInputStream certFile = new FileInputStream(clientCertFile);
FileInputStream keyFile = new FileInputStream(clientKeyFile)) {
String certificateChainContent = IOUtils.getContent(certFile);
String privateKeyContent = IOUtils.getContent(keyFile);
keyManager = PemUtils.parseIdentityMaterial(certificateChainContent,
privateKeyContent, null);
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
return keyManager;
}

static X509ExtendedTrustManager createTrustManager(String caPemFile) {
X509ExtendedTrustManager trustManager;
try (FileInputStream file = new FileInputStream(caPemFile)) {
String caPemContent = IOUtils.getContent(file);
List<X509Certificate> cert = PemUtils.parseCertificate(caPemContent);
trustManager = TrustManagerUtils.createTrustManager(cert);
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
return trustManager;
}

public static YqlResult executeQuery(String yqlQueryRequest)
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {

YqlAppSettingsState settings = YqlAppSettingsState.getInstance();

Optional<VespaClusterConfig> configOpt = settings.getCurrentClusterConfig();
VespaClusterConfig config = configOpt.orElseThrow(() ->
new RuntimeException("Could not get current connection configuration!")
);

HttpPost request = new HttpPost(config.queryEndpoint);
request.addHeader("content-type", "application/json;charset=UTF-8");
StringEntity entity = new StringEntity(yqlQueryRequest, ContentType.APPLICATION_JSON);
request.setEntity(entity);

HttpClientBuilder httpClientBuilder = HttpClients.custom();
if (settings.sslAllowAll) {
log.warn("Allowing all server TLS certificates");
allowAll(httpClientBuilder);
}
if (config.sslUseClientCert) {
SSLFactory sslFactory = createSslFactory(
config.sslCaCert,
config.sslClientCert,
config.sslClientKey);
LayeredConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
sslFactory.getSslContext(),
sslFactory.getSslParameters().getProtocols(),
sslFactory.getSslParameters().getCipherSuites(),
sslFactory.getHostnameVerifier()
);
httpClientBuilder.setSSLSocketFactory(socketFactory);
}
try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
CloseableHttpResponse response = httpClient.execute(request);
try {
String responseString = EntityUtils.toString(response.getEntity());
log.debug("HTTP Response Status: " + response.getStatusLine());
log.trace("HTTP Response: " + responseString);
return new YqlResult(responseString);
} finally {
response.close();
}
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}

public static void allowAll(HttpClientBuilder httpClientBuilder)
throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy)
.build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,
NoopHostnameVerifier.INSTANCE);

Registry<ConnectionSocketFactory> socketFactoryRegistry =
RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", sslsf)
.register("http", new PlainConnectionSocketFactory())
.build();

BasicHttpClientConnectionManager connectionManager =
new BasicHttpClientConnectionManager(socketFactoryRegistry);
httpClientBuilder.setConnectionManager(connectionManager);
}

public static JsonNode jsonGet(VespaClusterConfig config, String url) {
String res = get(config, url);
try {
return objectMapper.readTree(res);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

public static String get(VespaClusterConfig config, String url) {
HttpGet request = new HttpGet(url);
return execute(config, request);
}

public static String execute(VespaClusterConfig config, HttpRequestBase requestBase) {

YqlAppSettingsState settings = YqlAppSettingsState.getInstance();
//
// Optional<VespaClusterConfig> configOpt = settings.getCurrentClusterConfig();
// VespaClusterConfig config = configOpt.orElseThrow(() ->
// new RuntimeException("Could not get current connection configuration!")
// );

SocketConfig socketConfig = SocketConfig.custom()
.setSoTimeout(10_000)
.build();
HttpClientBuilder httpClientBuilder = HttpClients.custom()
.setDefaultSocketConfig(socketConfig);

URI uri = requestBase.getURI();
if (uri.getScheme().equals("https")) {
// NOTE: We cannot use the sslAllowAll for the config api, it typically requires the client cert to work
if (settings.sslAllowAll && uri.toString().equals(config.queryEndpoint)) {
log.warn("Allowing all server TLS certificates");
try {
allowAll(httpClientBuilder);
} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
throw new RuntimeException(e);
}
}
if (config.sslUseClientCert) {
SSLFactory sslFactory = VespaClusterConnection.createSslFactory(
config.sslCaCert,
config.sslClientCert,
config.sslClientKey);
LayeredConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
sslFactory.getSslContext(),
sslFactory.getSslParameters().getProtocols(),
sslFactory.getSslParameters().getCipherSuites(),
sslFactory.getHostnameVerifier()
);
httpClientBuilder.setSSLSocketFactory(socketFactory);
}
}
try (CloseableHttpClient httpClient = httpClientBuilder.build();
CloseableHttpResponse response = httpClient.execute(requestBase)) {
String responseString = EntityUtils.toString(response.getEntity());
log.trace("HTTP Response Status: " + response.getStatusLine());
log.trace("HTTP Response: " + responseString);
return responseString;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Loading

0 comments on commit 0fc7597

Please sign in to comment.