From 4b980213bc38068acd5e50f29cde0613de2d5b31 Mon Sep 17 00:00:00 2001 From: Martin Skarsaune Date: Sun, 29 Mar 2020 19:50:57 +0200 Subject: [PATCH] Initial support for authentication for pods in k6s --- .../KubernetesJmxConnectionProvider.java | 16 +++- .../client/KubernetesJmxConnector.java | 78 +++++++++++-------- .../client/MinimalHttpClientAdapter.java | 26 ++++++- .../src/test/script/testJConsole.sh | 2 +- 4 files changed, 79 insertions(+), 43 deletions(-) diff --git a/client/kubernetes/src/main/java/org/jolokia/kubernetes/client/KubernetesJmxConnectionProvider.java b/client/kubernetes/src/main/java/org/jolokia/kubernetes/client/KubernetesJmxConnectionProvider.java index d9d94fcfc..558c0a4ed 100644 --- a/client/kubernetes/src/main/java/org/jolokia/kubernetes/client/KubernetesJmxConnectionProvider.java +++ b/client/kubernetes/src/main/java/org/jolokia/kubernetes/client/KubernetesJmxConnectionProvider.java @@ -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 + * + * + * 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 kubectl 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 * @@ -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(); * @@ -32,7 +40,7 @@ public JMXConnector newJMXConnector(JMXServiceURL serviceURL, Map 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); } diff --git a/client/kubernetes/src/main/java/org/jolokia/kubernetes/client/KubernetesJmxConnector.java b/client/kubernetes/src/main/java/org/jolokia/kubernetes/client/KubernetesJmxConnector.java index 5dad76899..3a6f27688 100644 --- a/client/kubernetes/src/main/java/org/jolokia/kubernetes/client/KubernetesJmxConnector.java +++ b/client/kubernetes/src/main/java/org/jolokia/kubernetes/client/KubernetesJmxConnector.java @@ -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; @@ -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; @@ -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 environment) { @@ -39,37 +43,47 @@ public KubernetesJmxConnector(JMXServiceURL serviceURL, @Override public void connect(Map 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 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 env) throws IOException { + public static ApiClient getApiClient(Map env) throws IOException { + if(apiClient != null){ + return apiClient; + } + return buildApiClient(env); + } + + public static ApiClient buildApiClient(Map 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 env) throws MalformedURLException { Configuration.setDefaultApiClient(client); CoreV1Api api = new CoreV1Api(); String proxyPath = this.serviceUrl.getURLPath(); + final HashMap headersForProbe = createHeadersForProbe(env); try { if (SERVICE_PATTERN.matcher(proxyPath).matches()) { final Matcher matcher = SERVICE_PATTERN.matcher(proxyPath); @@ -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) { } @@ -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()) { @@ -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; } } @@ -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) { } } @@ -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 createHeadersForProbe( + Map env) { + final HashMap headers = new HashMap(); + String[] credentials= (String[]) env.get(JMXConnector.CREDENTIALS); + if(credentials != null) { + headers.put("Authorization", Credentials.basic(credentials[0], credentials[1])); + } + return headers; } /** @@ -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 headers) throws IOException, ApiException { return MinimalHttpClientAdapter .performRequest(client, proxyPath, Collections.singletonMap("type", "version"), - Collections.emptyList(), "POST", new HashMap()); + Collections.emptyList(), "POST", headers); } } diff --git a/client/kubernetes/src/main/java/org/jolokia/kubernetes/client/MinimalHttpClientAdapter.java b/client/kubernetes/src/main/java/org/jolokia/kubernetes/client/MinimalHttpClientAdapter.java index a26f0fcb8..4c259dbd0 100644 --- a/client/kubernetes/src/main/java/org/jolokia/kubernetes/client/MinimalHttpClientAdapter.java +++ b/client/kubernetes/src/main/java/org/jolokia/kubernetes/client/MinimalHttpClientAdapter.java @@ -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; @@ -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; @@ -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 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 @@ -58,8 +68,8 @@ public ClientConnectionManager getConnectionManager() { @Override public HttpResponse execute(HttpUriRequest httpUriRequest) - throws IOException, ClientProtocolException { - Map headers= new HashMap(); + throws IOException { + Map headers= createHeaders(); try { final Response response = performRequest(client, urlPath, extractBody(httpUriRequest, headers), extractQueryParameters(httpUriRequest), @@ -72,6 +82,14 @@ public HttpResponse execute(HttpUriRequest httpUriRequest) } + public HashMap createHeaders() { + final HashMap headers = new HashMap(); + 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 queryParams, String method, Map headers) throws IOException, ApiException { return client diff --git a/client/kubernetes/src/test/script/testJConsole.sh b/client/kubernetes/src/test/script/testJConsole.sh index de6f2fbbc..663f94d5b 100755 --- a/client/kubernetes/src/test/script/testJConsole.sh +++ b/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