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 5a468a31..36ee97d3 100644 --- a/src/main/java/io/cryostat/agent/ConfigModule.java +++ b/src/main/java/io/cryostat/agent/ConfigModule.java @@ -107,6 +107,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 1169572a..974c5dbb 100644 --- a/src/main/java/io/cryostat/agent/MainModule.java +++ b/src/main/java/io/cryostat/agent/MainModule.java @@ -37,7 +37,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; @@ -61,6 +63,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; @@ -85,6 +88,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 @@ -280,21 +284,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(); @@ -305,4 +349,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 fa99c56f..b0bf8aaf 100644 --- a/src/main/java/io/cryostat/agent/WebServer.java +++ b/src/main/java/io/cryostat/agent/WebServer.java @@ -62,6 +62,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; @@ -122,13 +123,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(); } @@ -176,6 +179,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 @@ -185,23 +200,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 432391c2..ca0726e0 100644 --- a/src/main/java/io/cryostat/agent/remote/EventTemplatesContext.java +++ b/src/main/java/io/cryostat/agent/remote/EventTemplatesContext.java @@ -70,30 +70,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 9e329da1..1f3025c1 100644 --- a/src/main/java/io/cryostat/agent/remote/EventTypesContext.java +++ b/src/main/java/io/cryostat/agent/remote/EventTypesContext.java @@ -72,25 +72,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 d63aae8b..635619d7 100644 --- a/src/main/java/io/cryostat/agent/remote/MBeanContext.java +++ b/src/main/java/io/cryostat/agent/remote/MBeanContext.java @@ -86,25 +86,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 caa5c88c..9397ffd3 100644 --- a/src/main/java/io/cryostat/agent/remote/RecordingsContext.java +++ b/src/main/java/io/cryostat/agent/remote/RecordingsContext.java @@ -37,19 +37,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; @@ -57,11 +79,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 @@ -71,67 +103,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 c6c3df6e..fdfad60c 100644 --- a/src/main/java/io/cryostat/agent/remote/RemoteContext.java +++ b/src/main/java/io/cryostat/agent/remote/RemoteContext.java @@ -41,4 +41,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