Skip to content

Commit

Permalink
Initial support for authentication for pods in k6s
Browse files Browse the repository at this point in the history
  • Loading branch information
skarsaune committed Jun 24, 2020
1 parent 3ef9a72 commit 4b98021
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 43 deletions.
Expand Up @@ -10,7 +10,16 @@

/**
* I provide support for handling JMX urls for the Jolokia protocol
* Syntax service:jmx:jolokia://host:port/path/to/jolokia/with/slash/suffix/
* Syntax examples
* <ul>
* <li>service:jmx:kubernetes:///api/v1/namespaces/mynamespace/pods/mypodname-.+/actuator/jolokia/</li>
* <li>service:jmx:kubernetes:///api/v1/namespaces/mynamespace/services/myservice-.+/actuator/jolokia/</li>
* </ul>
*
* Regular expressions in service url is supported so you can have working URLs across deploys.
* Regular expression URLs will connect to the first pod/service that matches expession.
* Prerequesite: You should have <code>kubectl</code> installed and have valid credentiatls for k8s cluster
* readily stored under $HOME/.kube/config
* My Jar contains a service loader, so that Jolokia JMX protocol is supported
* as long as my jar (jmx-adapter-version-standalone.jar) is on the classpath
*
Expand All @@ -19,8 +28,7 @@
* //NB: include trailing slash
* https will be used if port number fits the pattern *443 or connect env map contains "jmx.remote.x.check.stub"->"true"
* JMXConnector connector = JMXConnectorFactory
* .connect(new JMXServiceURL("service:jmx:kubernetes://host:port/jolokia/"), Collections.singletonMap(JMXConnector.CREDENTIALS, Arrays
* .asList("user", "password")));
* .connect(new JMXServiceURL("service:jmx:kubernetes:///api/v1/namespaces/mynamespace/pods/mypodname-.+/actuator/jolokia/")));
* connector.connect();
* connector.getMBeanServerConnection();
*
Expand All @@ -32,7 +40,7 @@ public JMXConnector newJMXConnector(JMXServiceURL serviceURL, Map<String, ?> env
//the exception will be handled by JMXConnectorFactory so that other handlers are allowed to handle
//other protocols
if(!"kubernetes".equals(serviceURL.getProtocol())) {
throw new MalformedURLException("I only serve Kubernetes connections");
throw new MalformedURLException(String.format("Invalid URL %s : Only protocol \"kubernetes\" is supported (not %s)", serviceURL, serviceURL.getProtocol()));
}
return new KubernetesJmxConnector(serviceURL, environment);
}
Expand Down
@@ -1,5 +1,6 @@
package org.jolokia.kubernetes.client;

import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.Response;
import io.kubernetes.client.ApiClient;
import io.kubernetes.client.ApiException;
Expand All @@ -19,8 +20,10 @@
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXServiceURL;
import org.jolokia.client.J4pClient;
import org.jolokia.client.J4pClientBuilderFactory;
import org.jolokia.client.jmxadapter.JolokiaJmxConnector;
import org.jolokia.client.jmxadapter.RemoteJmxAdapter;

Expand All @@ -30,6 +33,7 @@ public class KubernetesJmxConnector extends JolokiaJmxConnector {
.compile("/api/v1/namespaces/([^/]+)/pods/([^/]+)/proxy/(.+)");
private static Pattern SERVICE_PATTERN = Pattern
.compile("/api/v1/namespaces/([^/]+)/services/([^/]+)/proxy/(.+)");
private static ApiClient apiClient;

public KubernetesJmxConnector(JMXServiceURL serviceURL,
Map<String, ?> environment) {
Expand All @@ -39,37 +43,47 @@ public KubernetesJmxConnector(JMXServiceURL serviceURL,
@Override
public void connect(Map<String, ?> env) throws IOException {
if (!"kubernetes".equals(this.serviceUrl.getProtocol())) {
throw new MalformedURLException("Only Kubernetes urls are supported");
throw new MalformedURLException(String.format("Invalid URL %s : Only protocol \"kubernetes\" is supported (not %s)", serviceUrl, serviceUrl.getProtocol()));
}
ApiClient client = getApiClient(env);
final Map<String, Object> mergedEnvironment = this.mergedEnvironment(env);
ApiClient client = getApiClient(mergedEnvironment);

this.adapter = createAdapter(client);
this.adapter = createAdapter(expandAndProbeUrl(client, mergedEnvironment));
this.postCreateAdapter();
}

protected RemoteJmxAdapter createAdapter(ApiClient client) throws IOException {
return new RemoteJmxAdapter(expandAndProbeUrl(client));
protected RemoteJmxAdapter createAdapter(J4pClient client) throws IOException {
return new RemoteJmxAdapter(client);
}

protected ApiClient getApiClient(Map<String, ?> env) throws IOException {
public static ApiClient getApiClient(Map<String, ?> env) throws IOException {
if(apiClient != null){
return apiClient;
}
return buildApiClient(env);
}

public static ApiClient buildApiClient(Map<String, ?> env) throws IOException {
// file path to your KubeConfig
final Object configPath = env != null ? env.get("kube.config.path") : null;
String kubeConfigPath = configPath != null ? configPath.toString()
: String.format("%s/.kube/config", System.getProperty("user.home"));

// loading the out-of-cluster config, a kubeconfig from file-system
return ClientBuilder
return apiClient = ClientBuilder
.kubeconfig(KubeConfig.loadKubeConfig(new FileReader(kubeConfigPath))).build();
}

/**
* @return a connection if successful
*/
protected J4pClient expandAndProbeUrl(ApiClient client) throws MalformedURLException {
protected J4pClient expandAndProbeUrl(ApiClient client,
Map<String, Object> env) throws MalformedURLException {
Configuration.setDefaultApiClient(client);

CoreV1Api api = new CoreV1Api();
String proxyPath = this.serviceUrl.getURLPath();
final HashMap<String, String> headersForProbe = createHeadersForProbe(env);
try {
if (SERVICE_PATTERN.matcher(proxyPath).matches()) {
final Matcher matcher = SERVICE_PATTERN.matcher(proxyPath);
Expand Down Expand Up @@ -100,10 +114,12 @@ protected J4pClient expandAndProbeUrl(ApiClient client) throws MalformedURLExcep
}
//probe a request to this URL via proxy
try {
final Response response = probeProxyPath(client, proxyPath);
final Response response = probeProxyPath(client, proxyPath,
headersForProbe);
response.body().close();
if (response.isSuccessful()) {
return new J4pClient(
proxyPath, new MinimalHttpClientAdapter(client, proxyPath));
proxyPath, new MinimalHttpClientAdapter(client, proxyPath, env));
}
} catch (IOException ignore) {
}
Expand All @@ -123,7 +139,6 @@ protected J4pClient expandAndProbeUrl(ApiClient client) throws MalformedURLExcep
String podPattern = matcher.group(2);
String actualNamespace = null;
String actualPodName = null;
V1Pod actualPod = null;
for (final V1Pod pod : api
.listPodForAllNamespaces(null, null, false, null, null, null, null, 5, null)
.getItems()) {
Expand All @@ -132,7 +147,6 @@ protected J4pClient expandAndProbeUrl(ApiClient client) throws MalformedURLExcep
.getName().matches(podPattern)) {
actualNamespace = pod.getMetadata().getNamespace();
actualPodName = pod.getMetadata().getName();
actualPod = pod;
break;
}
}
Expand All @@ -148,26 +162,14 @@ protected J4pClient expandAndProbeUrl(ApiClient client) throws MalformedURLExcep
}
//probe a request to this URL via proxy
try {
final Response response = probeProxyPath(client, proxyPath);
final Response response = probeProxyPath(client, proxyPath,
headersForProbe);
response.body().close();
if (response.isSuccessful()) {
return new J4pClient(
proxyPath, new MinimalHttpClientAdapter(client, proxyPath));
proxyPath, new MinimalHttpClientAdapter(client, proxyPath, env));
}
/* port forward stragegy
else if (response.code() == 403 ) {//could be proxy is not allowed try a port forward workaround instead
String path = matcher.group(3);
final Integer port = actualPod.getSpec().getContainers().get(0).getPorts()
.get(0).getContainerPort();
final String scheme = "http";
final Response forwardResponse = api
.connectGetNamespacedPodPortforwardCall(actualPodName, actualNamespace,
port, null, null).execute();
if (forwardResponse.isSuccessful()) {
//test both http and https
return new RemoteJmxAdapter(
new J4pClient(String.format("%s://localhost:%d/%s", scheme, port, path)));
}
}*/

} catch (IOException ignore) {
}
}
Expand All @@ -176,10 +178,17 @@ else if (response.code() == 403 ) {//could be proxy is not allowed try a port fo

} catch (ApiException ignore) {
}
throw new

