From 17df0afe2fdec5f8662a9bcfcdddda0a98222ea0 Mon Sep 17 00:00:00 2001 From: Elliott Baron Date: Tue, 22 Oct 2024 16:39:25 -0400 Subject: [PATCH 1/3] feat(config): add config properties for callback on Kubernetes --- README.md | 5 + .../java/io/cryostat/agent/ConfigModule.java | 78 ++++++++++++ .../io/cryostat/agent/ConfigModuleTest.java | 118 ++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 src/test/java/io/cryostat/agent/ConfigModuleTest.java diff --git a/README.md b/README.md index 9a044f8e..e834acce 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,11 @@ and how it advertises itself to a Cryostat server instance. Properties that requ - [ ] `cryostat.agent.harvester.max-size-b` [`long`]: the JFR `maxsize` setting, specified in bytes, to apply to periodic uploads during the application lifecycle. Defaults to `0`, which means `unlimited`. - [ ] `cryostat.agent.smart-trigger.definitions` [`String[]`]: a comma-separated list of Smart Trigger definitions to load at startup. Defaults to the empty string: no Smart Triggers. - [ ] `cryostat.agent.smart-trigger.evaluation.period-ms` [`long`]: the length of time between Smart Trigger evaluations. Default `1000`. +- [ ] `cryostat.agent.kubernetes.callback.scheme` [`String`]: A Kubernetes-specific override for the scheme portion of the `cryostat.agent.callback` URL (e.g. `https`). +- [ ] `cryostat.agent.kubernetes.callback.pod.name` [`String`]: A Kubernetes-specific override for the host portion of the `cryostat.agent.callback` URL. If this pod is resolvable using its name as a host name, that will be used in the callback URL. +- [ ] `cryostat.agent.kubernetes.callback.ip` [`String`]: A Kubernetes-specific override for the host portion of the `cryostat.agent.callback` URL. If this pod is resolvable using the dashed IPv4 address (e.g. 1-2-3-4) as a host name, that will be used in the callback URL. +- [ ] `cryostat.agent.kubernetes.callback.domain` [`String`]: A Kubernetes-specific override for the domain portion of the `cryostat.agent.callback` URL. This will be appended to a resolvable host name to form the callback URL. +- [ ] `cryostat.agent.kubernetes.callback.port` [`int`]: A Kubernetes-specific override for the port portion of the `cryostat.agent.callback` URL. - [ ] `rht.insights.java.opt-out` [`boolean`]: for the Red Hat build of Cryostat, set this to true to disable data collection for Red Hat Insights. Defaults to `false`. Red Hat Insights data collection is always disabled for community builds of Cryostat. - [ ] `rht.insights.java.debug` [`boolean`]: for the Red Hat build of Cryostat, set this to true to enable debug logging for the Red Hat Insights Java Agent. Defaults to `false`. Red Hat Insights data collection is always disabled for community builds of Cryostat. diff --git a/src/main/java/io/cryostat/agent/ConfigModule.java b/src/main/java/io/cryostat/agent/ConfigModule.java index e2bb4675..5429b332 100644 --- a/src/main/java/io/cryostat/agent/ConfigModule.java +++ b/src/main/java/io/cryostat/agent/ConfigModule.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.net.InetAddress; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.net.UnknownHostException; @@ -49,6 +50,7 @@ import dagger.Module; import dagger.Provides; +import org.apache.http.client.utils.URIBuilder; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; import org.slf4j.Logger; @@ -208,6 +210,17 @@ public abstract class ConfigModule { public static final String CRYOSTAT_AGENT_SMART_TRIGGER_EVALUATION_PERIOD_MS = "cryostat.agent.smart-trigger.evaluation.period-ms"; + public static final String CRYOSTAT_AGENT_KUBERNETES_CALLBACK_SCHEME = + "cryostat.agent.kubernetes.callback.scheme"; + public static final String CRYOSTAT_AGENT_KUBERNETES_CALLBACK_POD_NAME = + "cryostat.agent.kubernetes.callback.pod.name"; + public static final String CRYOSTAT_AGENT_KUBERNETES_CALLBACK_IP = + "cryostat.agent.kubernetes.callback.ip"; + public static final String CRYOSTAT_AGENT_KUBERNETES_CALLBACK_DOMAIN = + "cryostat.agent.kubernetes.callback.domain"; + public static final String CRYOSTAT_AGENT_KUBERNETES_CALLBACK_PORT = + "cryostat.agent.kubernetes.callback.port"; + public static final String CRYOSTAT_AGENT_API_WRITES_ENABLED = "cryostat.agent.api.writes-enabled"; @@ -251,6 +264,10 @@ public static URI provideCryostatAgentBaseUri(Config config) { @Singleton @Named(CRYOSTAT_AGENT_CALLBACK) public static URI provideCryostatAgentCallback(Config config) { + Optional callback = buildCallbackKubernetes(config); + if (callback.isPresent()) { + return callback.get(); + } return config.getValue(CRYOSTAT_AGENT_CALLBACK, URI.class); } @@ -968,4 +985,65 @@ public void clear() { Arrays.fill(this.buf, (byte) 0); } } + + private static Optional buildCallbackKubernetes(Config config) { + Optional k8sScheme = + config.getOptionalValue(CRYOSTAT_AGENT_KUBERNETES_CALLBACK_SCHEME, String.class); + Optional k8sIP = + config.getOptionalValue(CRYOSTAT_AGENT_KUBERNETES_CALLBACK_IP, String.class); + Optional k8sPod = + config.getOptionalValue(CRYOSTAT_AGENT_KUBERNETES_CALLBACK_POD_NAME, String.class); + Optional k8sDomain = + config.getOptionalValue(CRYOSTAT_AGENT_KUBERNETES_CALLBACK_DOMAIN, String.class); + Optional k8sPort = + config.getOptionalValue(CRYOSTAT_AGENT_KUBERNETES_CALLBACK_PORT, Integer.class); + if (k8sScheme.isPresent() + && (k8sIP.isPresent() || k8sPod.isPresent()) + && k8sDomain.isPresent() + && k8sPort.isPresent()) { + + // Try resolving the pod name as a DNS name + Optional resolvedHost = + k8sPod.map(name -> name + "." + k8sDomain.get()) + .filter(host -> tryResolveHostname(host)); + + // Try resolving using dashed IP representation as a DNS name + if (resolvedHost.isEmpty()) { + resolvedHost = + k8sIP.map(ip -> ip.replaceAll("\\.", "-") + "." + k8sDomain.get()) + .filter(host -> tryResolveHostname(host)); + } + + // If none of the above resolved, then throw an error + if (resolvedHost.isEmpty()) { + throw new RuntimeException( + "Failed to resolve hostname, consider disabling hostname verification in" + + " Cryostat for the agent callback"); + } + + try { + URI result = + new URIBuilder() + .setScheme(k8sScheme.get()) + .setHost(resolvedHost.get()) + .setPort(k8sPort.get()) + .build(); + log.debug("Using " + result.toASCIIString() + " for callback URL"); + return Optional.of(result); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + return Optional.empty(); + } + + private static boolean tryResolveHostname(String hostname) { + try { + InetAddress addr = InetAddress.getByName(hostname); + log.debug("Resolved " + hostname + " to " + addr.getHostAddress()); + return true; + } catch (UnknownHostException ignored) { + return false; + } + } } diff --git a/src/test/java/io/cryostat/agent/ConfigModuleTest.java b/src/test/java/io/cryostat/agent/ConfigModuleTest.java new file mode 100644 index 00000000..ea781147 --- /dev/null +++ b/src/test/java/io/cryostat/agent/ConfigModuleTest.java @@ -0,0 +1,118 @@ +/* + * Copyright The Cryostat Authors. + * + * 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. + */ +package io.cryostat.agent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.util.Optional; + +import org.eclipse.microprofile.config.Config; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class ConfigModuleTest { + + @Mock Config config; + @Mock InetAddress addr; + MockedStatic addrMock; + + @BeforeEach + void setupEach() throws Exception { + addrMock = Mockito.mockStatic(InetAddress.class); + } + + @AfterEach + void teardownEach() { + addrMock.close(); + } + + @Test + void testCallback() throws Exception { + when(config.getValue(ConfigModule.CRYOSTAT_AGENT_CALLBACK, URI.class)) + .thenReturn(new URI("https://callback.example.com:9977")); + + URI result = ConfigModule.provideCryostatAgentCallback(config); + assertEquals("https://callback.example.com:9977", result.toASCIIString()); + } + + @Test + void testCallbackKubernetesPodName() throws Exception { + setupKubernetesCallback(); + + when(addr.getHostAddress()).thenReturn("10.2.3.4"); + addrMock.when(() -> InetAddress.getByName("foo.headless.svc.example.com")).thenReturn(addr); + + URI result = ConfigModule.provideCryostatAgentCallback(config); + assertEquals("https://foo.headless.svc.example.com:9977", result.toASCIIString()); + } + + @Test + void testCallbackKubernetesPodIP() throws Exception { + setupKubernetesCallback(); + + when(addr.getHostAddress()).thenReturn("10.2.3.4"); + addrMock.when(() -> InetAddress.getByName("foo.headless.svc.example.com")) + .thenThrow(new UnknownHostException("TEST")); + addrMock.when(() -> InetAddress.getByName("10-2-3-4.headless.svc.example.com")) + .thenReturn(addr); + + URI result = ConfigModule.provideCryostatAgentCallback(config); + assertEquals("https://10-2-3-4.headless.svc.example.com:9977", result.toASCIIString()); + } + + @Test + void testCallbackKubernetesNoMatch() throws Exception { + setupKubernetesCallback(); + + addrMock.when(() -> InetAddress.getByName("foo.headless.svc.example.com")) + .thenThrow(new UnknownHostException("TEST")); + addrMock.when(() -> InetAddress.getByName("10-2-3-4.headless.svc.example.com")) + .thenThrow(new UnknownHostException("TEST")); + + assertThrows( + RuntimeException.class, () -> ConfigModule.provideCryostatAgentCallback(config)); + } + + private void setupKubernetesCallback() { + when(config.getOptionalValue( + ConfigModule.CRYOSTAT_AGENT_KUBERNETES_CALLBACK_DOMAIN, String.class)) + .thenReturn(Optional.of("headless.svc.example.com")); + when(config.getOptionalValue( + ConfigModule.CRYOSTAT_AGENT_KUBERNETES_CALLBACK_IP, String.class)) + .thenReturn(Optional.of("10.2.3.4")); + when(config.getOptionalValue( + ConfigModule.CRYOSTAT_AGENT_KUBERNETES_CALLBACK_POD_NAME, String.class)) + .thenReturn(Optional.of("foo")); + when(config.getOptionalValue( + ConfigModule.CRYOSTAT_AGENT_KUBERNETES_CALLBACK_PORT, Integer.class)) + .thenReturn(Optional.of(9977)); + when(config.getOptionalValue( + ConfigModule.CRYOSTAT_AGENT_KUBERNETES_CALLBACK_SCHEME, String.class)) + .thenReturn(Optional.of("https")); + } +} From 596ba6f8c37f89aa299f82f778d8ade9fa5250b0 Mon Sep 17 00:00:00 2001 From: Elliott Baron Date: Mon, 4 Nov 2024 13:42:43 -0500 Subject: [PATCH 2/3] Use CEL expression for modifying host name candidates --- README.md | 9 +- .../java/io/cryostat/agent/ConfigModule.java | 117 ++++++++++++------ .../io/cryostat/agent/ConfigModuleTest.java | 29 ++--- 3 files changed, 92 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index e834acce..10939678 100644 --- a/README.md +++ b/README.md @@ -249,11 +249,10 @@ and how it advertises itself to a Cryostat server instance. Properties that requ - [ ] `cryostat.agent.harvester.max-size-b` [`long`]: the JFR `maxsize` setting, specified in bytes, to apply to periodic uploads during the application lifecycle. Defaults to `0`, which means `unlimited`. - [ ] `cryostat.agent.smart-trigger.definitions` [`String[]`]: a comma-separated list of Smart Trigger definitions to load at startup. Defaults to the empty string: no Smart Triggers. - [ ] `cryostat.agent.smart-trigger.evaluation.period-ms` [`long`]: the length of time between Smart Trigger evaluations. Default `1000`. -- [ ] `cryostat.agent.kubernetes.callback.scheme` [`String`]: A Kubernetes-specific override for the scheme portion of the `cryostat.agent.callback` URL (e.g. `https`). -- [ ] `cryostat.agent.kubernetes.callback.pod.name` [`String`]: A Kubernetes-specific override for the host portion of the `cryostat.agent.callback` URL. If this pod is resolvable using its name as a host name, that will be used in the callback URL. -- [ ] `cryostat.agent.kubernetes.callback.ip` [`String`]: A Kubernetes-specific override for the host portion of the `cryostat.agent.callback` URL. If this pod is resolvable using the dashed IPv4 address (e.g. 1-2-3-4) as a host name, that will be used in the callback URL. -- [ ] `cryostat.agent.kubernetes.callback.domain` [`String`]: A Kubernetes-specific override for the domain portion of the `cryostat.agent.callback` URL. This will be appended to a resolvable host name to form the callback URL. -- [ ] `cryostat.agent.kubernetes.callback.port` [`int`]: A Kubernetes-specific override for the port portion of the `cryostat.agent.callback` URL. +- [ ] `cryostat.agent.callback.scheme` [`String`]: An override for the scheme portion of the `cryostat.agent.callback` URL (e.g. `https`). +- [ ] `cryostat.agent.callback.host-name` [`String[]`]: An override for the host portion of the `cryostat.agent.callback` URL. Supports multiple possible host names. The first host name to resolve when paired with `cryostat.agent.callback.domain-name` will be selected. Supports an optional CEL expression that can be used to transform a provided host name. Syntax: `` or `[cel-expression]`, the latter evaluates the following CEL expression and uses the result as a host name candidate: `''.cel-expression`. Example: `host1, hostx[replace("x"\\, "2")]`, the agent will try to resolve host1, followed by host2. +- [ ] `cryostat.agent.callback.domain-name` [`String`]: An override for the domain portion of the `cryostat.agent.callback` URL. This will be appended to a resolvable host name to form the callback URL. +- [ ] `cryostat.agent.callback.port` [`int`]: An override for the port portion of the `cryostat.agent.callback` URL. - [ ] `rht.insights.java.opt-out` [`boolean`]: for the Red Hat build of Cryostat, set this to true to disable data collection for Red Hat Insights. Defaults to `false`. Red Hat Insights data collection is always disabled for community builds of Cryostat. - [ ] `rht.insights.java.debug` [`boolean`]: for the Red Hat build of Cryostat, set this to true to enable debug logging for the Red Hat Insights Java Agent. Defaults to `false`. Red Hat Insights data collection is always disabled for community builds of Cryostat. diff --git a/src/main/java/io/cryostat/agent/ConfigModule.java b/src/main/java/io/cryostat/agent/ConfigModule.java index 5429b332..744ae960 100644 --- a/src/main/java/io/cryostat/agent/ConfigModule.java +++ b/src/main/java/io/cryostat/agent/ConfigModule.java @@ -53,6 +53,10 @@ import org.apache.http.client.utils.URIBuilder; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import org.projectnessie.cel.extension.StringsLib; +import org.projectnessie.cel.tools.Script; +import org.projectnessie.cel.tools.ScriptException; +import org.projectnessie.cel.tools.ScriptHost; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -210,20 +214,20 @@ public abstract class ConfigModule { public static final String CRYOSTAT_AGENT_SMART_TRIGGER_EVALUATION_PERIOD_MS = "cryostat.agent.smart-trigger.evaluation.period-ms"; - public static final String CRYOSTAT_AGENT_KUBERNETES_CALLBACK_SCHEME = - "cryostat.agent.kubernetes.callback.scheme"; - public static final String CRYOSTAT_AGENT_KUBERNETES_CALLBACK_POD_NAME = - "cryostat.agent.kubernetes.callback.pod.name"; - public static final String CRYOSTAT_AGENT_KUBERNETES_CALLBACK_IP = - "cryostat.agent.kubernetes.callback.ip"; - public static final String CRYOSTAT_AGENT_KUBERNETES_CALLBACK_DOMAIN = - "cryostat.agent.kubernetes.callback.domain"; - public static final String CRYOSTAT_AGENT_KUBERNETES_CALLBACK_PORT = - "cryostat.agent.kubernetes.callback.port"; + public static final String CRYOSTAT_AGENT_CALLBACK_SCHEME = "cryostat.agent.callback.scheme"; + public static final String CRYOSTAT_AGENT_CALLBACK_HOST_NAME = + "cryostat.agent.callback.host-name"; + public static final String CRYOSTAT_AGENT_CALLBACK_DOMAIN_NAME = + "cryostat.agent.callback.domain-name"; + public static final String CRYOSTAT_AGENT_CALLBACK_PORT = "cryostat.agent.callback.port"; public static final String CRYOSTAT_AGENT_API_WRITES_ENABLED = "cryostat.agent.api.writes-enabled"; + private static final String HOST_SCRIPT_PATTERN_STRING = + "(?[A-Za-z0-9-.]+)(?:\\[(?