Skip to content

Commit

Permalink
feat(config): add config properties for callback on Kubernetes
Browse files Browse the repository at this point in the history
  • Loading branch information
ebaron committed Nov 4, 2024
1 parent 9ca90a3 commit 17df0af
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
78 changes: 78 additions & 0 deletions src/main/java/io/cryostat/agent/ConfigModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -251,6 +264,10 @@ public static URI provideCryostatAgentBaseUri(Config config) {
@Singleton
@Named(CRYOSTAT_AGENT_CALLBACK)
public static URI provideCryostatAgentCallback(Config config) {
Optional<URI> callback = buildCallbackKubernetes(config);
if (callback.isPresent()) {
return callback.get();
}
return config.getValue(CRYOSTAT_AGENT_CALLBACK, URI.class);
}

Expand Down Expand Up @@ -968,4 +985,65 @@ public void clear() {
Arrays.fill(this.buf, (byte) 0);
}
}

private static Optional<URI> buildCallbackKubernetes(Config config) {
Optional<String> k8sScheme =
config.getOptionalValue(CRYOSTAT_AGENT_KUBERNETES_CALLBACK_SCHEME, String.class);
Optional<String> k8sIP =
config.getOptionalValue(CRYOSTAT_AGENT_KUBERNETES_CALLBACK_IP, String.class);
Optional<String> k8sPod =
config.getOptionalValue(CRYOSTAT_AGENT_KUBERNETES_CALLBACK_POD_NAME, String.class);
Optional<String> k8sDomain =
config.getOptionalValue(CRYOSTAT_AGENT_KUBERNETES_CALLBACK_DOMAIN, String.class);
Optional<Integer> 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<String> 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;
}
}
}
118 changes: 118 additions & 0 deletions src/test/java/io/cryostat/agent/ConfigModuleTest.java
Original file line number Diff line number Diff line change
@@ -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<InetAddress> 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"));
}
}

0 comments on commit 17df0af

Please sign in to comment.