MalformedURLException("Unable to connect to proxypath " + proxyPath);
throw new MalformedURLException("Unable to connect to proxypath " + proxyPath);
}

private HashMap<String, String> createHeadersForProbe(
Map<String, Object> env) {
final HashMap<String, String> headers = new HashMap<String, String>();
String[] credentials= (String[]) env.get(JMXConnector.CREDENTIALS);
if(credentials != null) {
headers.put("Authorization", Credentials.basic(credentials[0], credentials[1]));
}
return headers;
}

/**
Expand Down Expand Up @@ -210,10 +219,11 @@ private String findPodPathIfAnyForService(String actualName, String actualNamesp

/**
*/
public static Response probeProxyPath(ApiClient client, String proxyPath)
public static Response probeProxyPath(ApiClient client, String proxyPath,
HashMap<String, String> headers)
throws IOException, ApiException {
return MinimalHttpClientAdapter
.performRequest(client, proxyPath, Collections.singletonMap("type", "version"),
Collections.<Pair>emptyList(), "POST", new HashMap<String, String>());
Collections.<Pair>emptyList(), "POST", headers);
}
}
@@ -1,5 +1,6 @@
package org.jolokia.kubernetes.client;

import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Response;
import io.kubernetes.client.ApiClient;
Expand All @@ -14,6 +15,7 @@
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import javax.management.remote.JMXConnector;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHeaders;
Expand All @@ -34,16 +36,24 @@

