diff --git a/README.md b/README.md
index ea21de395e..e0ff73b2d4 100644
--- a/README.md
+++ b/README.md
@@ -128,7 +128,7 @@ Cryostat can be configured via the following environment variables:
* `CRYOSTAT_CORS_ORIGIN`: the origin for CORS to load a different cryostat-web instance. Defaults to the empty string, which disables CORS.
* `CRYOSTAT_MAX_WS_CONNECTIONS`: the maximum number of websocket client connections allowed (minimum 1, maximum `Integer.MAX_VALUE`, default `Integer.MAX_VALUE`)
* `CRYOSTAT_AUTH_MANAGER`: the authentication/authorization manager used for validating user accesses. See the `USER AUTHENTICATION / AUTHORIZATION` section for more details. Set to the fully-qualified class name of the auth manager implementation to use, ex. `io.cryostat.net.BasicAuthManager`. Defaults to an AuthManager corresponding to the selected deployment platform, whether explicit or automatic (see below).
-* `CRYOSTAT_PLATFORM`: the platform client used for performing platform-specific actions, such as listing available target JVMs. If `CRYOSTAT_AUTH_MANAGER` is not specified then a default auth manager will also be selected corresponding to the platform, whether that platform is specified by the user or automatically detected. Set to the fully-qualified name of the platform detection strategy implementation to use, ex. `io.cryostat.platform.internal.KubeEnvPlatformStrategy`.
+* `CRYOSTAT_PLATFORM`: the platform clients used for performing platform-specific actions, such as listing available target JVMs. If `CRYOSTAT_AUTH_MANAGER` is not specified then a default auth manager will also be selected corresponding to the highest priority platform, whether those platforms are specified by the user or automatically detected. Set to the fully-qualified names of the platform detection strategy implementations to use, ex. `io.cryostat.platform.internal.KubeEnvPlatformStrategy,io.cryostat.platform.internal.PodmanPlatformStrategy`.
* `CRYOSTAT_ENABLE_JDP_BROADCAST`: enable the Cryostat JVM to broadcast itself via JDP (Java Discovery Protocol). Defaults to `true`.
* `CRYOSTAT_JDP_ADDRESS`: the JDP multicast address to send discovery packets. Defaults to `224.0.23.178`.
* `CRYOSTAT_JDP_PORT`: the JDP multicast port to send discovery packets. Defaults to `7095`.
diff --git a/pom.xml b/pom.xml
index 087dbce64f..636432c4da 100644
--- a/pom.xml
+++ b/pom.xml
@@ -207,6 +207,14 @@
jasypt-hibernate5
${org.jasypt-hibernate5.version}
+
+ io.netty
+ netty-transport-native-epoll
+ linux-x86_64
+ 4.1.86.Final
+
+
+
org.slf4j
slf4j-jdk14
diff --git a/run.sh b/run.sh
index b820b14229..d3fce69f7d 100755
--- a/run.sh
+++ b/run.sh
@@ -107,6 +107,14 @@ if ! podman pod exists cryostat-pod; then
--publish "$CRYOSTAT_EXT_WEB_PORT":"$CRYOSTAT_WEB_PORT"
fi
+# do: $ podman system service -t 0
+# or do: $ systemctl --user start podman.socket
+# to create the podman.sock to volume-mount into the container
+#
+# to check the podman socket is reachable and connectable within the container:
+# $ podman exec -it cryo /bin/sh
+# sh-4.4# curl -v -s --unix-socket /run/user/0/podman/podman.sock http://d:80/v3.0.0/libpod/info
+#
# run as root (uid 0) within the container - with rootless podman this means
# that the process will actually run with your own uid on the host machine,
# rather than the uid being remapped to something else
@@ -114,6 +122,7 @@ podman run \
--pod cryostat-pod \
--name cryostat \
--user 0 \
+ --label io.cryostat.connectUrl="service:jmx:rmi:///jndi/rmi://localhost:0/jmxrmi" \
--memory 768M \
--mount type=bind,source="$(dirname "$0")/archive",destination=/opt/cryostat.d/recordings.d,relabel=shared \
--mount type=bind,source="$(dirname "$0")/certs",destination=/certs,relabel=shared \
@@ -122,6 +131,8 @@ podman run \
--mount type=bind,source="$(dirname "$0")/templates",destination=/opt/cryostat.d/templates.d,relabel=shared \
--mount type=bind,source="$(dirname "$0")/truststore",destination=/truststore,relabel=shared \
--mount type=bind,source="$(dirname "$0")/probes",destination=/opt/cryostat.d/conf.d/probes.d,relabel=shared \
+ -v "$XDG_RUNTIME_DIR"/podman/podman.sock:/run/user/0/podman/podman.sock:Z \
+ --security-opt label=disable \
-e CRYOSTAT_ENABLE_JDP_BROADCAST="true" \
-e CRYOSTAT_REPORT_GENERATOR="$CRYOSTAT_REPORT_GENERATOR" \
-e CRYOSTAT_PLATFORM="$CRYOSTAT_PLATFORM" \
diff --git a/smoketest.sh b/smoketest.sh
index 2f59c532fb..4aebf72254 100755
--- a/smoketest.sh
+++ b/smoketest.sh
@@ -97,6 +97,7 @@ runDemoApps() {
--env HTTP_PORT=8081 \
--env JMX_PORT=9093 \
--pod cryostat-pod \
+ --label io.cryostat.connectUrl="service:jmx:rmi:///jndi/rmi://localhost:9093/jmxrmi" \
--rm -d quay.io/andrewazores/vertx-fib-demo:0.9.1
podman run \
@@ -105,6 +106,7 @@ runDemoApps() {
--env JMX_PORT=9094 \
--env USE_AUTH=true \
--pod cryostat-pod \
+ --label io.cryostat.connectUrl="service:jmx:rmi:///jndi/rmi://localhost:9094/jmxrmi" \
--rm -d quay.io/andrewazores/vertx-fib-demo:0.9.1
podman run \
@@ -114,6 +116,7 @@ runDemoApps() {
--env USE_SSL=true \
--env USE_AUTH=true \
--pod cryostat-pod \
+ --label io.cryostat.connectUrl="service:jmx:rmi:///jndi/rmi://localhost:9095/jmxrmi" \
--rm -d quay.io/andrewazores/vertx-fib-demo:0.9.1
local webPort;
@@ -234,6 +237,7 @@ runReportGenerator() {
--name reports \
--pull always \
--pod cryostat-pod \
+ --label io.cryostat.connectUrl="service:jmx:remote+http://localhost:${RJMX_PORT}" \
--cpus 1 \
--memory 512M \
--restart on-failure \
diff --git a/src/main/java/io/cryostat/discovery/BuiltInDiscovery.java b/src/main/java/io/cryostat/discovery/BuiltInDiscovery.java
index 3950574899..628b297b73 100644
--- a/src/main/java/io/cryostat/discovery/BuiltInDiscovery.java
+++ b/src/main/java/io/cryostat/discovery/BuiltInDiscovery.java
@@ -37,19 +37,22 @@
*/
package io.cryostat.discovery;
+import java.util.HashSet;
+import java.util.Iterator;
import java.util.Map;
import java.util.Set;
+import java.util.SortedSet;
import java.util.UUID;
import java.util.function.Consumer;
+import java.util.stream.Stream;
-import io.cryostat.configuration.Variables;
import io.cryostat.core.log.Logger;
-import io.cryostat.core.sys.Environment;
import io.cryostat.messaging.notifications.NotificationFactory;
import io.cryostat.platform.PlatformClient;
import io.cryostat.platform.TargetDiscoveryEvent;
import io.cryostat.platform.discovery.EnvironmentNode;
import io.cryostat.platform.internal.CustomTargetPlatformClient;
+import io.cryostat.platform.internal.PlatformDetectionStrategy;
import dagger.Lazy;
import io.vertx.core.AbstractVerticle;
@@ -60,23 +63,24 @@ public class BuiltInDiscovery extends AbstractVerticle implements Consumer platformClients;
+ private final Set> selectedStrategies;
+ private final Set> unselectedStrategies;
private final Lazy customTargets;
- private final Environment env;
+ private final Set enabledClients = new HashSet<>();
private final NotificationFactory notificationFactory;
private final Logger logger;
BuiltInDiscovery(
DiscoveryStorage storage,
- Set platformClients,
+ SortedSet> selectedStrategies,
+ SortedSet> unselectedStrategies,
Lazy customTargets,
- Environment env,
NotificationFactory notificationFactory,
Logger logger) {
this.storage = storage;
- this.platformClients = platformClients;
+ this.selectedStrategies = selectedStrategies;
+ this.unselectedStrategies = unselectedStrategies;
this.customTargets = customTargets;
- this.env = env;
this.notificationFactory = notificationFactory;
this.logger = logger;
}
@@ -84,9 +88,23 @@ public class BuiltInDiscovery extends AbstractVerticle implements Consumer start) {
storage.addTargetDiscoveryListener(this);
- (env.hasEnv(Variables.DISABLE_BUILTIN_DISCOVERY)
- ? Set.of(customTargets.get())
- : platformClients)
+
+ unselectedStrategies.stream()
+ .map(PlatformDetectionStrategy::getPlatformClient)
+ .forEach(
+ platform ->
+ storage.getBuiltInPluginByRealm(
+ platform.getDiscoveryTree().getName())
+ .map(PluginInfo::getId)
+ .ifPresent(storage::deregister));
+
+ Stream.concat(
+ // ensure custom targets is always available regardless of other
+ // configurations
+ Stream.of(customTargets.get()),
+ selectedStrategies.stream()
+ .map(PlatformDetectionStrategy::getPlatformClient))
+ .distinct()
.forEach(
platform -> {
logger.info(
@@ -126,6 +144,7 @@ public void start(Promise start) {
try {
platform.start();
platform.load(promise);
+ enabledClients.add(platform);
} catch (Exception e) {
start.fail(e);
}
@@ -136,14 +155,15 @@ public void start(Promise start) {
@Override
public void stop() {
storage.removeTargetDiscoveryListener(this);
- this.platformClients.forEach(
- platform -> {
- try {
- platform.stop();
- } catch (Exception e) {
- logger.error(e);
- }
- });
+ Iterator it = enabledClients.iterator();
+ while (it.hasNext()) {
+ try {
+ it.next().stop();
+ } catch (Exception e) {
+ logger.error(e);
+ }
+ it.remove();
+ }
}
@Override
diff --git a/src/main/java/io/cryostat/discovery/DiscoveryModule.java b/src/main/java/io/cryostat/discovery/DiscoveryModule.java
index f27560dd5f..8947ec6edc 100644
--- a/src/main/java/io/cryostat/discovery/DiscoveryModule.java
+++ b/src/main/java/io/cryostat/discovery/DiscoveryModule.java
@@ -39,6 +39,7 @@
import java.time.Duration;
import java.util.Set;
+import java.util.SortedSet;
import javax.inject.Named;
import javax.inject.Singleton;
@@ -50,9 +51,10 @@
import io.cryostat.core.log.Logger;
import io.cryostat.core.sys.Environment;
import io.cryostat.messaging.notifications.NotificationFactory;
-import io.cryostat.platform.PlatformClient;
+import io.cryostat.platform.PlatformModule;
import io.cryostat.platform.discovery.AbstractNode;
import io.cryostat.platform.internal.CustomTargetPlatformClient;
+import io.cryostat.platform.internal.PlatformDetectionStrategy;
import io.cryostat.recordings.JvmIdHelper;
import io.cryostat.rules.MatchExpressionEvaluator;
import io.cryostat.util.PluggableTypeAdapter;
@@ -116,13 +118,20 @@ static DiscoveryStorage provideDiscoveryStorage(
@Singleton
static BuiltInDiscovery provideBuiltInDiscovery(
DiscoveryStorage storage,
- Set platformClients,
+ @Named(PlatformModule.SELECTED_PLATFORMS)
+ SortedSet> selectedStrategies,
+ @Named(PlatformModule.UNSELECTED_PLATFORMS)
+ SortedSet> unselectedStrategies,
Lazy customTargets,
- Environment env,
NotificationFactory notificationFactory,
Logger logger) {
return new BuiltInDiscovery(
- storage, platformClients, customTargets, env, notificationFactory, logger);
+ storage,
+ selectedStrategies,
+ unselectedStrategies,
+ customTargets,
+ notificationFactory,
+ logger);
}
@Provides
diff --git a/src/main/java/io/cryostat/net/HttpServer.java b/src/main/java/io/cryostat/net/HttpServer.java
index 0d7a9601e6..8137a47755 100644
--- a/src/main/java/io/cryostat/net/HttpServer.java
+++ b/src/main/java/io/cryostat/net/HttpServer.java
@@ -82,7 +82,10 @@ public class HttpServer extends AbstractVerticle {
.setPort(netConf.getInternalWebServerPort())
.addWebSocketSubProtocol("*")
.setCompressionSupported(true)
- .setLogActivity(true)));
+ .setLogActivity(true)
+ .setTcpFastOpen(true)
+ .setTcpNoDelay(true)
+ .setTcpQuickAck(true)));
if (!sslConf.enabled()) {
this.logger.warn("No available SSL certificates. Fallback to plain HTTP.");
diff --git a/src/main/java/io/cryostat/net/NetworkModule.java b/src/main/java/io/cryostat/net/NetworkModule.java
index 8eee644a8c..524f9ca1cc 100644
--- a/src/main/java/io/cryostat/net/NetworkModule.java
+++ b/src/main/java/io/cryostat/net/NetworkModule.java
@@ -69,6 +69,7 @@
import dagger.Provides;
import dagger.multibindings.IntoSet;
import io.vertx.core.Vertx;
+import io.vertx.core.VertxOptions;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
@@ -155,7 +156,7 @@ static JFRConnectionToolkit provideJFRConnectionToolkit(
@Provides
@Singleton
static Vertx provideVertx() {
- return Vertx.vertx();
+ return Vertx.vertx(new VertxOptions().setPreferNativeTransport(true));
}
@Provides
diff --git a/src/main/java/io/cryostat/platform/PlatformModule.java b/src/main/java/io/cryostat/platform/PlatformModule.java
index 7e1dfb755f..841300a0d7 100644
--- a/src/main/java/io/cryostat/platform/PlatformModule.java
+++ b/src/main/java/io/cryostat/platform/PlatformModule.java
@@ -37,16 +37,21 @@
*/
package io.cryostat.platform;
+import java.util.Arrays;
+import java.util.List;
+import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.function.Predicate;
+import javax.inject.Named;
import javax.inject.Singleton;
import io.cryostat.configuration.Variables;
import io.cryostat.core.log.Logger;
-import io.cryostat.core.net.discovery.JvmDiscoveryClient;
import io.cryostat.core.sys.Environment;
-import io.cryostat.core.sys.FileSystem;
import io.cryostat.discovery.DiscoveryStorage;
import io.cryostat.net.AuthManager;
import io.cryostat.platform.discovery.PlatformDiscoveryModule;
@@ -54,15 +59,16 @@
import io.cryostat.platform.internal.PlatformDetectionStrategy;
import io.cryostat.platform.internal.PlatformStrategyModule;
-import dagger.Binds;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
-import dagger.multibindings.IntoSet;
@Module(includes = {PlatformStrategyModule.class, PlatformDiscoveryModule.class})
public abstract class PlatformModule {
+ public static final String SELECTED_PLATFORMS = "SELECTED_PLATFORMS";
+ public static final String UNSELECTED_PLATFORMS = "UNSELECTED_PLATFORMS";
+
@Provides
@Singleton
static CustomTargetPlatformClient provideCustomTargetPlatformClient(
@@ -70,78 +76,97 @@ static CustomTargetPlatformClient provideCustomTargetPlatformClient(
return new CustomTargetPlatformClient(storage);
}
- @Binds
- @IntoSet
- abstract PlatformClient bindCustomTargetPlatformClient(CustomTargetPlatformClient client);
-
@Provides
@Singleton
static AuthManager provideAuthManager(
PlatformDetectionStrategy> platformStrategy,
Environment env,
- FileSystem fs,
Set authManagers,
Logger logger) {
final String authManagerClass;
if (env.hasEnv(Variables.AUTH_MANAGER_ENV_VAR)) {
authManagerClass = env.getEnv(Variables.AUTH_MANAGER_ENV_VAR);
logger.info("Selecting configured AuthManager \"{}\"", authManagerClass);
+ return authManagers.stream()
+ .filter(
+ mgr ->
+ Objects.equals(
+ mgr.getClass().getCanonicalName(), authManagerClass))
+ .findFirst()
+ .orElseThrow(
+ () ->
+ new RuntimeException(
+ String.format(
+ "Selected AuthManager \"%s\" is not available",
+ authManagerClass)));
} else {
- authManagerClass = platformStrategy.getAuthManager().getClass().getCanonicalName();
- logger.info("Selecting platform default AuthManager \"{}\"", authManagerClass);
+ AuthManager auth = platformStrategy.getAuthManager();
+ logger.info(
+ "Selecting platform default AuthManager \"{}\"",
+ auth.getClass().getCanonicalName());
+ return auth;
}
- return authManagers.stream()
- .filter(mgr -> Objects.equals(mgr.getClass().getCanonicalName(), authManagerClass))
- .findFirst()
- .orElseThrow(
- () ->
- new RuntimeException(
- String.format(
- "Selected AuthManager \"%s\" is not available",
- authManagerClass)));
}
@Provides
@Singleton
- static PlatformDetectionStrategy> providePlatformStrategy(
- Logger logger, Set> strategies, Environment env) {
- PlatformDetectionStrategy> strat = null;
+ @Named(SELECTED_PLATFORMS)
+ static SortedSet> provideSelectedPlatformStrategies(
+ Set> platformStrategies, Environment env) {
+ // reverse sort, higher priorities should be earlier in the stream
+ SortedSet> selectedStrategies =
+ new TreeSet<>((a, b) -> Integer.compare(b.getPriority(), a.getPriority()));
+ Predicate> fn;
if (env.hasEnv(Variables.PLATFORM_STRATEGY_ENV_VAR)) {
- String platform = env.getEnv(Variables.PLATFORM_STRATEGY_ENV_VAR);
- logger.info("Selecting configured PlatformDetectionStrategy \"{}\"", platform);
- for (PlatformDetectionStrategy> s : strategies) {
- if (Objects.equals(platform, s.getClass().getCanonicalName())) {
- strat = s;
- break;
- }
- }
- if (strat == null) {
- throw new RuntimeException(
- String.format(
- "Selected PlatformDetectionStrategy \"%s\" not found", platform));
- }
+ List platforms =
+ Arrays.asList(env.getEnv(Variables.PLATFORM_STRATEGY_ENV_VAR).split(","));
+ fn = s -> platforms.contains(s.getClass().getCanonicalName());
+ } else if (env.hasEnv(Variables.DISABLE_BUILTIN_DISCOVERY)) {
+ fn = s -> false;
+ } else {
+ fn = PlatformDetectionStrategy::isAvailable;
}
- if (strat == null) {
- strat =
- strategies.stream()
- // reverse sort, higher priorities should be earlier in the stream
- .sorted((a, b) -> Integer.compare(b.getPriority(), a.getPriority()))
- .filter(PlatformDetectionStrategy::isAvailable)
- .findFirst()
- .orElseThrow();
+ for (PlatformDetectionStrategy> s : platformStrategies) {
+ if (fn.test(s)) {
+ selectedStrategies.add(s);
+ }
}
- return strat;
+ return selectedStrategies;
}
@Provides
- @IntoSet
- static PlatformClient provideDetectedPlatformClient(PlatformDetectionStrategy> strat) {
- return strat.getPlatformClient();
+ @Singleton
+ @Named(UNSELECTED_PLATFORMS)
+ static SortedSet> provideUnselectedPlatformStrategies(
+ @Named(SELECTED_PLATFORMS) SortedSet> selectedStrategies,
+ Set> platformStrategies) {
+ SortedSet> unselected =
+ new TreeSet<>((a, b) -> Integer.compare(b.getPriority(), a.getPriority()));
+ unselected.addAll(platformStrategies);
+ unselected.removeAll(selectedStrategies);
+ return unselected;
}
@Provides
@Singleton
- static JvmDiscoveryClient provideJvmDiscoveryClient(Logger logger) {
- return new JvmDiscoveryClient(logger);
+ static PlatformDetectionStrategy> providePlatformStrategy(
+ @Named(SELECTED_PLATFORMS) SortedSet> selectedStrategies,
+ Set> strategies) {
+ return selectedStrategies.stream()
+ .findFirst()
+ .orElseThrow(
+ () ->
+ new NoSuchElementException(
+ String.format(
+ "No selected platforms found. Available platforms:"
+ + " %s",
+ strategies.stream()
+ .sorted(
+ (a, b) ->
+ Integer.compare(
+ b.getPriority(),
+ a.getPriority()))
+ .map(s -> s.getClass().getCanonicalName())
+ .toList())));
}
}
diff --git a/src/main/java/io/cryostat/platform/internal/PlatformStrategyModule.java b/src/main/java/io/cryostat/platform/internal/PlatformStrategyModule.java
index 9b3eeac50d..104e46f4d3 100644
--- a/src/main/java/io/cryostat/platform/internal/PlatformStrategyModule.java
+++ b/src/main/java/io/cryostat/platform/internal/PlatformStrategyModule.java
@@ -52,10 +52,12 @@
import io.cryostat.net.openshift.OpenShiftAuthManager;
import io.cryostat.net.web.WebModule;
+import com.google.gson.Gson;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;
+import io.vertx.core.Vertx;
@Module
public abstract class PlatformStrategyModule {
@@ -68,16 +70,19 @@ static Set> providePlatformDetectionStrategies(
Lazy noopAuthManager,
Lazy connectionToolkit,
@Named(WebModule.VERTX_EXECUTOR) ExecutorService executor,
+ Vertx vertx,
+ Gson gson,
NetworkResolver resolver,
Environment env,
- FileSystem fs,
- Lazy discoveryClient) {
+ FileSystem fs) {
return Set.of(
new OpenShiftPlatformStrategy(
logger, executor, openShiftAuthManager, connectionToolkit, env, fs),
new KubeApiPlatformStrategy(
logger, executor, noopAuthManager, connectionToolkit, env, fs),
new KubeEnvPlatformStrategy(logger, fs, noopAuthManager, connectionToolkit, env),
- new DefaultPlatformStrategy(logger, noopAuthManager, discoveryClient));
+ new PodmanPlatformStrategy(logger, noopAuthManager, vertx, gson, fs),
+ new DefaultPlatformStrategy(
+ logger, noopAuthManager, () -> new JvmDiscoveryClient(logger)));
}
}
diff --git a/src/main/java/io/cryostat/platform/internal/PodmanPlatformClient.java b/src/main/java/io/cryostat/platform/internal/PodmanPlatformClient.java
new file mode 100644
index 0000000000..be325ae42f
--- /dev/null
+++ b/src/main/java/io/cryostat/platform/internal/PodmanPlatformClient.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright The Cryostat Authors
+ *
+ * The Universal Permissive License (UPL), Version 1.0
+ *
+ * Subject to the condition set forth below, permission is hereby granted to any
+ * person obtaining a copy of this software, associated documentation and/or data
+ * (collectively the "Software"), free of charge and under any and all copyright
+ * rights in the Software, and any and all patent rights owned or freely
+ * licensable by each licensor hereunder covering either (i) the unmodified
+ * Software as contributed to or provided by such licensor, or (ii) the Larger
+ * Works (as defined below), to deal in both
+ *
+ * (a) the Software, and
+ * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+ * one is included with the Software (each a "Larger Work" to which the Software
+ * is contributed by such licensors),
+ *
+ * without restriction, including without limitation the rights to copy, create
+ * derivative works of, display, perform, and distribute the Software and make,
+ * use, sell, offer for sale, import, export, have made, and have sold the
+ * Software and the Larger Work(s), and to sublicense the foregoing rights on
+ * either these or other terms.
+ *
+ * This license is subject to the following condition:
+ * The above copyright notice and either this complete permission notice or at
+ * a minimum a reference to the UPL must be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package io.cryostat.platform.internal;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Consumer;
+
+import javax.management.remote.JMXServiceURL;
+
+import io.cryostat.core.log.Logger;
+import io.cryostat.core.net.discovery.JvmDiscoveryClient.EventKind;
+import io.cryostat.platform.AbstractPlatformClient;
+import io.cryostat.platform.ServiceRef;
+import io.cryostat.platform.ServiceRef.AnnotationKey;
+import io.cryostat.platform.discovery.AbstractNode;
+import io.cryostat.platform.discovery.BaseNodeType;
+import io.cryostat.platform.discovery.EnvironmentNode;
+import io.cryostat.platform.discovery.NodeType;
+import io.cryostat.platform.discovery.TargetNode;
+import io.cryostat.util.URIUtil;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.core.net.SocketAddress;
+import io.vertx.ext.web.client.HttpResponse;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.codec.BodyCodec;
+import org.apache.commons.lang3.StringUtils;
+
+public class PodmanPlatformClient extends AbstractPlatformClient {
+
+ public static final String REALM = "Podman";
+ public static final String CRYOSTAT_LABEL = "io.cryostat.connectUrl";
+
+ private final Vertx vertx;
+ private final WebClient webClient;
+ private final Gson gson;
+ private final SocketAddress podmanSocket;
+ private final Logger logger;
+ private long timerId;
+
+ private final CopyOnWriteArrayList containers = new CopyOnWriteArrayList<>();
+
+ PodmanPlatformClient(Vertx vertx, SocketAddress podmanSocket, Gson gson, Logger logger) {
+ this.vertx = vertx;
+ this.webClient = WebClient.create(vertx);
+ this.podmanSocket = podmanSocket;
+ this.gson = gson;
+ this.logger = logger;
+ }
+
+ @Override
+ public void start() throws Exception {
+ super.start();
+ queryContainers();
+ this.timerId =
+ vertx.setPeriodic(
+ // TODO make this configurable
+ 10_000, unused -> queryContainers());
+ }
+
+ @Override
+ public void stop() throws Exception {
+ super.stop();
+ vertx.cancelTimer(timerId);
+ }
+
+ @Override
+ public List listDiscoverableServices() {
+ HashMap result = new HashMap<>();
+
+ for (ContainerSpec container : containers) {
+ result.put(container.Id, container);
+ }
+
+ return convert(result.values());
+ }
+
+ private void queryContainers() {
+ doPodmanRequest(
+ current -> {
+ Set previous = new HashSet<>(containers);
+ Set updated = new HashSet<>(current);
+
+ Set intersection = new HashSet<>(containers);
+ intersection.retainAll(updated);
+
+ Set removed = new HashSet<>(previous);
+ removed.removeAll(intersection);
+
+ Set added = new HashSet<>(updated);
+ added.removeAll(intersection);
+
+ // does anything ever get modified in this scheme?
+ // notifyAsyncTargetDiscovery(EventKind.MODIFIED, sr);
+
+ containers.removeAll(removed);
+ removed.forEach(
+ spec -> notifyAsyncTargetDiscovery(EventKind.LOST, convert(spec)));
+
+ containers.addAll(added);
+ added.forEach(
+ spec -> notifyAsyncTargetDiscovery(EventKind.FOUND, convert(spec)));
+ });
+ }
+
+ private void doPodmanRequest(Consumer> successHandler) {
+ URI requestPath = URI.create("http://d/v3.0.0/libpod/containers/json");
+ vertx.executeBlocking(
+ promise ->
+ webClient
+ .request(
+ HttpMethod.GET,
+ podmanSocket,
+ 80,
+ "localhost",
+ requestPath.toString())
+ .addQueryParam(
+ "filters",
+ gson.toJson(Map.of("label", List.of(CRYOSTAT_LABEL))))
+ // TODO make this configurable?
+ .timeout(5_000L)
+ .as(BodyCodec.string())
+ .send(
+ ar -> {
+ if (ar.failed()) {
+ Throwable t = ar.cause();
+ logger.error("Podman API request failed", t);
+ promise.fail(t);
+ return;
+ }
+ HttpResponse response = ar.result();
+ successHandler.accept(
+ gson.fromJson(
+ response.body(),
+ new TypeToken<
+ List>() {}));
+ promise.complete();
+ }));
+ }
+
+ private ServiceRef convert(ContainerSpec desc) {
+ String connectUrl = desc.Labels.get(CRYOSTAT_LABEL);
+ URI serviceUrl;
+ try {
+ serviceUrl = new URI(connectUrl);
+ } catch (URISyntaxException e) {
+ logger.warn(e);
+ return null;
+ }
+
+ ServiceRef serviceRef =
+ new ServiceRef(
+ null, serviceUrl, Optional.ofNullable(desc.Names.get(0)).orElse(desc.Id));
+
+ Map cryostatAnnotations = new HashMap<>();
+ cryostatAnnotations.put(AnnotationKey.REALM, REALM);
+
+ String host = serviceUrl.getHost();
+ int port = serviceUrl.getPort();
+ if ("service".equals(serviceUrl.getScheme())) {
+ try {
+ JMXServiceURL jmx = new JMXServiceURL(serviceUrl.toString());
+ if (URIUtil.isRmiUrl(jmx)) {
+ serviceUrl = URIUtil.getRmiTarget(jmx);
+ host = serviceUrl.getHost();
+ port = serviceUrl.getPort();
+ } else {
+ host = jmx.getHost();
+ port = jmx.getPort();
+ }
+ } catch (URISyntaxException | MalformedURLException e) {
+ logger.warn(e);
+ return null;
+ }
+ }
+ cryostatAnnotations.put(AnnotationKey.HOST, host);
+ cryostatAnnotations.put(AnnotationKey.PORT, Integer.toString(port));
+
+ serviceRef.setCryostatAnnotations(cryostatAnnotations);
+ // TODO perform podman inspection query to populate annotations
+ // serviceRef.setPlatformAnnotations();
+ serviceRef.setLabels(desc.Labels);
+
+ return serviceRef;
+ }
+
+ private List convert(Collection descs) {
+ return descs.stream().map(this::convert).filter(Objects::nonNull).toList();
+ }
+
+ @Override
+ public EnvironmentNode getDiscoveryTree() {
+ List children = new ArrayList<>();
+
+ Map pods = new HashMap<>();
+ for (ContainerSpec container : containers) {
+ ServiceRef sr = convert(container);
+ if (sr == null) {
+ continue;
+ }
+ TargetNode target = new TargetNode(BaseNodeType.JVM, sr);
+ String podName = container.PodName;
+ if (StringUtils.isNotBlank(podName)) {
+ pods.computeIfAbsent(podName, n -> new EnvironmentNode(n, PodmanNodeType.POD));
+ pods.get(podName).addChildNode(target);
+ } else {
+ children.add(target);
+ }
+ }
+ children.addAll(pods.values());
+ return new EnvironmentNode(REALM, BaseNodeType.REALM, Collections.emptyMap(), children);
+ }
+
+ static record PortSpec(
+ long container_port, String host_ip, long host_port, String protocol, long range) {}
+
+ static record ContainerSpec(
+ String Id,
+ String Image,
+ Map Labels,
+ List Names,
+ long Pid,
+ String Pod,
+ String PodName,
+ List Ports,
+ long StartedAt,
+ String State) {}
+
+ public enum PodmanNodeType implements NodeType {
+ POD("Pod"),
+ ;
+
+ private final String label;
+
+ PodmanNodeType(String label) {
+ this.label = label;
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+
+ @Override
+ public String getKind() {
+ return label;
+ }
+ }
+}
diff --git a/src/main/java/io/cryostat/platform/internal/PodmanPlatformStrategy.java b/src/main/java/io/cryostat/platform/internal/PodmanPlatformStrategy.java
new file mode 100644
index 0000000000..c5603a0a48
--- /dev/null
+++ b/src/main/java/io/cryostat/platform/internal/PodmanPlatformStrategy.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright The Cryostat Authors
+ *
+ * The Universal Permissive License (UPL), Version 1.0
+ *
+ * Subject to the condition set forth below, permission is hereby granted to any
+ * person obtaining a copy of this software, associated documentation and/or data
+ * (collectively the "Software"), free of charge and under any and all copyright
+ * rights in the Software, and any and all patent rights owned or freely
+ * licensable by each licensor hereunder covering either (i) the unmodified
+ * Software as contributed to or provided by such licensor, or (ii) the Larger
+ * Works (as defined below), to deal in both
+ *
+ * (a) the Software, and
+ * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+ * one is included with the Software (each a "Larger Work" to which the Software
+ * is contributed by such licensors),
+ *
+ * without restriction, including without limitation the rights to copy, create
+ * derivative works of, display, perform, and distribute the Software and make,
+ * use, sell, offer for sale, import, export, have made, and have sold the
+ * Software and the Larger Work(s), and to sublicense the foregoing rights on
+ * either these or other terms.
+ *
+ * This license is subject to the following condition:
+ * The above copyright notice and either this complete permission notice or at
+ * a minimum a reference to the UPL must be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package io.cryostat.platform.internal;
+
+import io.cryostat.core.log.Logger;
+import io.cryostat.core.sys.FileSystem;
+import io.cryostat.net.AuthManager;
+
+import com.google.gson.Gson;
+import com.sun.security.auth.module.UnixSystem;
+import dagger.Lazy;
+import io.vertx.core.Vertx;
+import io.vertx.core.net.SocketAddress;
+
+class PodmanPlatformStrategy implements PlatformDetectionStrategy {
+
+ private final Logger logger;
+ private final Lazy extends AuthManager> authMgr;
+ private final Vertx vertx;
+ private final Gson gson;
+ private final FileSystem fs;
+
+ PodmanPlatformStrategy(
+ Logger logger,
+ Lazy extends AuthManager> authMgr,
+ Vertx vertx,
+ Gson gson,
+ FileSystem fs) {
+ this.logger = logger;
+ this.authMgr = authMgr;
+ this.vertx = vertx;
+ this.gson = gson;
+ this.fs = fs;
+ }
+
+ @Override
+ public int getPriority() {
+ return PRIORITY_PLATFORM + 5;
+ }
+
+ @Override
+ public boolean isAvailable() {
+ String socketPath = getSocketPath();
+ logger.info("Testing {} Availability via {}", getClass().getSimpleName(), socketPath);
+ // TODO check that the service is actually available on the socket using an HTTP request
+ boolean available = fs.isReadable(fs.pathOf(socketPath));
+ logger.info("{} available? {}", getClass().getSimpleName(), available);
+ return available;
+ }
+
+ @Override
+ public PodmanPlatformClient getPlatformClient() {
+ logger.info("Selected {} Strategy", getClass().getSimpleName());
+ String socketPath = getSocketPath();
+ SocketAddress podmanPath = SocketAddress.domainSocketAddress(socketPath);
+ return new PodmanPlatformClient(vertx, podmanPath, gson, logger);
+ }
+
+ @Override
+ public AuthManager getAuthManager() {
+ return authMgr.get();
+ }
+
+ private static String getSocketPath() {
+ long uid = new UnixSystem().getUid();
+ String socketPath = String.format("/run/user/%d/podman/podman.sock", uid);
+ return socketPath;
+ }
+}
diff --git a/src/main/java/io/cryostat/util/URIUtil.java b/src/main/java/io/cryostat/util/URIUtil.java
index 0d7c54756c..b81ae2fec6 100644
--- a/src/main/java/io/cryostat/util/URIUtil.java
+++ b/src/main/java/io/cryostat/util/URIUtil.java
@@ -57,10 +57,15 @@ public static URI convert(JMXServiceURL serviceUrl) throws URISyntaxException {
return new URI(serviceUrl.toString());
}
- public static URI getRmiTarget(JMXServiceURL serviceUrl) throws URISyntaxException {
+ public static boolean isRmiUrl(JMXServiceURL serviceUrl) {
String rmiPart = "/jndi/rmi://";
String pathPart = serviceUrl.getURLPath();
- if (!pathPart.startsWith(rmiPart)) {
+ return pathPart.startsWith(rmiPart);
+ }
+
+ public static URI getRmiTarget(JMXServiceURL serviceUrl) throws URISyntaxException {
+ String pathPart = serviceUrl.getURLPath();
+ if (!isRmiUrl(serviceUrl)) {
throw new IllegalArgumentException(serviceUrl.getURLPath());
}
return new URI(pathPart.substring("/jndi/".length(), pathPart.length()));