Skip to content

Commit

Permalink
First cut kubernetes support
Browse files Browse the repository at this point in the history
  • Loading branch information
skarsaune committed Jun 19, 2020
1 parent e969126 commit 3ef9a72
Show file tree
Hide file tree
Showing 9 changed files with 600 additions and 2 deletions.
86 changes: 86 additions & 0 deletions client/kubernetes/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<artifactId>jolokia-kubernetes</artifactId>
<packaging>bundle</packaging>
<parent>
<groupId>org.jolokia</groupId>
<artifactId>jolokia-client-parent</artifactId>
<version>1.6.3-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

<name>jolokia-kubernetes</name>

<dependencies>


<dependency>
<groupId>org.jolokia</groupId>
<artifactId>jolokia-jmx-adapter</artifactId>
<version>${project.version}</version>
<!-- as jolokia-jmx-adapter has its own bundle, keep them separate -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
<version>5.0.0</version>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<id>bundle-manifest</id>
<phase>process-classes</phase>
<goals>
<goal>manifest</goal>
</goals>
</execution>
</executions>
<configuration>
<instructions>
<Export-Package>
org.jolokia.kubernetes.client.*
</Export-Package>
<Bundle-SymbolicName>org.jolokia</Bundle-SymbolicName>
<Bundle-Description>Jolokia JMX adapter</Bundle-Description>
</instructions>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/main/assembly/standalone.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
32 changes: 32 additions & 0 deletions client/kubernetes/src/main/assembly/standalone.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2009-2020 Roland Huss
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<assembly>
<id>standalone</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<outputFileNameMapping>${artifact.artifactId}.${artifact.extension}</outputFileNameMapping>
<unpack>true</unpack>
<scope>runtime</scope>
<useProjectArtifact>true</useProjectArtifact>
</dependencySet>
</dependencySets>
</assembly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.jolokia.kubernetes.client;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Map;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorProvider;
import javax.management.remote.JMXServiceURL;
import org.jolokia.client.jmxadapter.JolokiaJmxConnector;