/**
* I implement a minimum support of the HttpClient interface
* based on what is used by J4PClient
* based on what is used by J4PClient hence I need to adapt
* One HTTP client to another HTTP client API
*/
public class MinimalHttpClientAdapter implements HttpClient {

private final ApiClient client;
private final String urlPath;
private String user;
private String password;

public MinimalHttpClientAdapter(ApiClient client, String urlPath) {
public MinimalHttpClientAdapter(ApiClient client, String urlPath, Map<String,Object> env) {
this.client = client;
this.urlPath = urlPath;
String[] credentials= (String[]) env.get(JMXConnector.CREDENTIALS);
if(credentials != null) {
this.user=credentials[0];
this.password=credentials[1];
}
}

@Override
Expand All @@ -58,8 +68,8 @@ public ClientConnectionManager getConnectionManager() {

@Override
public HttpResponse execute(HttpUriRequest httpUriRequest)
throws IOException, ClientProtocolException {
Map<String,String> headers= new HashMap<String, String>();
throws IOException {
Map<String,String> headers= createHeaders();
try {
final Response response = performRequest(client, urlPath,
extractBody(httpUriRequest, headers), extractQueryParameters(httpUriRequest),
Expand All @@ -72,6 +82,14 @@ public HttpResponse execute(HttpUriRequest httpUriRequest)

}

public HashMap<String, String> createHeaders() {
final HashMap<String, String> headers = new HashMap<String, String>();
if(this.user != null) {
headers.put("Authorization", Credentials.basic(this.user, this.password));
}
return headers;
}

public static Response performRequest(ApiClient client, String urlPath, Object body,
List<Pair> queryParams, String method, Map<String, String> headers) throws IOException, ApiException {
return client
Expand Down
2 changes: 1 addition & 1 deletion client/kubernetes/src/test/script/testJConsole.sh
@@ -1,6 +1,6 @@
#!/bin/bash

#for instance test with: service:jmx:jolokia://localhost:8779/jolokia/
#for instance test with: service:jmx:kubernetes:///api/v1/namespaces/mynamespace/pods/mypodname-.*/actuator/jolokia/
if [ "$JAVA_HOME" = "" ]
then
echo "JAVA_HOME must be set and valid to run $0" ; exit 1
Expand Down

0 comments on commit 4b98021

Please sign in to comment.