Skip to content

Commit

Permalink
Use CEL expression for modifying host name candidates
Browse files Browse the repository at this point in the history
  • Loading branch information
ebaron committed Nov 4, 2024
1 parent ead5ada commit a5cec3a
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 63 deletions.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<host name>` or `<host name>[cel-expression]`, the latter evaluates the following CEL expression and uses the result as a host name candidate: `'<host name>'.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.

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

Expand Down Expand Up @@ -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 =
"(?<host>[A-Za-z0-9-.]+)(?:\\[(?<script>.+)\\])?";
private static final Pattern HOST_SCRIPT_PATTERN = Pattern.compile(HOST_SCRIPT_PATTERN_STRING);

@Provides
@Singleton
public static Config provideConfig() {
Expand Down Expand Up @@ -264,7 +268,7 @@ public static URI provideCryostatAgentBaseUri(Config config) {
@Singleton
@Named(CRYOSTAT_AGENT_CALLBACK)
public static URI provideCryostatAgentCallback(Config config) {
Optional<URI> callback = buildCallbackKubernetes(config);
Optional<URI> callback = buildCallbackFromComponents(config);
if (callback.isPresent()) {
return callback.get();
}
Expand Down Expand Up @@ -986,33 +990,27 @@ public void clear() {
}
}

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
private static Optional<URI> buildCallbackFromComponents(Config config) {
Optional<String> scheme =
config.getOptionalValue(CRYOSTAT_AGENT_CALLBACK_SCHEME, String.class);
Optional<String[]> hostNames =
config.getOptionalValue(CRYOSTAT_AGENT_CALLBACK_HOST_NAME, String[].class);
Optional<String> domainName =
config.getOptionalValue(CRYOSTAT_AGENT_CALLBACK_DOMAIN_NAME, String.class);
Optional<Integer> port =
config.getOptionalValue(CRYOSTAT_AGENT_CALLBACK_PORT, Integer.class);
if (scheme.isPresent()
&& hostNames.isPresent()
&& domainName.isPresent()
&& port.isPresent()) {

// Try resolving the each provided host name in order 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));
}
computeHostNames(hostNames.get()).stream()
.sequential()
.map(name -> name + "." + domainName.get())
.filter(host -> tryResolveHostname(host))
.findFirst();

// If none of the above resolved, then throw an error
if (resolvedHost.isEmpty()) {
Expand All @@ -1024,11 +1022,11 @@ private static Optional<URI> buildCallbackKubernetes(Config config) {
try {
URI result =
new URIBuilder()
.setScheme(k8sScheme.get())
.setScheme(scheme.get())
.setHost(resolvedHost.get())
.setPort(k8sPort.get())
.setPort(port.get())
.build();
log.debug("Using " + result.toASCIIString() + " for callback URL");
log.debug("Using {} for callback URL", result.toASCIIString());
return Optional.of(result);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
Expand All @@ -1037,10 +1035,47 @@ private static Optional<URI> buildCallbackKubernetes(Config config) {
return Optional.empty();
}

private static List<String> computeHostNames(String[] hostNames) {
ScriptHost scriptHost = ScriptHost.newBuilder().build();
List<String> result = new ArrayList<>(hostNames.length);
for (String hostName : hostNames) {
Matcher m = HOST_SCRIPT_PATTERN.matcher(hostName);
if (!m.matches()) {
throw new RuntimeException(
"Invalid hostname argument encountered: "
+ hostName
+ ". Expected format: \"hostname\" or \"hostname[cel-script]\".");
}
if (m.group("script") == null) {
result.add(m.group("host"));
} else {
result.add(evaluateHostnameScript(scriptHost, m.group("script"), m.group("host")));
}
}

return result;
}

private static String evaluateHostnameScript(
ScriptHost scriptHost, String scriptText, String hostname) {
try {
Script script =
scriptHost
.buildScript("\"" + hostname + "\"." + scriptText)
.withLibraries(new StringsLib())
.build();
Map<String, Object> args = new HashMap<>();
return script.execute(String.class, args);
} catch (ScriptException e) {
throw new RuntimeException("Failed to execute provided CEL script", e);
}
}

private static boolean tryResolveHostname(String hostname) {
try {
log.debug("Attempting to resolve {}", hostname);
InetAddress addr = InetAddress.getByName(hostname);
log.debug("Resolved " + hostname + " to " + addr.getHostAddress());
log.debug("Resolved {} to {}", hostname, addr.getHostAddress());
return true;
} catch (UnknownHostException ignored) {
return false;
Expand Down
29 changes: 12 additions & 17 deletions src/test/java/io/cryostat/agent/ConfigModuleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ void testCallback() throws Exception {
}

@Test
void testCallbackKubernetesPodName() throws Exception {
setupKubernetesCallback();
void testCallbackComponentsHostname() throws Exception {
setupCallbackComponents();

when(addr.getHostAddress()).thenReturn("10.2.3.4");
addrMock.when(() -> InetAddress.getByName("foo.headless.svc.example.com")).thenReturn(addr);
Expand All @@ -72,8 +72,8 @@ void testCallbackKubernetesPodName() throws Exception {
}

@Test
void testCallbackKubernetesPodIP() throws Exception {
setupKubernetesCallback();
void testCallbackComponentsDashedIP() throws Exception {
setupCallbackComponents();

when(addr.getHostAddress()).thenReturn("10.2.3.4");
addrMock.when(() -> InetAddress.getByName("foo.headless.svc.example.com"))
Expand All @@ -86,8 +86,8 @@ void testCallbackKubernetesPodIP() throws Exception {
}

@Test
void testCallbackKubernetesNoMatch() throws Exception {
setupKubernetesCallback();
void testCallbackComponentsNoMatch() throws Exception {
setupCallbackComponents();

addrMock.when(() -> InetAddress.getByName("foo.headless.svc.example.com"))
.thenThrow(new UnknownHostException("TEST"));
Expand All @@ -98,21 +98,16 @@ void testCallbackKubernetesNoMatch() throws Exception {
RuntimeException.class, () -> ConfigModule.provideCryostatAgentCallback(config));
}

private void setupKubernetesCallback() {
private void setupCallbackComponents() {
when(config.getOptionalValue(
ConfigModule.CRYOSTAT_AGENT_KUBERNETES_CALLBACK_DOMAIN, String.class))
ConfigModule.CRYOSTAT_AGENT_CALLBACK_DOMAIN_NAME, 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))
ConfigModule.CRYOSTAT_AGENT_CALLBACK_HOST_NAME, String[].class))
.thenReturn(Optional.of(new String[] {"foo", "10.2.3.4[replace(\".\", \"-\")]"}));
when(config.getOptionalValue(ConfigModule.CRYOSTAT_AGENT_CALLBACK_PORT, Integer.class))
.thenReturn(Optional.of(9977));
when(config.getOptionalValue(
ConfigModule.CRYOSTAT_AGENT_KUBERNETES_CALLBACK_SCHEME, String.class))
when(config.getOptionalValue(ConfigModule.CRYOSTAT_AGENT_CALLBACK_SCHEME, String.class))
.thenReturn(Optional.of("https"));
}
}

0 comments on commit a5cec3a

Please sign in to comment.