From 06a8cc10f24feba30c9db3ae31f1b31f29f1b428 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 27 Jul 2023 16:10:08 -0400 Subject: [PATCH] feat(api): enable dynamic JFR start (#165) --- README.md | 1 + .../java/io/cryostat/agent/ConfigModule.java | 3 + .../java/io/cryostat/agent/MainModule.java | 73 +++++- .../java/io/cryostat/agent/WebServer.java | 63 +++-- .../agent/remote/EventTemplatesContext.java | 48 ++-- .../agent/remote/EventTypesContext.java | 35 +-- .../cryostat/agent/remote/MBeanContext.java | 38 +-- .../agent/remote/MutatingRemoteContext.java | 60 +++++ .../agent/remote/RecordingsContext.java | 217 +++++++++++++----- .../cryostat/agent/remote/RemoteContext.java | 4 + .../META-INF/microprofile-config.properties | 2 + 11 files changed, 401 insertions(+), 143 deletions(-) create mode 100644 src/main/java/io/cryostat/agent/remote/MutatingRemoteContext.java diff --git a/README.md b/README.md index 483f886f..97348324 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ and how it advertises itself to a Cryostat server instance. Required properties - [x] `cryostat.agent.baseuri` [`java.net.URI`]: the URL location of the Cryostat server backend that this agent advertises itself to. - [x] `cryostat.agent.callback` [`java.net.URI`]: a URL pointing back to this agent, ex. `"https://12.34.56.78:1234/"`. Cryostat will use this URL to perform health checks and request updates from the agent. This reflects the externally-visible IP address/hostname and port where this application and agent can be found. +- [ ] `cryostat.agent.api.writes-enabled` [`boolean`]: Control whether the agent accepts "write" or mutating operations on its HTTP API. Requests for remote operations such as dynamically starting Flight Recordings will be rejected unless this is set. Default `false`. - [ ] `cryostat.agent.instance-id` [`String`]: a unique ID for this agent instance. This will be used to uniquely identify the agent in the Cryostat discovery database, as well as to unambiguously match its encrypted stored credentials. The default is a random UUID string. It is not recommended to override this value. - [ ] `cryostat.agent.hostname` [`String`]: the hostname for this application instance. This will be used for the published JMX connection URL. If not provided then the default is to attempt to resolve the localhost hostname. - [ ] `cryostat.agent.realm` [`String`]: the Cryostat Discovery API "realm" that this agent belongs to. This should be unique per agent instance. The default is the value of `cryostat.agent.app.name`. diff --git a/src/main/java/io/cryostat/agent/ConfigModule.java b/src/main/java/io/cryostat/agent/ConfigModule.java index c1b1c9e3..a464439b 100644 --- a/src/main/java/io/cryostat/agent/ConfigModule.java +++ b/src/main/java/io/cryostat/agent/ConfigModule.java @@ -85,6 +85,9 @@ public abstract class ConfigModule { public static final String CRYOSTAT_AGENT_HARVESTER_MAX_SIZE_B = "cryostat.agent.harvester.max-size-b"; + public static final String CRYOSTAT_AGENT_API_WRITES_ENABLED = + "cryostat.agent.api.writes-enabled"; + @Provides @Singleton public static SmallRyeConfig provideConfig() { diff --git a/src/main/java/io/cryostat/agent/MainModule.java b/src/main/java/io/cryostat/agent/MainModule.java index e963ab66..d66e1280 100644 --- a/src/main/java/io/cryostat/agent/MainModule.java +++ b/src/main/java/io/cryostat/agent/MainModule.java @@ -15,7 +15,9 @@ */ package io.cryostat.agent; +import java.io.IOException; import java.net.URI; +import java.nio.file.Path; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -39,6 +41,7 @@ import io.cryostat.core.net.JFRConnectionToolkit; import io.cryostat.core.sys.Environment; import io.cryostat.core.sys.FileSystem; +import io.cryostat.core.templates.LocalStorageTemplateService; import io.cryostat.core.tui.ClientWriter; import com.fasterxml.jackson.databind.ObjectMapper; @@ -63,6 +66,7 @@ public abstract class MainModule { // one for outbound HTTP requests, one for incoming HTTP requests, and one as a general worker private static final int NUM_WORKER_THREADS = 3; private static final String JVM_ID = "JVM_ID"; + private static final String TEMPLATES_PATH = "TEMPLATES_PATH"; @Provides @Singleton @@ -258,21 +262,61 @@ public static Harvester provideHarvester( registration); } + @Provides + @Singleton + public static FileSystem provideFileSystem() { + return new FileSystem(); + } + + @Provides + @Singleton + @Named(TEMPLATES_PATH) + public static Path provideTemplatesTmpPath(FileSystem fs) { + try { + return fs.createTempDirectory(null); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Provides + @Singleton + public static Environment provideEnvironment(@Named(TEMPLATES_PATH) Path templatesTmp) { + return new Environment() { + @Override + public String getEnv(String key) { + if (LocalStorageTemplateService.TEMPLATE_PATH.equals(key)) { + return templatesTmp.toString(); + } + return super.getEnv(key); + } + }; + } + + @Provides + @Singleton + public static ClientWriter provideClientWriter() { + Logger log = LoggerFactory.getLogger(JFRConnectionToolkit.class); + return new ClientWriter() { + @Override + public void print(String msg) { + log.info(msg); + } + }; + } + + @Provides + @Singleton + public static JFRConnectionToolkit provideJfrConnectionToolkit( + ClientWriter cw, FileSystem fs, Environment env) { + return new JFRConnectionToolkit(cw, fs, env); + } + @Provides @Singleton @Named(JVM_ID) - public static String provideJvmId() { + public static String provideJvmId(JFRConnectionToolkit tk) { Logger log = LoggerFactory.getLogger(JFRConnectionToolkit.class); - JFRConnectionToolkit tk = - new JFRConnectionToolkit( - new ClientWriter() { - @Override - public void print(String msg) { - log.warn(msg); - } - }, - new FileSystem(), - new Environment()); try { try (JFRConnection connection = tk.connect(tk.createServiceURL("localhost", 0))) { String id = connection.getJvmId(); @@ -283,4 +327,11 @@ public void print(String msg) { throw new RuntimeException(e); } } + + @Provides + @Singleton + public static LocalStorageTemplateService provideLocalStorageTemplateService( + FileSystem fs, Environment env) { + return new LocalStorageTemplateService(fs, env); + } } diff --git a/src/main/java/io/cryostat/agent/WebServer.java b/src/main/java/io/cryostat/agent/WebServer.java index 7c349f4d..1d2f78c6 100644 --- a/src/main/java/io/cryostat/agent/WebServer.java +++ b/src/main/java/io/cryostat/agent/WebServer.java @@ -40,6 +40,7 @@ import com.sun.net.httpserver.Filter; import com.sun.net.httpserver.HttpContext; import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import dagger.Lazy; import org.apache.http.HttpStatus; @@ -100,13 +101,15 @@ void start() throws IOException, NoSuchAlgorithmException { Set mergedContexts = new HashSet<>(remoteContexts.get()); mergedContexts.add(new PingContext()); - mergedContexts.forEach( - rc -> { - HttpContext ctx = this.http.createContext(rc.path(), rc::handle); - ctx.setAuthenticator(agentAuthenticator); - ctx.getFilters().add(requestLoggingFilter); - ctx.getFilters().add(compressionFilter); - }); + mergedContexts.stream() + .filter(RemoteContext::available) + .forEach( + rc -> { + HttpContext ctx = this.http.createContext(rc.path(), wrap(rc::handle)); + ctx.setAuthenticator(agentAuthenticator); + ctx.getFilters().add(requestLoggingFilter); + ctx.getFilters().add(compressionFilter); + }); this.http.start(); } @@ -154,6 +157,18 @@ CompletableFuture generateCredentials() throws NoSuchAlgorithmException { } } + private HttpHandler wrap(HttpHandler handler) { + return x -> { + try { + handler.handle(x); + } catch (Exception e) { + log.error("Unhandled exception", e); + x.sendResponseHeaders(HttpStatus.SC_INTERNAL_SERVER_ERROR, 0); + x.close(); + } + }; + } + private class PingContext implements RemoteContext { @Override @@ -163,23 +178,25 @@ public String path() { @Override public void handle(HttpExchange exchange) throws IOException { - String mtd = exchange.getRequestMethod(); - switch (mtd) { - case "POST": - synchronized (WebServer.this.credentials) { - executor.execute(registration.get()::tryRegister); + try { + String mtd = exchange.getRequestMethod(); + switch (mtd) { + case "POST": + synchronized (WebServer.this.credentials) { + executor.execute(registration.get()::tryRegister); + exchange.sendResponseHeaders(HttpStatus.SC_NO_CONTENT, -1); + } + break; + case "GET": exchange.sendResponseHeaders(HttpStatus.SC_NO_CONTENT, -1); - exchange.close(); - } - break; - case "GET": - exchange.sendResponseHeaders(HttpStatus.SC_NO_CONTENT, -1); - exchange.close(); - break; - default: - exchange.sendResponseHeaders(HttpStatus.SC_NOT_FOUND, -1); - exchange.close(); - break; + break; + default: + log.warn("Unknown request method {}", mtd); + exchange.sendResponseHeaders(HttpStatus.SC_METHOD_NOT_ALLOWED, -1); + break; + } + } finally { + exchange.close(); } } } diff --git a/src/main/java/io/cryostat/agent/remote/EventTemplatesContext.java b/src/main/java/io/cryostat/agent/remote/EventTemplatesContext.java index 03c0770d..fb5fca55 100644 --- a/src/main/java/io/cryostat/agent/remote/EventTemplatesContext.java +++ b/src/main/java/io/cryostat/agent/remote/EventTemplatesContext.java @@ -48,30 +48,32 @@ public String path() { @Override public void handle(HttpExchange exchange) throws IOException { - String mtd = exchange.getRequestMethod(); - switch (mtd) { - case "GET": - try { - exchange.sendResponseHeaders(HttpStatus.SC_OK, 0); - try (OutputStream response = exchange.getResponseBody()) { - FlightRecorderMXBean bean = - ManagementFactory.getPlatformMXBean(FlightRecorderMXBean.class); - List xmlTexts = - bean.getConfigurations().stream() - .map(ConfigurationInfo::getContents) - .collect(Collectors.toList()); - mapper.writeValue(response, xmlTexts); + try { + String mtd = exchange.getRequestMethod(); + switch (mtd) { + case "GET": + try { + exchange.sendResponseHeaders(HttpStatus.SC_OK, 0); + try (OutputStream response = exchange.getResponseBody()) { + FlightRecorderMXBean bean = + ManagementFactory.getPlatformMXBean(FlightRecorderMXBean.class); + List xmlTexts = + bean.getConfigurations().stream() + .map(ConfigurationInfo::getContents) + .collect(Collectors.toList()); + mapper.writeValue(response, xmlTexts); + } + } catch (Exception e) { + log.error("events serialization failure", e); } - } catch (Exception e) { - log.error("events serialization failure", e); - } finally { - exchange.close(); - } - break; - default: - exchange.sendResponseHeaders(HttpStatus.SC_NOT_FOUND, -1); - exchange.close(); - break; + break; + default: + log.warn("Unknown request method {}", mtd); + exchange.sendResponseHeaders(HttpStatus.SC_METHOD_NOT_ALLOWED, -1); + break; + } + } finally { + exchange.close(); } } } diff --git a/src/main/java/io/cryostat/agent/remote/EventTypesContext.java b/src/main/java/io/cryostat/agent/remote/EventTypesContext.java index 8efe03fb..0fc79763 100644 --- a/src/main/java/io/cryostat/agent/remote/EventTypesContext.java +++ b/src/main/java/io/cryostat/agent/remote/EventTypesContext.java @@ -50,25 +50,30 @@ public String path() { @Override public void handle(HttpExchange exchange) throws IOException { - String mtd = exchange.getRequestMethod(); - switch (mtd) { - case "GET": - try { - List events = getEventTypes(); + try { + String mtd = exchange.getRequestMethod(); + switch (mtd) { + case "GET": + List events = new ArrayList<>(); + try { + events.addAll(getEventTypes()); + } catch (Exception e) { + log.error("events serialization failure", e); + exchange.sendResponseHeaders(HttpStatus.SC_INTERNAL_SERVER_ERROR, 0); + break; + } exchange.sendResponseHeaders(HttpStatus.SC_OK, 0); try (OutputStream response = exchange.getResponseBody()) { mapper.writeValue(response, events); } - } catch (Exception e) { - log.error("events serialization failure", e); - } finally { - exchange.close(); - } - break; - default: - exchange.sendResponseHeaders(HttpStatus.SC_NOT_FOUND, -1); - exchange.close(); - break; + break; + default: + log.warn("Unknown request method {}", mtd); + exchange.sendResponseHeaders(HttpStatus.SC_METHOD_NOT_ALLOWED, -1); + break; + } + } finally { + exchange.close(); } } diff --git a/src/main/java/io/cryostat/agent/remote/MBeanContext.java b/src/main/java/io/cryostat/agent/remote/MBeanContext.java index 3b7bd1bd..c085df90 100644 --- a/src/main/java/io/cryostat/agent/remote/MBeanContext.java +++ b/src/main/java/io/cryostat/agent/remote/MBeanContext.java @@ -64,25 +64,27 @@ public String path() { @Override public void handle(HttpExchange exchange) throws IOException { - String mtd = exchange.getRequestMethod(); - switch (mtd) { - case "GET": - try { - MBeanMetrics metrics = getMBeanMetrics(); - exchange.sendResponseHeaders(HttpStatus.SC_OK, 0); - try (OutputStream response = exchange.getResponseBody()) { - mapper.writeValue(response, metrics); + try { + String mtd = exchange.getRequestMethod(); + switch (mtd) { + case "GET": + try { + MBeanMetrics metrics = getMBeanMetrics(); + exchange.sendResponseHeaders(HttpStatus.SC_OK, 0); + try (OutputStream response = exchange.getResponseBody()) { + mapper.writeValue(response, metrics); + } + } catch (Exception e) { + log.error("mbean serialization failure", e); } - } catch (Exception e) { - log.error("mbean serialization failure", e); - } finally { - exchange.close(); - } - break; - default: - exchange.sendResponseHeaders(HttpStatus.SC_NOT_FOUND, -1); - exchange.close(); - break; + break; + default: + log.warn("Unknown request method {}", mtd); + exchange.sendResponseHeaders(HttpStatus.SC_METHOD_NOT_ALLOWED, -1); + break; + } + } finally { + exchange.close(); } } diff --git a/src/main/java/io/cryostat/agent/remote/MutatingRemoteContext.java b/src/main/java/io/cryostat/agent/remote/MutatingRemoteContext.java new file mode 100644 index 00000000..89f65c67 --- /dev/null +++ b/src/main/java/io/cryostat/agent/remote/MutatingRemoteContext.java @@ -0,0 +1,60 @@ +/* + * 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.agent.remote; + +import io.cryostat.agent.ConfigModule; + +import io.smallrye.config.SmallRyeConfig; + +public abstract class MutatingRemoteContext implements RemoteContext { + + protected final SmallRyeConfig config; + + protected MutatingRemoteContext(SmallRyeConfig config) { + this.config = config; + } + + @Override + public boolean available() { + return apiWritesEnabled(config); + } + + public static boolean apiWritesEnabled(SmallRyeConfig config) { + return config.getValue(ConfigModule.CRYOSTAT_AGENT_API_WRITES_ENABLED, boolean.class); + } +} diff --git a/src/main/java/io/cryostat/agent/remote/RecordingsContext.java b/src/main/java/io/cryostat/agent/remote/RecordingsContext.java index 0a605cbd..d5c9cc96 100644 --- a/src/main/java/io/cryostat/agent/remote/RecordingsContext.java +++ b/src/main/java/io/cryostat/agent/remote/RecordingsContext.java @@ -15,19 +15,41 @@ */ package io.cryostat.agent.remote; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import javax.inject.Inject; +import org.openjdk.jmc.common.unit.IConstrainedMap; +import org.openjdk.jmc.common.unit.QuantityConversionException; +import org.openjdk.jmc.common.unit.UnitLookup; +import org.openjdk.jmc.flightrecorder.configuration.events.EventOptionID; +import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder; +import org.openjdk.jmc.rjmx.ServiceNotAvailableException; +import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService; + +import io.cryostat.agent.StringUtils; +import io.cryostat.core.FlightRecorderException; +import io.cryostat.core.net.JFRConnection; +import io.cryostat.core.net.JFRConnectionToolkit; +import io.cryostat.core.serialization.SerializableRecordingDescriptor; +import io.cryostat.core.templates.LocalStorageTemplateService; +import io.cryostat.core.templates.MutableTemplateService.InvalidEventTemplateException; +import io.cryostat.core.templates.MutableTemplateService.InvalidXmlException; +import io.cryostat.core.templates.RemoteTemplateService; +import io.cryostat.core.templates.Template; +import io.cryostat.core.templates.TemplateType; + import com.fasterxml.jackson.databind.ObjectMapper; import com.sun.net.httpserver.HttpExchange; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.smallrye.config.SmallRyeConfig; import jdk.jfr.FlightRecorder; -import jdk.jfr.Recording; import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,11 +57,21 @@ class RecordingsContext implements RemoteContext { private final Logger log = LoggerFactory.getLogger(getClass()); + private final SmallRyeConfig config; private final ObjectMapper mapper; + private final JFRConnectionToolkit jfrConnectionToolkit; + private final LocalStorageTemplateService localStorageTemplateService; @Inject - RecordingsContext(ObjectMapper mapper) { + RecordingsContext( + SmallRyeConfig config, + ObjectMapper mapper, + JFRConnectionToolkit jfrConnectionToolkit, + LocalStorageTemplateService localStorageTemplateService) { + this.config = config; this.mapper = mapper; + this.jfrConnectionToolkit = jfrConnectionToolkit; + this.localStorageTemplateService = localStorageTemplateService; } @Override @@ -49,67 +81,146 @@ public String path() { @Override public void handle(HttpExchange exchange) throws IOException { - String mtd = exchange.getRequestMethod(); - switch (mtd) { - case "GET": - try { - List recordings = getRecordings(); - exchange.sendResponseHeaders(HttpStatus.SC_OK, 0); + try { + String mtd = exchange.getRequestMethod(); + if (!ensureMethodAccepted(exchange)) { + return; + } + switch (mtd) { + case "GET": try (OutputStream response = exchange.getResponseBody()) { + List recordings = getRecordings(); + exchange.sendResponseHeaders(HttpStatus.SC_OK, 0); mapper.writeValue(response, recordings); + } catch (Exception e) { + log.error("recordings serialization failure", e); + } + break; + case "POST": + try (InputStream body = exchange.getRequestBody()) { + StartRecordingRequest req = + mapper.readValue(body, StartRecordingRequest.class); + if (!req.isValid()) { + exchange.sendResponseHeaders(HttpStatus.SC_BAD_REQUEST, -1); + return; + } + SerializableRecordingDescriptor recording = startRecording(req); + exchange.sendResponseHeaders(HttpStatus.SC_CREATED, 0); + try (OutputStream response = exchange.getResponseBody()) { + mapper.writeValue(response, recording); + } + } catch (QuantityConversionException + | ServiceNotAvailableException + | FlightRecorderException + | org.openjdk.jmc.rjmx.services.jfr.FlightRecorderException + | InvalidEventTemplateException + | InvalidXmlException + | IOException e) { + log.error("Failed to start recording", e); + exchange.sendResponseHeaders(HttpStatus.SC_INTERNAL_SERVER_ERROR, -1); } - } catch (Exception e) { - log.error("recordings serialization failure", e); - } finally { - exchange.close(); - } - break; - default: - exchange.sendResponseHeaders(HttpStatus.SC_NOT_FOUND, -1); - exchange.close(); - break; + break; + default: + log.warn("Unknown request method {}", mtd); + exchange.sendResponseHeaders(HttpStatus.SC_METHOD_NOT_ALLOWED, -1); + break; + } + } finally { + exchange.close(); + } + } + + private boolean ensureMethodAccepted(HttpExchange exchange) throws IOException { + Set blocked = Set.of("POST"); + String mtd = exchange.getRequestMethod(); + boolean restricted = blocked.contains(mtd); + if (!restricted) { + return true; + } + boolean passed = restricted && MutatingRemoteContext.apiWritesEnabled(config); + if (!passed) { + exchange.sendResponseHeaders(HttpStatus.SC_FORBIDDEN, -1); } + return passed; } - private List getRecordings() { + private List getRecordings() { return FlightRecorder.getFlightRecorder().getRecordings().stream() - .map(RecordingInfo::new) + .map(SerializableRecordingDescriptor::new) .collect(Collectors.toList()); } - @SuppressFBWarnings(value = "URF_UNREAD_FIELD") - private static class RecordingInfo { - - public final long id; - public final String name; - public final String state; - public final Map options; - public final long startTime; - public final long duration; - public final boolean isContinuous; - public final boolean toDisk; - public final long maxSize; - public final long maxAge; - - RecordingInfo(Recording rec) { - this.id = rec.getId(); - this.name = rec.getName(); - this.state = rec.getState().name(); - this.options = rec.getSettings(); - if (rec.getStartTime() != null) { - this.startTime = rec.getStartTime().toEpochMilli(); + private SerializableRecordingDescriptor startRecording(StartRecordingRequest req) + throws QuantityConversionException, ServiceNotAvailableException, + FlightRecorderException, + org.openjdk.jmc.rjmx.services.jfr.FlightRecorderException, + InvalidEventTemplateException, InvalidXmlException, IOException { + Runnable cleanup = () -> {}; + try { + JFRConnection conn = + jfrConnectionToolkit.connect( + jfrConnectionToolkit.createServiceURL("localhost", 0)); + IConstrainedMap events; + if (req.requestsCustomTemplate()) { + Template template = + localStorageTemplateService.addTemplate( + new ByteArrayInputStream( + req.template.getBytes(StandardCharsets.UTF_8))); + events = localStorageTemplateService.getEvents(template).orElseThrow(); + cleanup = + () -> { + try { + localStorageTemplateService.deleteTemplate(template); + } catch (InvalidEventTemplateException | IOException e) { + log.error("Failed to clean up template " + template.getName(), e); + } + }; } else { - this.startTime = 0; - } - this.isContinuous = rec.getDuration() == null; - this.duration = this.isContinuous ? 0 : rec.getDuration().toMillis(); - this.toDisk = rec.isToDisk(); - this.maxSize = rec.getMaxSize(); - if (rec.getMaxAge() != null) { - this.maxAge = rec.getMaxAge().toMillis(); - } else { - this.maxAge = 0; + events = + new RemoteTemplateService(conn) + .getEvents(req.localTemplateName, TemplateType.TARGET).stream() + .findFirst() + .orElseThrow(); } + IFlightRecorderService svc = conn.getService(); + return new SerializableRecordingDescriptor( + svc.start( + new RecordingOptionsBuilder(conn.getService()) + .name(req.name) + .duration(UnitLookup.MILLISECOND.quantity(req.duration)) + .maxSize(UnitLookup.BYTE.quantity(req.maxSize)) + .maxAge(UnitLookup.MILLISECOND.quantity(req.maxAge)) + .toDisk(true) + .build(), + events)); + } finally { + cleanup.run(); + } + } + + static class StartRecordingRequest { + + public String name; + public String localTemplateName; + public String template; + public long duration; + public long maxSize; + public long maxAge; + + boolean requestsCustomTemplate() { + return !StringUtils.isBlank(template); + } + + boolean requestsBundledTemplate() { + return !StringUtils.isBlank(localTemplateName); + } + + boolean isValid() { + boolean requestsCustomTemplate = requestsCustomTemplate(); + boolean requestsBundledTemplate = requestsBundledTemplate(); + boolean requestsEither = requestsCustomTemplate || requestsBundledTemplate; + boolean requestsBoth = requestsCustomTemplate && requestsBundledTemplate; + return requestsEither && !requestsBoth; } } } diff --git a/src/main/java/io/cryostat/agent/remote/RemoteContext.java b/src/main/java/io/cryostat/agent/remote/RemoteContext.java index 5290cd39..3b5a2271 100644 --- a/src/main/java/io/cryostat/agent/remote/RemoteContext.java +++ b/src/main/java/io/cryostat/agent/remote/RemoteContext.java @@ -19,4 +19,8 @@ public interface RemoteContext extends HttpHandler { String path(); + + default boolean available() { + return true; + } } diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index c1eac25e..3af7416f 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -1,6 +1,8 @@ cryostat.agent.app.name=cryostat-agent cryostat.agent.baseuri= +cryostat.agent.api.writes-enabled=false + cryostat.agent.webclient.ssl.trust-all=false cryostat.agent.webclient.ssl.verify-hostname=true cryostat.agent.webclient.connect.timeout-ms=1000