From d675fef9ae5527932eeed2436ee86363104e9f6d Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Tue, 14 Mar 2023 15:36:20 -0400 Subject: [PATCH] feat(discovery): Podman platform (#1394) * tmp * rebase fixes * working on experimental podman handler * update netty native version * fixup! working on experimental podman handler * add comment with vertx deps BOM note * add note about systemctl start podman.socket * fixup! update netty native version * fixup netty version Signed-off-by: Andrew Azores * add security opt flag for mounting podman socket * cleanup UID lookup, run on worker thread, set API version to 2.3 * add Podman built-in discovery plugin Signed-off-by: Andrew Azores * use request query param to filter for containers with specific label * cleanup * remove obsolete podman API testing endpoints * improve JMX URL handling, add label to smoketest targets * perform fewer COW updates Signed-off-by: Andrew Azores * fix ignored podless containers * rebase cleanup * refactor platform selection allow multiple available strategies to be selected. the highest priority available platform provides the authmanager if none explicitly specified. set of selected strategies can be mapped to set of available platform clients, allowing multiple enabled built-in plugins at once Signed-off-by: Andrew Azores * deregister builtin plugins if they are no longer available/selected Signed-off-by: Andrew Azores * CRYOSTAT_PLATFORM takes comma-separated list Signed-off-by: Andrew Azores --------- Signed-off-by: Andrew Azores --- README.md | 2 +- pom.xml | 8 + run.sh | 11 + smoketest.sh | 4 + .../cryostat/discovery/BuiltInDiscovery.java | 58 ++-- .../cryostat/discovery/DiscoveryModule.java | 17 +- src/main/java/io/cryostat/net/HttpServer.java | 5 +- .../java/io/cryostat/net/NetworkModule.java | 3 +- .../io/cryostat/platform/PlatformModule.java | 125 +++++--- .../internal/PlatformStrategyModule.java | 11 +- .../internal/PodmanPlatformClient.java | 300 ++++++++++++++++++ .../internal/PodmanPlatformStrategy.java | 104 ++++++ src/main/java/io/cryostat/util/URIUtil.java | 9 +- 13 files changed, 576 insertions(+), 81 deletions(-) create mode 100644 src/main/java/io/cryostat/platform/internal/PodmanPlatformClient.java create mode 100644 src/main/java/io/cryostat/platform/internal/PodmanPlatformStrategy.java 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 authMgr; + private final Vertx vertx; + private final Gson gson; + private final FileSystem fs; + + PodmanPlatformStrategy( + Logger logger, + Lazy 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()));