/**
* I provide support for handling JMX urls for the Jolokia protocol
* Syntax service:jmx:jolokia://host:port/path/to/jolokia/with/slash/suffix/
* 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
*
* <code>
* Example:
* //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")));
* connector.connect();
* connector.getMBeanServerConnection();
*
* </code>
*/
public class KubernetesJmxConnectionProvider implements JMXConnectorProvider {
@Override
public JMXConnector newJMXConnector(JMXServiceURL serviceURL, Map<String, ?> environment) throws IOException {
//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");
}
return new KubernetesJmxConnector(serviceURL, environment);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package org.jolokia.kubernetes.client;

import com.squareup.okhttp.Response;
import io.kubernetes.client.ApiClient;
import io.kubernetes.client.ApiException;
import io.kubernetes.client.Configuration;
import io.kubernetes.client.Pair;
import io.kubernetes.client.apis.CoreV1Api;
import io.kubernetes.client.models.V1OwnerReference;
import io.kubernetes.client.models.V1Pod;
import io.kubernetes.client.models.V1Service;
import io.kubernetes.client.util.ClientBuilder;
import io.kubernetes.client.util.KubeConfig;
import java.io.FileReader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.management.remote.JMXServiceURL;
import org.jolokia.client.J4pClient;
import org.jolokia.client.jmxadapter.JolokiaJmxConnector;
import org.jolokia.client.jmxadapter.RemoteJmxAdapter;

public class KubernetesJmxConnector extends JolokiaJmxConnector {

private static Pattern POD_PATTERN = Pattern
.compile("/api/v1/namespaces/([^/]+)/pods/([^/]+)/proxy/(.+)");
private static Pattern SERVICE_PATTERN = Pattern
.compile("/api/v1/namespaces/([^/]+)/services/([^/]+)/proxy/(.+)");

public KubernetesJmxConnector(JMXServiceURL serviceURL,
Map<String, ?> environment) {
super(serviceURL, environment);
}

@Override
public void connect(Map<String, ?> env) throws IOException {
if (!"kubernetes".equals(this.serviceUrl.getProtocol())) {
throw new MalformedURLException("Only Kubernetes urls are supported");
}
ApiClient client = getApiClient(env);

this.adapter = createAdapter(client);
this.postCreateAdapter();
}

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

protected ApiClient getApiClient(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
.kubeconfig(KubeConfig.loadKubeConfig(new FileReader(kubeConfigPath))).build();
}

/**
* @return a connection if successful
*/
protected J4pClient expandAndProbeUrl(ApiClient client) throws MalformedURLException {
Configuration.setDefaultApiClient(client);

CoreV1Api api = new CoreV1Api();
String proxyPath = this.serviceUrl.getURLPath();
try {
if (SERVICE_PATTERN.matcher(proxyPath).matches()) {
final Matcher matcher = SERVICE_PATTERN.matcher(proxyPath);
if (matcher.find()) {
String namespacePattern = matcher.group(1);
String servicePattern = matcher.group();
String actualNamespace = null;
String actualName = null;
for (final V1Service service : api
.listServiceForAllNamespaces(null, null, false, null, null, null, null, 5, null)
.getItems()) {

if (service.getMetadata().getNamespace().matches(namespacePattern) && service
.getMetadata().getName().matches(servicePattern)) {
actualNamespace = service.getMetadata().getNamespace();
actualName = service.getMetadata().getName();
}
}
if (actualName == null || actualNamespace == null) {
throw new MalformedURLException(
"Coult not find service in cluster for pattern " + proxyPath);
}
if (!actualNamespace.equals(namespacePattern)) {
proxyPath = proxyPath.replace(namespacePattern, actualNamespace);
}
if (!actualName.equals(servicePattern)) {
proxyPath = proxyPath.replace(servicePattern, actualName);
}
//probe a request to this URL via proxy
try {
final Response response = probeProxyPath(client, proxyPath);
if (response.isSuccessful()) {
return new J4pClient(
proxyPath, new MinimalHttpClientAdapter(client, proxyPath));
}
} catch (IOException ignore) {
}
//try to fall back to finding a pod to see if we are more successful connecting to it, will be picked up from next if block if successful

String path = matcher.group(3);
proxyPath = findPodPathIfAnyForService(actualName, actualNamespace, path, api);


}
}
if (POD_PATTERN.matcher(proxyPath).matches()) {
final Matcher matcher = POD_PATTERN.matcher(proxyPath);
if (matcher.find()) {
String namespacePattern = matcher.group(1);

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()) {

if (pod.getMetadata().getNamespace().matches(namespacePattern) && pod.getMetadata()
.getName().matches(podPattern)) {
actualNamespace = pod.getMetadata().getNamespace();
actualPodName = pod.getMetadata().getName();
actualPod = pod;
break;
}
}
if (actualPodName == null || actualNamespace == null) {
throw new MalformedURLException(
"Could not find pod in cluster for pattern " + proxyPath);
}
if (!actualNamespace.equals(namespacePattern)) {
proxyPath = proxyPath.replace(namespacePattern, actualNamespace);
}
if (!actualPodName.equals(podPattern)) {
proxyPath = proxyPath.replace(podPattern, actualPodName);
}
//probe a request to this URL via proxy
try {
final Response response = probeProxyPath(client, proxyPath);
if (response.isSuccessful()) {
return new J4pClient(
proxyPath, new MinimalHttpClientAdapter(client, proxyPath));
}
/* 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) {
}
}

}

} catch (ApiException ignore) {
}
throw new

MalformedURLException("Unable to connect to proxypath " + proxyPath);

}

/**
* Find a pod of a service, fail if none
*/
private String findPodPathIfAnyForService(String actualName, String actualNamespace, String path,
CoreV1Api api) throws MalformedURLException {
try {
for (V1Pod pod : api
.listNamespacedPod(actualNamespace, false, null, null, null, null, null, null, 5, false)
.getItems()) {
for (V1OwnerReference ref : pod.getMetadata().getOwnerReferences()) {
//pod that references the service
if (ref.getName().equals(actualName)) {
return String
.format("/api/v1/namespaces/%s/pods/%s/proxy/%s", actualNamespace, actualName,
path);
}
}
}
} catch (ApiException ignore) {
}

throw new MalformedURLException(String
.format("Could not find any pod of service %s in namespace %s with path %s", actualName,
actualNamespace, path));
}

/**
*/
public static Response probeProxyPath(ApiClient client, String proxyPath)
throws IOException, ApiException {
return MinimalHttpClientAdapter
.performRequest(client, proxyPath, Collections.singletonMap("type", "version"),
Collections.<Pair>emptyList(), "POST", new HashMap<String, String>());
}
}

0 comments on commit 3ef9a72

Please sign in to comment.