diff --git a/pom.xml b/pom.xml
index df9911f149..e01af401ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -85,7 +85,7 @@
2.47
${com.google.dagger.version}
- 2.21.1
+ 2.22.0
15.4
3.12.0
diff --git a/smoketest.sh b/smoketest.sh
index d7a8ca71d3..30243d7bb3 100755
--- a/smoketest.sh
+++ b/smoketest.sh
@@ -235,6 +235,7 @@ runDemoApps() {
--env CRYOSTAT_AGENT_TRUST_ALL="true" \
--env CRYOSTAT_AGENT_AUTHORIZATION="Basic $(echo user:pass | base64)" \
--env CRYOSTAT_AGENT_REGISTRATION_PREFER_JMX="false" \
+ --env CRYOSTAT_AGENT_API_WRITES_ENABLED="true" \
--rm -d quay.io/andrewazores/quarkus-test:latest
# copy a jboss-client.jar into /clientlib first
diff --git a/src/main/java/io/cryostat/jmc/serialization/HyperlinkedSerializableRecordingDescriptor.java b/src/main/java/io/cryostat/jmc/serialization/HyperlinkedSerializableRecordingDescriptor.java
index 8d276e1cb9..000952c577 100644
--- a/src/main/java/io/cryostat/jmc/serialization/HyperlinkedSerializableRecordingDescriptor.java
+++ b/src/main/java/io/cryostat/jmc/serialization/HyperlinkedSerializableRecordingDescriptor.java
@@ -19,6 +19,7 @@
import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor;
import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor.RecordingState;
+import io.cryostat.core.serialization.SerializableRecordingDescriptor;
import io.cryostat.recordings.RecordingMetadataManager.Metadata;
import org.apache.commons.lang3.builder.EqualsBuilder;
diff --git a/src/main/java/io/cryostat/jmc/serialization/SerializableRecordingDescriptor.java b/src/main/java/io/cryostat/jmc/serialization/SerializableRecordingDescriptor.java
deleted file mode 100644
index 83ab728c51..0000000000
--- a/src/main/java/io/cryostat/jmc/serialization/SerializableRecordingDescriptor.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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.jmc.serialization;
-
-import org.openjdk.jmc.common.unit.QuantityConversionException;
-import org.openjdk.jmc.common.unit.UnitLookup;
-import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor;
-import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor.RecordingState;
-
-import org.apache.commons.lang3.builder.EqualsBuilder;
-import org.apache.commons.lang3.builder.HashCodeBuilder;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-
-public class SerializableRecordingDescriptor {
-
- protected long id;
- protected String name;
- protected RecordingState state;
- protected long startTime;
- protected long duration;
- protected boolean continuous;
- protected boolean toDisk;
- protected long maxSize;
- protected long maxAge;
-
- public SerializableRecordingDescriptor(IRecordingDescriptor orig)
- throws QuantityConversionException {
- this.id = orig.getId();
- this.name = orig.getName();
- this.state = orig.getState();
- this.startTime = orig.getStartTime().longValueIn(UnitLookup.EPOCH_MS);
- this.duration = orig.getDuration().longValueIn(UnitLookup.MILLISECOND);
- this.continuous = orig.isContinuous();
- this.toDisk = orig.getToDisk();
- this.maxSize = orig.getMaxSize().longValueIn(UnitLookup.BYTE);
- this.maxAge = orig.getMaxAge().longValueIn(UnitLookup.MILLISECOND);
- }
-
- public SerializableRecordingDescriptor(SerializableRecordingDescriptor o) {
- this.id = o.getId();
- this.name = o.getName();
- this.state = o.getState();
- this.startTime = o.getStartTime();
- this.duration = o.getDuration();
- this.continuous = o.isContinuous();
- this.toDisk = o.getToDisk();
- this.maxSize = o.getMaxSize();
- this.maxAge = o.getMaxAge();
- }
-
- public long getId() {
- return id;
- }
-
- public String getName() {
- return name;
- }
-
- public RecordingState getState() {
- return state;
- }
-
- public long getStartTime() {
- return startTime;
- }
-
- public long getDuration() {
- return duration;
- }
-
- public boolean isContinuous() {
- return continuous;
- }
-
- public boolean getToDisk() {
- return toDisk;
- }
-
- public long getMaxSize() {
- return maxSize;
- }
-
- public long getMaxAge() {
- return maxAge;
- }
-
- @Override
- public String toString() {
- return ToStringBuilder.reflectionToString(this);
- }
-
- @Override
- public int hashCode() {
- return HashCodeBuilder.reflectionHashCode(this);
- }
-
- @Override
- public boolean equals(Object o) {
- return EqualsBuilder.reflectionEquals(this, o);
- }
-}
diff --git a/src/main/java/io/cryostat/messaging/MessagingServer.java b/src/main/java/io/cryostat/messaging/MessagingServer.java
index 0c1a12c6f3..050ad757e0 100644
--- a/src/main/java/io/cryostat/messaging/MessagingServer.java
+++ b/src/main/java/io/cryostat/messaging/MessagingServer.java
@@ -35,7 +35,7 @@
import io.cryostat.messaging.notifications.NotificationFactory;
import io.cryostat.messaging.notifications.NotificationListener;
import io.cryostat.net.AuthManager;
-import io.cryostat.net.AuthorizationErrorException;
+import io.cryostat.net.AuthenticationErrorException;
import io.cryostat.net.HttpServer;
import io.cryostat.net.security.ResourceAction;
import io.cryostat.net.web.http.HttpMimeType;
@@ -132,7 +132,7 @@ public void start() throws SocketException, UnknownHostException {
.onFailure(
() ->
promise.fail(
- new AuthorizationErrorException(
+ new AuthenticationErrorException(
"")))
.execute();
} catch (InterruptedException
@@ -146,9 +146,9 @@ public void start() throws SocketException, UnknownHostException {
if (result.failed()) {
if (ExceptionUtils.hasCause(
result.cause(),
- AuthorizationErrorException.class)) {
+ AuthenticationErrorException.class)) {
logger.info(
- (AuthorizationErrorException)
+ (AuthenticationErrorException)
result.cause());
logger.info(
"Disconnected remote client {} due to"
diff --git a/src/main/java/io/cryostat/net/AgentApiException.java b/src/main/java/io/cryostat/net/AgentApiException.java
new file mode 100644
index 0000000000..8bd235e00c
--- /dev/null
+++ b/src/main/java/io/cryostat/net/AgentApiException.java
@@ -0,0 +1,22 @@
+/*
+ * 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.net;
+
+public class AgentApiException extends RuntimeException {
+ public AgentApiException(int statusCode) {
+ super(String.format("Unexpected HTTP response code %d", statusCode));
+ }
+}
diff --git a/src/main/java/io/cryostat/net/AgentClient.java b/src/main/java/io/cryostat/net/AgentClient.java
index eacde81eed..11a035d405 100644
--- a/src/main/java/io/cryostat/net/AgentClient.java
+++ b/src/main/java/io/cryostat/net/AgentClient.java
@@ -21,20 +21,19 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
-import javax.management.ObjectName;
import javax.script.ScriptException;
import org.openjdk.jmc.common.unit.IConstrainedMap;
import org.openjdk.jmc.common.unit.IConstraint;
+import org.openjdk.jmc.common.unit.IMutableConstrainedMap;
import org.openjdk.jmc.common.unit.IOptionDescriptor;
-import org.openjdk.jmc.common.unit.IQuantity;
import org.openjdk.jmc.common.unit.QuantityConversionException;
import org.openjdk.jmc.common.unit.SimpleConstrainedMap;
-import org.openjdk.jmc.common.unit.UnitLookup;
import org.openjdk.jmc.flightrecorder.configuration.events.EventOptionID;
import org.openjdk.jmc.flightrecorder.configuration.events.IEventTypeID;
import org.openjdk.jmc.flightrecorder.configuration.internal.EventTypeIDV2;
@@ -45,10 +44,14 @@
import io.cryostat.core.log.Logger;
import io.cryostat.core.net.Credentials;
import io.cryostat.core.net.MBeanMetrics;
+import io.cryostat.core.serialization.SerializableRecordingDescriptor;
+import io.cryostat.net.AgentJFRService.StartRecordingRequest;
import io.cryostat.util.HttpStatusCodeIdentifier;
import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
import io.vertx.core.Future;
+import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
@@ -57,6 +60,8 @@
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.codec.BodyCodec;
+import jdk.jfr.RecordingState;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.http.auth.InvalidCredentialsException;
@@ -99,38 +104,196 @@ Future ping() {
Future mbeanMetrics() {
Future> f =
- invoke(HttpMethod.GET, "/mbean-metrics", BodyCodec.string());
+ invoke(HttpMethod.GET, "/mbean-metrics/", BodyCodec.string());
return f.map(HttpResponse::body)
// uses Gson rather than Vertx's Jackson because Gson is able to handle MBeanMetrics
// with no additional fuss. Jackson complains about private final fields.
.map(s -> gson.fromJson(s, MBeanMetrics.class));
}
+ Future startRecording(StartRecordingRequest req) {
+ Future> f =
+ invoke(
+ HttpMethod.POST,
+ "/recordings/",
+ Buffer.buffer(gson.toJson(req)),
+ BodyCodec.string());
+ return f.map(
+ resp -> {
+ int statusCode = resp.statusCode();
+ if (HttpStatusCodeIdentifier.isSuccessCode(statusCode)) {
+ String body = resp.body();
+ return gson.fromJson(body, SerializableRecordingDescriptor.class)
+ .toJmcForm();
+ } else if (statusCode == 403) {
+ throw new AuthorizationErrorException(
+ new UnsupportedOperationException("startRecording"));
+ } else {
+ throw new AgentApiException(statusCode);
+ }
+ });
+ }
+
+ Future startSnapshot() {
+ StartRecordingRequest snapshotReq = new StartRecordingRequest("snapshot", "", "", 0, 0, 0);
+
+ Future> f =
+ invoke(
+ HttpMethod.POST,
+ "/recordings/",
+ Buffer.buffer(gson.toJson(snapshotReq)),
+ BodyCodec.string());
+
+ return f.map(
+ resp -> {
+ int statusCode = resp.statusCode();
+ if (HttpStatusCodeIdentifier.isSuccessCode(statusCode)) {
+ String body = resp.body();
+ return gson.fromJson(body, SerializableRecordingDescriptor.class)
+ .toJmcForm();
+ } else if (statusCode == 403) {
+ throw new AuthorizationErrorException(
+ new UnsupportedOperationException("startSnapshot"));
+ } else {
+ throw new AgentApiException(statusCode);
+ }
+ });
+ }
+
+ Future updateRecordingOptions(long id, IConstrainedMap newSettings) {
+ JsonObject jsonSettings = new JsonObject();
+ for (String key : newSettings.keySet()) {
+ Object value = newSettings.get(key);
+ if (value == null) {
+ continue;
+ }
+ if (value instanceof String && StringUtils.isBlank((String) value)) {
+ continue;
+ }
+ jsonSettings.put(key, value);
+ }
+ Future> f =
+ invoke(
+ HttpMethod.PATCH,
+ String.format("/recordings/%d", id),
+ jsonSettings.toBuffer(),
+ BodyCodec.none());
+
+ return f.map(
+ resp -> {
+ int statusCode = resp.statusCode();
+ if (HttpStatusCodeIdentifier.isSuccessCode(statusCode)) {
+ return null;
+ } else if (statusCode == 403) {
+ throw new AuthorizationErrorException(
+ new UnsupportedOperationException("updateRecordingOptions"));
+ } else {
+ throw new AgentApiException(statusCode);
+ }
+ });
+ }
+
+ Future openStream(long id) {
+ Future> f =
+ invoke(HttpMethod.GET, "/recordings/" + id, BodyCodec.buffer());
+ return f.map(
+ resp -> {
+ int statusCode = resp.statusCode();
+ if (HttpStatusCodeIdentifier.isSuccessCode(statusCode)) {
+ return resp.body();
+ } else if (statusCode == 403) {
+ throw new AuthorizationErrorException(
+ new UnsupportedOperationException("openStream"));
+ } else {
+ throw new AgentApiException(statusCode);
+ }
+ });
+ }
+
+ Future stopRecording(long id) {
+ // FIXME this is a terrible hack, the interfaces here should not require only an
+ // IConstrainedMap with IOptionDescriptors but allow us to pass other and more simply
+ // serializable data to the Agent, such as this recording state entry
+ IConstrainedMap map =
+ new IConstrainedMap() {
+ @Override
+ public Set keySet() {
+ return Set.of("state");
+ }
+
+ @Override
+ public Object get(String key) {
+ return RecordingState.STOPPED.name();
+ }
+
+ @Override
+ public IConstraint> getConstraint(String key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getPersistableString(String key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public IMutableConstrainedMap emptyWithSameConstraints() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public IMutableConstrainedMap mutableCopy() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ return updateRecordingOptions(id, map);
+ }
+
+ Future deleteRecording(long id) {
+ Future> f =
+ invoke(
+ HttpMethod.DELETE,
+ String.format("/recordings/%d", id),
+ Buffer.buffer(),
+ BodyCodec.none());
+ return f.map(
+ resp -> {
+ int statusCode = resp.statusCode();
+ if (HttpStatusCodeIdentifier.isSuccessCode(statusCode)) {
+ return null;
+ } else if (statusCode == 403) {
+ throw new AuthorizationErrorException(
+ new UnsupportedOperationException("deleteRecording"));
+ } else {
+ throw new AgentApiException(statusCode);
+ }
+ });
+ }
+
Future> activeRecordings() {
- Future> f =
- invoke(HttpMethod.GET, "/recordings", BodyCodec.jsonArray());
+ Future> f = invoke(HttpMethod.GET, "/recordings/", BodyCodec.string());
return f.map(HttpResponse::body)
.map(
- arr ->
- arr.stream()
- .map(
- o ->
- (IRecordingDescriptor)
- new AgentRecordingDescriptor(
- (JsonObject) o))
- .toList());
+ s ->
+ (List)
+ gson.fromJson(
+ s,
+ new TypeToken<
+ List<
+ SerializableRecordingDescriptor>>() {}.getType()))
+ .map(arr -> arr.stream().map(SerializableRecordingDescriptor::toJmcForm).toList());
}
Future> eventTypes() {
Future> f =
- invoke(HttpMethod.GET, "/event-types", BodyCodec.jsonArray());
+ invoke(HttpMethod.GET, "/event-types/", BodyCodec.jsonArray());
return f.map(HttpResponse::body)
.map(arr -> arr.stream().map(o -> new AgentEventTypeInfo((JsonObject) o)).toList());
}
Future> eventSettings() {
Future> f =
- invoke(HttpMethod.GET, "/event-settings", BodyCodec.jsonArray());
+ invoke(HttpMethod.GET, "/event-settings/", BodyCodec.jsonArray());
return f.map(HttpResponse::body)
.map(
arr -> {
@@ -184,11 +347,16 @@ Future> eventSettings() {
Future> eventTemplates() {
Future> f =
- invoke(HttpMethod.GET, "/event-templates", BodyCodec.jsonArray());
+ invoke(HttpMethod.GET, "/event-templates/", BodyCodec.jsonArray());
return f.map(HttpResponse::body).map(arr -> arr.stream().map(Object::toString).toList());
}
private Future> invoke(HttpMethod mtd, String path, BodyCodec codec) {
+ return invoke(mtd, path, null, codec);
+ }
+
+ private Future> invoke(
+ HttpMethod mtd, String path, Buffer payload, BodyCodec codec) {
return Future.fromCompletionStage(
CompletableFuture.supplyAsync(
() -> {
@@ -223,14 +391,21 @@ private Future> invoke(HttpMethod mtd, String path, BodyCode
credentials.getPassword()));
} catch (ScriptException | InvalidCredentialsException e) {
logger.error(e);
- throw new RuntimeException(e);
+ throw new IllegalStateException(e);
}
try {
- return req.send()
- .toCompletionStage()
- .toCompletableFuture()
- .get();
+ if (payload != null) {
+ return req.sendBuffer(payload)
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get();
+ } else {
+ return req.send()
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get();
+ }
} catch (InterruptedException | ExecutionException e) {
logger.error(e);
throw new RuntimeException(e);
@@ -273,97 +448,6 @@ AgentClient create(URI agentUri) {
}
}
- private static class AgentRecordingDescriptor implements IRecordingDescriptor {
-
- final JsonObject json;
-
- AgentRecordingDescriptor(JsonObject json) {
- this.json = json;
- }
-
- @Override
- public IQuantity getDataStartTime() {
- return getStartTime();
- }
-
- @Override
- public IQuantity getDataEndTime() {
- if (isContinuous()) {
- return UnitLookup.EPOCH_MS.quantity(0);
- }
- return getDataStartTime().add(getDuration());
- }
-
- @Override
- public IQuantity getDuration() {
- return UnitLookup.MILLISECOND.quantity(json.getLong("duration"));
- }
-
- @Override
- public Long getId() {
- return json.getLong("id");
- }
-
- @Override
- public IQuantity getMaxAge() {
- return UnitLookup.MILLISECOND.quantity(json.getLong("maxAge"));
- }
-
- @Override
- public IQuantity getMaxSize() {
- return UnitLookup.BYTE.quantity(json.getLong("maxSize"));
- }
-
- @Override
- public String getName() {
- return json.getString("name");
- }
-
- @Override
- public ObjectName getObjectName() {
- return null;
- }
-
- @Override
- public Map getOptions() {
- return json.getJsonObject("options").getMap();
- }
-
- @Override
- public IQuantity getStartTime() {
- return UnitLookup.EPOCH_MS.quantity(json.getLong("startTime"));
- }
-
- @Override
- public RecordingState getState() {
- // avoid using Enum.valueOf() since that throws an exception if the name isn't part of
- // the type, and it's nicer to not throw and catch exceptions
- String state = json.getString("state");
- switch (state) {
- case "CREATED":
- return RecordingState.CREATED;
- case "RUNNING":
- return RecordingState.RUNNING;
- case "STOPPING":
- return RecordingState.STOPPING;
- case "STOPPED":
- return RecordingState.STOPPED;
- default:
- return RecordingState.RUNNING;
- }
- }
-
- @Override
- public boolean getToDisk() {
- return json.getBoolean("toDisk");
- }
-
- @Override
- public boolean isContinuous() {
- return json.getBoolean("isContinuous");
- }
- }
-
private static class AgentEventTypeInfo implements IEventTypeInfo {
final JsonObject json;
diff --git a/src/main/java/io/cryostat/net/AgentConnection.java b/src/main/java/io/cryostat/net/AgentConnection.java
index 65ce5dd072..cce516b2d6 100644
--- a/src/main/java/io/cryostat/net/AgentConnection.java
+++ b/src/main/java/io/cryostat/net/AgentConnection.java
@@ -28,14 +28,16 @@
import org.openjdk.jmc.rjmx.ConnectionException;
import org.openjdk.jmc.rjmx.IConnectionHandle;
import org.openjdk.jmc.rjmx.ServiceNotAvailableException;
-import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService;
import io.cryostat.core.log.Logger;
+import io.cryostat.core.net.CryostatFlightRecorderService;
import io.cryostat.core.net.IDException;
import io.cryostat.core.net.JFRConnection;
import io.cryostat.core.net.MBeanMetrics;
import io.cryostat.core.sys.Clock;
-import io.cryostat.core.templates.RemoteTemplateService;
+import io.cryostat.core.sys.Environment;
+import io.cryostat.core.sys.FileSystem;
+import io.cryostat.core.templates.MergedTemplateService;
import io.cryostat.core.templates.TemplateService;
import io.cryostat.recordings.JvmIdHelper;
@@ -45,11 +47,20 @@ public class AgentConnection implements JFRConnection {
private final AgentClient client;
private final JvmIdHelper idHelper;
+ private final FileSystem fs;
+ private final Environment env;
private final Logger logger;
- AgentConnection(AgentClient client, JvmIdHelper idHelper, Logger logger) {
+ AgentConnection(
+ AgentClient client,
+ JvmIdHelper idHelper,
+ FileSystem fs,
+ Environment env,
+ Logger logger) {
this.client = client;
this.idHelper = idHelper;
+ this.fs = fs;
+ this.env = env;
this.logger = logger;
}
@@ -112,14 +123,14 @@ public int getPort() {
}
@Override
- public IFlightRecorderService getService()
+ public CryostatFlightRecorderService getService()
throws ConnectionException, IOException, ServiceNotAvailableException {
- return new AgentJFRService(client, logger);
+ return new AgentJFRService(client, (MergedTemplateService) getTemplateService(), logger);
}
@Override
public TemplateService getTemplateService() {
- return new RemoteTemplateService(this);
+ return new MergedTemplateService(this, fs, env);
}
@Override
@@ -141,16 +152,25 @@ public MBeanMetrics getMBeanMetrics()
public static class Factory {
private final AgentClient.Factory clientFactory;
private final JvmIdHelper idHelper;
+ private final FileSystem fs;
+ private final Environment env;
private final Logger logger;
- Factory(AgentClient.Factory clientFactory, JvmIdHelper idHelper, Logger logger) {
+ Factory(
+ AgentClient.Factory clientFactory,
+ JvmIdHelper idHelper,
+ FileSystem fs,
+ Environment env,
+ Logger logger) {
this.clientFactory = clientFactory;
this.idHelper = idHelper;
+ this.fs = fs;
+ this.env = env;
this.logger = logger;
}
AgentConnection createConnection(URI agentUri) {
- return new AgentConnection(clientFactory.create(agentUri), idHelper, logger);
+ return new AgentConnection(clientFactory.create(agentUri), idHelper, fs, env, logger);
}
}
}
diff --git a/src/main/java/io/cryostat/net/AgentJFRService.java b/src/main/java/io/cryostat/net/AgentJFRService.java
index bc6d68a9e7..1a25e58b9b 100644
--- a/src/main/java/io/cryostat/net/AgentJFRService.java
+++ b/src/main/java/io/cryostat/net/AgentJFRService.java
@@ -15,7 +15,11 @@
*/
package io.cryostat.net;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
import java.io.InputStream;
+import java.text.ParseException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -26,34 +30,53 @@
import org.openjdk.jmc.common.unit.IDescribedMap;
import org.openjdk.jmc.common.unit.IOptionDescriptor;
import org.openjdk.jmc.common.unit.IQuantity;
+import org.openjdk.jmc.common.unit.ITypedQuantity;
+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.events.IEventTypeID;
import org.openjdk.jmc.flightrecorder.configuration.internal.DefaultValueMap;
+import org.openjdk.jmc.flightrecorder.configuration.internal.KnownEventOptions;
+import org.openjdk.jmc.flightrecorder.configuration.internal.KnownRecordingOptions;
+import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder;
+import org.openjdk.jmc.rjmx.ConnectionException;
+import org.openjdk.jmc.rjmx.ServiceNotAvailableException;
import org.openjdk.jmc.rjmx.services.jfr.FlightRecorderException;
import org.openjdk.jmc.rjmx.services.jfr.IEventTypeInfo;
-import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService;
import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor;
+import io.cryostat.core.EventOptionsBuilder.EventOptionException;
+import io.cryostat.core.EventOptionsBuilder.EventTypeException;
import io.cryostat.core.log.Logger;
+import io.cryostat.core.net.CryostatFlightRecorderService;
+import io.cryostat.core.templates.MergedTemplateService;
+import io.cryostat.core.templates.Template;
+import io.cryostat.core.templates.TemplateType;
-class AgentJFRService implements IFlightRecorderService {
+import io.vertx.core.Future;
+import io.vertx.core.buffer.Buffer;
+import org.jsoup.nodes.Document;
+
+class AgentJFRService implements CryostatFlightRecorderService {
private final AgentClient client;
+ private final MergedTemplateService templateService;
private final Logger logger;
- AgentJFRService(AgentClient client, Logger logger) {
+ AgentJFRService(AgentClient client, MergedTemplateService templateService, Logger logger) {
this.client = client;
+ this.templateService = templateService;
this.logger = logger;
}
@Override
public IDescribedMap getDefaultEventOptions() {
- return new DefaultValueMap<>(Map.of());
+ return KnownEventOptions.OPTION_DEFAULTS_V2;
}
@Override
public IDescribedMap getDefaultRecordingOptions() {
- return new DefaultValueMap<>(Map.of());
+ return KnownRecordingOptions.OPTION_DEFAULTS_V2;
}
@Override
@@ -63,7 +86,14 @@ public String getVersion() {
@Override
public void close(IRecordingDescriptor descriptor) throws FlightRecorderException {
- throw new UnimplementedException();
+ try {
+ client.deleteRecording(descriptor.getId())
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new FlightRecorderException("Failed to stop recording", e);
+ }
}
@Override
@@ -85,8 +115,7 @@ public Collection extends IEventTypeInfo> getAvailableEventTypes()
@Override
public Map> getAvailableRecordingOptions()
throws FlightRecorderException {
- // TODO Auto-generated method stub
- return Map.of();
+ return KnownRecordingOptions.DESCRIPTORS_BY_KEY_V2;
}
@Override
@@ -145,7 +174,11 @@ public List getServerTemplates() throws FlightRecorderException {
@Override
public IRecordingDescriptor getSnapshotRecording() throws FlightRecorderException {
- throw new UnimplementedException();
+ try {
+ return client.startSnapshot().toCompletionStage().toCompletableFuture().get();
+ } catch (ExecutionException | InterruptedException e) {
+ throw new FlightRecorderException("Failed to create snapshot recording", e);
+ }
}
@Override
@@ -161,20 +194,31 @@ public boolean isEnabled() {
}
@Override
- public InputStream openStream(IRecordingDescriptor arg0, boolean arg1)
+ public InputStream openStream(IRecordingDescriptor descriptor, boolean removeOnClose)
throws FlightRecorderException {
- throw new UnimplementedException();
+ Future f = client.openStream(descriptor.getId());
+ try {
+ Buffer b = f.toCompletionStage().toCompletableFuture().get();
+ return new BufferedInputStream(new ByteArrayInputStream(b.getBytes()));
+ } catch (ExecutionException | InterruptedException e) {
+ logger.warn(e);
+ throw new FlightRecorderException("Failed to open remote recording stream", e);
+ }
}
@Override
- public InputStream openStream(IRecordingDescriptor arg0, IQuantity arg1, boolean arg2)
+ public InputStream openStream(
+ IRecordingDescriptor descriptor, IQuantity lastPartDuration, boolean removeOnClose)
throws FlightRecorderException {
throw new UnimplementedException();
}
@Override
public InputStream openStream(
- IRecordingDescriptor arg0, IQuantity arg1, IQuantity arg2, boolean arg3)
+ IRecordingDescriptor descriptor,
+ IQuantity startTime,
+ IQuantity endTime,
+ boolean removeOnClose)
throws FlightRecorderException {
throw new UnimplementedException();
}
@@ -187,8 +231,15 @@ public IRecordingDescriptor start(
}
@Override
- public void stop(IRecordingDescriptor arg0) throws FlightRecorderException {
- throw new UnimplementedException();
+ public void stop(IRecordingDescriptor descriptor) throws FlightRecorderException {
+ try {
+ client.stopRecording(descriptor.getId())
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new FlightRecorderException("Failed to stop recording", e);
+ }
}
@Override
@@ -198,10 +249,98 @@ public void updateEventOptions(IRecordingDescriptor arg0, IConstrainedMap arg1)
+ public void updateRecordingOptions(
+ IRecordingDescriptor recordingDescriptor, IConstrainedMap newSettings)
throws FlightRecorderException {
+ try {
+ long recordingId = recordingDescriptor.getId();
+ client.updateRecordingOptions(recordingId, newSettings)
+ .toCompletionStage()
+ .toCompletableFuture()
+ .get();
+ } catch (ExecutionException | InterruptedException e) {
+ throw new FlightRecorderException("Failed to update recording options", e);
+ }
+ }
+
+ @Override
+ public IRecordingDescriptor start(
+ IConstrainedMap recordingOptions,
+ String templateName,
+ TemplateType preferredTemplateType)
+ throws io.cryostat.core.FlightRecorderException, FlightRecorderException,
+ ConnectionException, IOException, ServiceNotAvailableException,
+ QuantityConversionException, EventOptionException, EventTypeException {
+ StartRecordingRequest req;
+ String recordingName = recordingOptions.get("name").toString();
+ long duration =
+ (Optional.ofNullable(
+ (ITypedQuantity)
+ recordingOptions.get(
+ RecordingOptionsBuilder.KEY_DURATION))
+ .orElse(UnitLookup.MILLISECOND.quantity(0)))
+ .longValueIn(UnitLookup.MILLISECOND);
+ long maxSize =
+ (Optional.ofNullable(
+ (ITypedQuantity)
+ recordingOptions.get(
+ RecordingOptionsBuilder.KEY_MAX_SIZE))
+ .orElse(UnitLookup.BYTE.quantity(0)))
+ .longValueIn(UnitLookup.BYTE);
+ long maxAge =
+ (Optional.ofNullable(
+ (ITypedQuantity)
+ recordingOptions.get(
+ RecordingOptionsBuilder.KEY_MAX_AGE))
+ .orElse(UnitLookup.MILLISECOND.quantity(0)))
+ .longValueIn(UnitLookup.MILLISECOND);
+ if (preferredTemplateType.equals(TemplateType.CUSTOM)) {
+ req =
+ new StartRecordingRequest(
+ recordingName,
+ null,
+ templateService
+ .getXml(templateName, preferredTemplateType)
+ .orElseThrow()
+ .outerHtml(),
+ duration,
+ maxSize,
+ maxAge);
+ } else {
+ req =
+ new StartRecordingRequest(
+ recordingName, templateName, null, duration, maxSize, maxAge);
+ }
+ try {
+ return client.startRecording(req).toCompletionStage().toCompletableFuture().get();
+ } catch (ExecutionException | InterruptedException e) {
+ throw new io.cryostat.core.FlightRecorderException(e);
+ }
+ }
+
+ @Override
+ public IRecordingDescriptor start(
+ IConstrainedMap recordingOptions, Template eventTemplate)
+ throws io.cryostat.core.FlightRecorderException, FlightRecorderException,
+ ConnectionException, IOException, FlightRecorderException,
+ ServiceNotAvailableException, QuantityConversionException, EventOptionException,
+ EventTypeException {
+ return CryostatFlightRecorderService.super.start(recordingOptions, eventTemplate);
+ }
+
+ @Override
+ public IRecordingDescriptor start(IConstrainedMap recordingOptions, Document template)
+ throws FlightRecorderException, ParseException, IOException {
throw new UnimplementedException();
}
public static class UnimplementedException extends IllegalStateException {}
+
+ static record StartRecordingRequest(
+ String name,
+ String localTemplateName,
+ String template,
+ long duration,
+ long maxSize,
+ long maxAge) {}
}
diff --git a/src/main/java/io/cryostat/net/AuthenticationErrorException.java b/src/main/java/io/cryostat/net/AuthenticationErrorException.java
new file mode 100644
index 0000000000..ebb58467e2
--- /dev/null
+++ b/src/main/java/io/cryostat/net/AuthenticationErrorException.java
@@ -0,0 +1,22 @@
+/*
+ * 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.net;
+
+public class AuthenticationErrorException extends Exception {
+ public AuthenticationErrorException(String msg) {
+ super(msg);
+ }
+}
diff --git a/src/main/java/io/cryostat/net/AuthorizationErrorException.java b/src/main/java/io/cryostat/net/AuthorizationErrorException.java
index 9b11927f7f..2ebc4f4c40 100644
--- a/src/main/java/io/cryostat/net/AuthorizationErrorException.java
+++ b/src/main/java/io/cryostat/net/AuthorizationErrorException.java
@@ -15,8 +15,8 @@
*/
package io.cryostat.net;
-public class AuthorizationErrorException extends Exception {
- public AuthorizationErrorException(String msg) {
- super(msg);
+public class AuthorizationErrorException extends RuntimeException {
+ public AuthorizationErrorException(Throwable cause) {
+ super(cause);
}
}
diff --git a/src/main/java/io/cryostat/net/NetworkModule.java b/src/main/java/io/cryostat/net/NetworkModule.java
index b7954b7641..765d387e0c 100644
--- a/src/main/java/io/cryostat/net/NetworkModule.java
+++ b/src/main/java/io/cryostat/net/NetworkModule.java
@@ -97,8 +97,12 @@ static Duration provideMaxTargetTTL(Environment env) {
@Provides
@Singleton
static AgentConnection.Factory provideAgentConnectionFactory(
- AgentClient.Factory clientFactory, JvmIdHelper idHelper, Logger logger) {
- return new AgentConnection.Factory(clientFactory, idHelper, logger);
+ AgentClient.Factory clientFactory,
+ JvmIdHelper idHelper,
+ FileSystem fs,
+ Environment env,
+ Logger logger) {
+ return new AgentConnection.Factory(clientFactory, idHelper, fs, env, logger);
}
@Provides
diff --git a/src/main/java/io/cryostat/net/openshift/OpenShiftAuthManager.java b/src/main/java/io/cryostat/net/openshift/OpenShiftAuthManager.java
index 528026c53a..e962e28534 100644
--- a/src/main/java/io/cryostat/net/openshift/OpenShiftAuthManager.java
+++ b/src/main/java/io/cryostat/net/openshift/OpenShiftAuthManager.java
@@ -44,8 +44,8 @@
import io.cryostat.core.log.Logger;
import io.cryostat.core.sys.Environment;
import io.cryostat.net.AbstractAuthManager;
+import io.cryostat.net.AuthenticationErrorException;
import io.cryostat.net.AuthenticationScheme;
-import io.cryostat.net.AuthorizationErrorException;
import io.cryostat.net.MissingEnvironmentVariableException;
import io.cryostat.net.PermissionDeniedException;
import io.cryostat.net.TokenNotFoundException;
@@ -178,7 +178,7 @@ public Future getUserInfo(Supplier httpHeaderProvider) {
TokenReviewStatus status = fStatus.get();
if (!Boolean.TRUE.equals(status.getAuthenticated())) {
return CompletableFuture.failedFuture(
- new AuthorizationErrorException("Authentication Failed"));
+ new AuthenticationErrorException("Authentication Failed"));
}
return CompletableFuture.completedFuture(new UserInfo(status.getUser().getUsername()));
} catch (ExecutionException ee) {
@@ -203,7 +203,7 @@ public Optional getLoginRedirectUrl(
} catch (ExecutionException ee) {
Throwable cause = ee.getCause();
if (cause instanceof PermissionDeniedException
- || cause instanceof AuthorizationErrorException
+ || cause instanceof AuthenticationErrorException
|| cause instanceof KubernetesClientException) {
return Optional.of(this.computeAuthorizationEndpoint().get());
}
@@ -413,7 +413,7 @@ private Future performTokenReview(String token) {
TokenReviewStatus status = review.getStatus();
if (StringUtils.isNotBlank(status.getError())) {
return CompletableFuture.failedFuture(
- new AuthorizationErrorException(status.getError()));
+ new AuthenticationErrorException(status.getError()));
}
return CompletableFuture.completedFuture(status);
} catch (KubernetesClientException e) {
diff --git a/src/main/java/io/cryostat/net/web/WebServer.java b/src/main/java/io/cryostat/net/web/WebServer.java
index 278ad84e18..1346904500 100644
--- a/src/main/java/io/cryostat/net/web/WebServer.java
+++ b/src/main/java/io/cryostat/net/web/WebServer.java
@@ -38,7 +38,6 @@
import io.cryostat.MainModule;
import io.cryostat.core.log.Logger;
import io.cryostat.core.net.JFRConnection;
-import io.cryostat.net.AgentConnection;
import io.cryostat.net.AuthManager;
import io.cryostat.net.HttpServer;
import io.cryostat.net.NetworkConfiguration;
@@ -50,6 +49,7 @@
import io.cryostat.net.web.http.api.ApiVersion;
import io.cryostat.net.web.http.api.v2.ApiException;
import io.cryostat.util.HttpStatusCodeIdentifier;
+import io.cryostat.util.URIUtil;
import com.google.gson.Gson;
import io.vertx.core.AbstractVerticle;
@@ -296,13 +296,7 @@ public String getAssetDownloadURL(ApiVersion apiVersion, String... pathSegments)
}
private String getTargetId(JFRConnection conn) throws IOException {
- // TODO this is a hack, the JFRConnection interface should be refactored to expose a more
- // general connection URL / targetId method since the JMX implementation is now only one
- // possible implementation
- if (conn instanceof AgentConnection) {
- return ((AgentConnection) conn).getUri().toString();
- }
- return conn.getJMXURL().toString();
+ return URIUtil.getConnectionUri(conn).toString();
}
public FileUpload getTempFileUpload(
diff --git a/src/main/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandler.java b/src/main/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandler.java
index 848535fc07..c268c3842f 100644
--- a/src/main/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandler.java
+++ b/src/main/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandler.java
@@ -35,6 +35,7 @@
import io.cryostat.core.log.Logger;
import io.cryostat.core.net.Credentials;
import io.cryostat.net.AuthManager;
+import io.cryostat.net.AuthenticationErrorException;
import io.cryostat.net.AuthorizationErrorException;
import io.cryostat.net.ConnectionDescriptor;
import io.cryostat.net.PermissionDeniedException;
@@ -81,8 +82,11 @@ public void handle(RoutingContext ctx) {
} catch (ApiException | HttpException e) {
throw e;
} catch (Exception e) {
- if (isAuthFailure(e)) {
- throw new HttpException(401, "HTTP Authorization Failure", e);
+ if (isAuthenticationFailure(e)) {
+ throw new HttpException(401, "HTTP Unauthorized", e);
+ }
+ if (isAuthorizationFailure(e)) {
+ throw new HttpException(403, "HTTP Forbidden", e);
}
if (isTargetConnectionFailure(e)) {
handleConnectionException(ctx, e);
@@ -148,14 +152,18 @@ public static boolean isTargetConnectionFailure(Exception e) {
|| ExceptionUtils.indexOfType(e, FlightRecorderException.class) >= 0;
}
- public static boolean isAuthFailure(Exception e) {
+ public static boolean isAuthenticationFailure(Exception e) {
// Check if the Exception has a PermissionDeniedException or KubernetesClientException
// in its cause chain
- return ExceptionUtils.indexOfType(e, PermissionDeniedException.class) >= 0
- || ExceptionUtils.indexOfType(e, AuthorizationErrorException.class) >= 0
+ return ExceptionUtils.indexOfType(e, AuthenticationErrorException.class) >= 0
|| ExceptionUtils.indexOfType(e, KubernetesClientException.class) >= 0;
}
+ public static boolean isAuthorizationFailure(Exception e) {
+ return ExceptionUtils.indexOfType(e, PermissionDeniedException.class) >= 0
+ || ExceptionUtils.indexOfType(e, AuthorizationErrorException.class) >= 0;
+ }
+
public static boolean isJmxAuthFailure(Exception e) {
return ExceptionUtils.indexOfType(e, SecurityException.class) >= 0
|| ExceptionUtils.indexOfType(e, SaslException.class) >= 0;
diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/AbstractV2RequestHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/AbstractV2RequestHandler.java
index a0f3e757f2..f8172d07ea 100644
--- a/src/main/java/io/cryostat/net/web/http/api/v2/AbstractV2RequestHandler.java
+++ b/src/main/java/io/cryostat/net/web/http/api/v2/AbstractV2RequestHandler.java
@@ -87,8 +87,11 @@ public final void handle(RoutingContext ctx) {
} catch (ApiException | HttpException e) {
throw e;
} catch (Exception e) {
- if (AbstractAuthenticatedRequestHandler.isAuthFailure(e)) {
- throw new ApiException(401, "HTTP Authorization Failure", e);
+ if (AbstractAuthenticatedRequestHandler.isAuthenticationFailure(e)) {
+ throw new ApiException(401, "HTTP Unauthorized", e);
+ }
+ if (AbstractAuthenticatedRequestHandler.isAuthorizationFailure(e)) {
+ throw new ApiException(403, "HTTP Forbidden", e);
}
if (AbstractAuthenticatedRequestHandler.isTargetConnectionFailure(e)) {
handleConnectionException(ctx, e);
diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/graph/AbstractPermissionedDataFetcher.java b/src/main/java/io/cryostat/net/web/http/api/v2/graph/AbstractPermissionedDataFetcher.java
index f623eec279..e50061fc4f 100644
--- a/src/main/java/io/cryostat/net/web/http/api/v2/graph/AbstractPermissionedDataFetcher.java
+++ b/src/main/java/io/cryostat/net/web/http/api/v2/graph/AbstractPermissionedDataFetcher.java
@@ -18,7 +18,7 @@
import java.util.Set;
import io.cryostat.net.AuthManager;
-import io.cryostat.net.AuthorizationErrorException;
+import io.cryostat.net.AuthenticationErrorException;
import io.cryostat.net.security.PermissionedAction;
import graphql.GraphQLContext;
@@ -53,7 +53,7 @@ public final T get(DataFetchingEnvironment environment) throws Exception {
resourceActions())
.get();
if (!authenticated) {
- throw new AuthorizationErrorException("Unauthorized");
+ throw new AuthenticationErrorException("Unauthorized");
}
return getAuthenticated(environment);
}
diff --git a/src/main/java/io/cryostat/recordings/EventOptionsBuilder.java b/src/main/java/io/cryostat/recordings/EventOptionsBuilder.java
deleted file mode 100644
index 9266df3d48..0000000000
--- a/src/main/java/io/cryostat/recordings/EventOptionsBuilder.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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.recordings;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.Supplier;
-
-import org.openjdk.jmc.common.unit.IConstrainedMap;
-import org.openjdk.jmc.common.unit.IConstraint;
-import org.openjdk.jmc.common.unit.IMutableConstrainedMap;
-import org.openjdk.jmc.common.unit.IOptionDescriptor;
-import org.openjdk.jmc.flightrecorder.configuration.events.EventOptionID;
-import org.openjdk.jmc.flightrecorder.configuration.events.IEventTypeID;
-import org.openjdk.jmc.rjmx.IConnectionHandle;
-import org.openjdk.jmc.rjmx.services.jfr.IEventTypeInfo;
-import org.openjdk.jmc.rjmx.services.jfr.internal.FlightRecorderServiceV2;
-
-import io.cryostat.core.net.JFRConnection;
-import io.cryostat.core.tui.ClientWriter;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-public class EventOptionsBuilder {
-
- private final boolean isV2;
- private final IMutableConstrainedMap map;
- private Map>> knownTypes;
- private Map eventIds;
-
- // Testing only
- EventOptionsBuilder(ClientWriter cw, JFRConnection connection, Supplier v2)
- throws Exception {
- this.isV2 = v2.get();
- this.map = connection.getService().getDefaultEventOptions().emptyWithSameConstraints();
- knownTypes = new HashMap<>();
- eventIds = new HashMap<>();
-
- if (!isV2) {
- cw.println("Flight Recorder V1 is not yet supported");
- }
-
- for (IEventTypeInfo eventTypeInfo : connection.getService().getAvailableEventTypes()) {
- eventIds.put(
- eventTypeInfo.getEventTypeID().getFullKey(), eventTypeInfo.getEventTypeID());
- knownTypes.putIfAbsent(
- eventTypeInfo.getEventTypeID(),
- new HashMap<>(eventTypeInfo.getOptionDescriptors()));
- }
- }
-
- public EventOptionsBuilder addEvent(String typeId, String option, String value)
- throws Exception {
- if (!eventIds.containsKey(typeId)) {
- throw new EventTypeException(typeId);
- }
- Map> optionDescriptors = knownTypes.get(eventIds.get(typeId));
- if (!optionDescriptors.containsKey(option)) {
- throw new EventOptionException(typeId, option);
- }
- IConstraint> constraint = optionDescriptors.get(option).getConstraint();
- Object parsedValue = constraint.parseInteractive(value);
- constraint.validate(capture(parsedValue));
- this.map.put(new EventOptionID(eventIds.get(typeId), option), parsedValue);
-
- return this;
- }
-
- static V capture(T t) {
- // TODO clean up this generics hack
- return (V) t;
- }
-
- @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "Field is never mutated")
- public IConstrainedMap build() {
- if (!isV2) {
- return null;
- }
- return map;
- }
-
- public static class EventTypeException extends Exception {
- EventTypeException(String eventType) {
- super(String.format("Unknown event type \"%s\"", eventType));
- }
- }
-
- static class EventOptionException extends Exception {
- EventOptionException(String eventType, String option) {
- super(String.format("Unknown option \"%s\" for event \"%s\"", option, eventType));
- }
- }
-
- public static class Factory {
- private final ClientWriter cw;
-
- public Factory(ClientWriter cw) {
- this.cw = cw;
- }
-
- public EventOptionsBuilder create(JFRConnection connection) throws Exception {
- IConnectionHandle handle = connection.getHandle();
- return new EventOptionsBuilder(
- cw, connection, () -> FlightRecorderServiceV2.isAvailable(handle));
- }
- }
-}
diff --git a/src/main/java/io/cryostat/recordings/RecordingArchiveHelper.java b/src/main/java/io/cryostat/recordings/RecordingArchiveHelper.java
index 5ca7130d5f..c5d45fab29 100644
--- a/src/main/java/io/cryostat/recordings/RecordingArchiveHelper.java
+++ b/src/main/java/io/cryostat/recordings/RecordingArchiveHelper.java
@@ -916,7 +916,7 @@ private void validateRecordingPath(
Path writeRecordingToDestination(JFRConnection connection, IRecordingDescriptor descriptor)
throws IOException, URISyntaxException, FlightRecorderException, Exception {
- URI serviceUri = URIUtil.convert(connection.getJMXURL());
+ URI serviceUri = URIUtil.getConnectionUri(connection);
String jvmId = jvmIdHelper.getJvmId(serviceUri.toString());
Path specificRecordingsPath = getRecordingSubdirectoryPath(jvmId);
if (!fs.exists(specificRecordingsPath)) {
diff --git a/src/main/java/io/cryostat/recordings/RecordingTargetHelper.java b/src/main/java/io/cryostat/recordings/RecordingTargetHelper.java
index e0e9e7bdbe..1b01c98ef1 100644
--- a/src/main/java/io/cryostat/recordings/RecordingTargetHelper.java
+++ b/src/main/java/io/cryostat/recordings/RecordingTargetHelper.java
@@ -19,7 +19,6 @@
import java.io.InputStream;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
@@ -30,12 +29,11 @@
import java.util.stream.Collectors;
import org.openjdk.jmc.common.unit.IConstrainedMap;
-import org.openjdk.jmc.flightrecorder.configuration.events.EventOptionID;
import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder;
-import org.openjdk.jmc.rjmx.services.jfr.IEventTypeInfo;
import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor;
import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor.RecordingState;
+import io.cryostat.core.EventOptionsBuilder;
import io.cryostat.core.log.Logger;
import io.cryostat.core.net.JFRConnection;
import io.cryostat.core.templates.Template;
@@ -167,12 +165,7 @@ public IRecordingDescriptor startRecording(
IRecordingDescriptor desc =
connection
.getService()
- .start(
- recordingOptions,
- enableEvents(
- connection,
- templateName,
- preferredTemplateType));
+ .start(recordingOptions, templateName, preferredTemplateType);
String targetId = connectionDescriptor.getTargetId();
Map labels = metadata.getLabels();
@@ -240,21 +233,16 @@ public Future> getRecording(
targetConnectionManager.executeConnectedTask(
connectionDescriptor,
conn ->
- conn.getService().getAvailableRecordings().stream()
- .filter(r -> Objects.equals(recordingName, r.getName()))
+ getDescriptorByName(conn, recordingName)
.map(
desc -> {
try {
return conn.getService()
.openStream(desc, false);
} catch (Exception e) {
- logger.error(e);
- return null;
+ throw new RuntimeException(e);
}
- })
- .filter(Objects::nonNull)
- .findFirst());
-
+ }));
future.complete(recording);
} catch (Exception e) {
future.completeExceptionally(e);
@@ -280,9 +268,7 @@ public IRecordingDescriptor stopRecording(
connectionDescriptor,
connection -> {
Optional descriptor =
- connection.getService().getAvailableRecordings().stream()
- .filter(recording -> recording.getName().equals(recordingName))
- .findFirst();
+ getDescriptorByName(connection, recordingName);
if (descriptor.isPresent()) {
IRecordingDescriptor d = descriptor.get();
if (d.getState().equals(RecordingState.STOPPED) && quiet) {
@@ -530,30 +516,6 @@ private TemplateType getPreferredTemplateType(
String.format("Invalid/unknown event template %s", templateName));
}
- private IConstrainedMap enableEvents(
- JFRConnection connection, String templateName, TemplateType templateType)
- throws Exception {
- if (templateName.equals("ALL")) {
- return enableAllEvents(connection);
- }
- // if template type not specified, try to find a Custom template by that name. If none,
- // fall back on finding a Target built-in template by the name. If not, throw an
- // exception and bail out.
- TemplateType type = getPreferredTemplateType(connection, templateName, templateType);
- return connection.getTemplateService().getEvents(templateName, type).get();
- }
-
- private IConstrainedMap enableAllEvents(JFRConnection connection)
- throws Exception {
- EventOptionsBuilder builder = eventOptionsBuilderFactory.create(connection);
-
- for (IEventTypeInfo eventTypeInfo : connection.getService().getAvailableEventTypes()) {
- builder.addEvent(eventTypeInfo.getEventTypeID().getFullKey(), "enabled", "true");
- }
-
- return builder.build();
- }
-
private void scheduleRecordingTasks(
String recordingName,
long delay,
@@ -595,7 +557,11 @@ private void scheduleRecordingTasks(
connection, name));
return linked;
}
- return null;
+ throw new IllegalStateException(
+ String.format(
+ "Could not find expected recording"
+ + " named \"%s\" in target %s",
+ recordingName, targetId));
});
promise.complete(linkedDesc);
} catch (Exception e) {
diff --git a/src/main/java/io/cryostat/recordings/RecordingsModule.java b/src/main/java/io/cryostat/recordings/RecordingsModule.java
index 9f1fcbb598..1ab3e655ea 100644
--- a/src/main/java/io/cryostat/recordings/RecordingsModule.java
+++ b/src/main/java/io/cryostat/recordings/RecordingsModule.java
@@ -33,6 +33,7 @@
import io.cryostat.configuration.ConfigurationModule;
import io.cryostat.configuration.CredentialsManager;
import io.cryostat.configuration.Variables;
+import io.cryostat.core.EventOptionsBuilder;
import io.cryostat.core.RecordingOptionsCustomizer;
import io.cryostat.core.log.Logger;
import io.cryostat.core.sys.Clock;
diff --git a/src/main/java/io/cryostat/util/URIUtil.java b/src/main/java/io/cryostat/util/URIUtil.java
index 30a76c39a5..d794d325a9 100644
--- a/src/main/java/io/cryostat/util/URIUtil.java
+++ b/src/main/java/io/cryostat/util/URIUtil.java
@@ -15,11 +15,15 @@
*/
package io.cryostat.util;
+import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import javax.management.remote.JMXServiceURL;
+import io.cryostat.core.net.JFRConnection;
+import io.cryostat.net.AgentConnection;
+
public class URIUtil {
private URIUtil() {}
@@ -48,4 +52,14 @@ public static URI getRmiTarget(JMXServiceURL serviceUrl) throws URISyntaxExcepti
}
return new URI(pathPart.substring("/jndi/".length(), pathPart.length()));
}
+
+ public static URI getConnectionUri(JFRConnection connection) throws IOException {
+ // TODO this is a hack, the JFRConnection interface should be refactored to expose a more
+ // general connection URL / targetId method since the JMX implementation is now only one
+ // possible implementation
+ if (connection instanceof AgentConnection) {
+ return ((AgentConnection) connection).getUri();
+ }
+ return URI.create(connection.getJMXURL().toString());
+ }
}
diff --git a/src/test/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandlerTest.java b/src/test/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandlerTest.java
index 007a24f9a1..724175bd81 100644
--- a/src/test/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandlerTest.java
+++ b/src/test/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandlerTest.java
@@ -87,7 +87,7 @@ void shouldPutDefaultContentTypeHeader() {
}
@Test
- void shouldThrow401IfAuthFails() {
+ void shouldThrow401IfAuthorizationFails() {
when(auth.validateHttpHeader(Mockito.any(), Mockito.any()))
.thenReturn(CompletableFuture.completedFuture(false));
@@ -96,7 +96,7 @@ void shouldThrow401IfAuthFails() {
}
@Test
- void shouldThrow401IfAuthFails2() {
+ void shouldThrow403IfAuthenticationFails() {
when(auth.validateHttpHeader(Mockito.any(), Mockito.any()))
.thenReturn(
CompletableFuture.failedFuture(
@@ -104,11 +104,11 @@ void shouldThrow401IfAuthFails2() {
"namespace", "resourc.group", "verb", "reason")));
HttpException ex = Assertions.assertThrows(HttpException.class, () -> handler.handle(ctx));
- MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(401));
+ MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(403));
}
@Test
- void shouldThrow401IfAuthFails3() {
+ void shouldThrow401IfAuthenticationFails2() {
when(auth.validateHttpHeader(Mockito.any(), Mockito.any()))
.thenReturn(CompletableFuture.failedFuture(new KubernetesClientException("test")));
@@ -117,7 +117,7 @@ void shouldThrow401IfAuthFails3() {
}
@Test
- void shouldThrow401IfAuthFails4() {
+ void shouldThrow403IfAuthorizationFails2() {
// Check a doubly-nested PermissionDeniedException
when(auth.validateHttpHeader(Mockito.any(), Mockito.any()))
.thenReturn(
@@ -127,11 +127,11 @@ void shouldThrow401IfAuthFails4() {
"namespace", "resource.group", "verb", "reason"))));
HttpException ex = Assertions.assertThrows(HttpException.class, () -> handler.handle(ctx));
- MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(401));
+ MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(403));
}
@Test
- void shouldThrow401IfAuthFails5() {
+ void shouldThrow401IfAuthenticationFails3() {
// Check doubly-nested KubernetesClientException with its own cause
when(auth.validateHttpHeader(Mockito.any(), Mockito.any()))
.thenReturn(
diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetEventsGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetEventsGetHandlerTest.java
index d6d4565142..95cb5e5b8d 100644
--- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetEventsGetHandlerTest.java
+++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetEventsGetHandlerTest.java
@@ -23,11 +23,11 @@
import org.openjdk.jmc.flightrecorder.configuration.events.IEventTypeID;
import org.openjdk.jmc.rjmx.services.jfr.IEventTypeInfo;
-import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService;
import io.cryostat.MainModule;
import io.cryostat.configuration.CredentialsManager;
import io.cryostat.core.log.Logger;
+import io.cryostat.core.net.CryostatFlightRecorderService;
import io.cryostat.core.net.JFRConnection;
import io.cryostat.jmc.serialization.SerializableEventTypeInfo;
import io.cryostat.net.AuthManager;
@@ -107,7 +107,7 @@ void shouldRespondWithErrorIfExceptionThrown() throws Exception {
@Test
void shouldRespondWithEventsList() throws Exception {
JFRConnection connection = Mockito.mock(JFRConnection.class);
- IFlightRecorderService service = Mockito.mock(IFlightRecorderService.class);
+ CryostatFlightRecorderService service = Mockito.mock(CryostatFlightRecorderService.class);
IEventTypeInfo event1 = Mockito.mock(IEventTypeInfo.class);
IEventTypeID eventTypeId1 = Mockito.mock(IEventTypeID.class);
diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingGetHandlerTest.java
index 9d392d2070..257a633876 100644
--- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingGetHandlerTest.java
+++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingGetHandlerTest.java
@@ -28,10 +28,9 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
-import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService;
-
import io.cryostat.configuration.CredentialsManager;
import io.cryostat.core.log.Logger;
+import io.cryostat.core.net.CryostatFlightRecorderService;
import io.cryostat.core.net.JFRConnection;
import io.cryostat.net.AuthManager;
import io.cryostat.net.HttpServer;
@@ -78,7 +77,7 @@ class TargetRecordingGetHandlerTest {
@Mock Logger logger;
@Mock JFRConnection connection;
- @Mock IFlightRecorderService service;
+ @Mock CryostatFlightRecorderService service;
@BeforeEach
void setup() {
diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsGetHandlerTest.java
index a17c2ae386..9931a51408 100644
--- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsGetHandlerTest.java
+++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsGetHandlerTest.java
@@ -20,11 +20,11 @@
import org.openjdk.jmc.common.unit.IConstrainedMap;
import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder;
-import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService;
import io.cryostat.MainModule;
import io.cryostat.configuration.CredentialsManager;
import io.cryostat.core.log.Logger;
+import io.cryostat.core.net.CryostatFlightRecorderService;
import io.cryostat.core.net.JFRConnection;
import io.cryostat.net.AuthManager;
import io.cryostat.net.ConnectionDescriptor;
@@ -151,7 +151,7 @@ public Map answer(InvocationOnMock args) throws Throwable {
resp.putHeader(
Mockito.any(CharSequence.class), Mockito.any(CharSequence.class)))
.thenReturn(resp);
- IFlightRecorderService service = Mockito.mock(IFlightRecorderService.class);
+ CryostatFlightRecorderService service = Mockito.mock(CryostatFlightRecorderService.class);
Mockito.when(jfrConnection.getService()).thenReturn(service);
handler.handleAuthenticated(ctx);
diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchHandlerTest.java
index fa18b44b18..cabe9981b4 100644
--- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchHandlerTest.java
+++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchHandlerTest.java
@@ -21,12 +21,12 @@
import org.openjdk.jmc.common.unit.IConstrainedMap;
import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder;
-import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService;
import io.cryostat.configuration.CredentialsManager;
import io.cryostat.core.RecordingOptionsCustomizer;
import io.cryostat.core.RecordingOptionsCustomizer.OptionKey;
import io.cryostat.core.log.Logger;
+import io.cryostat.core.net.CryostatFlightRecorderService;
import io.cryostat.core.net.JFRConnection;
import io.cryostat.net.AuthManager;
import io.cryostat.net.ConnectionDescriptor;
@@ -136,7 +136,7 @@ public Map answer(InvocationOnMock args) throws Throwable {
Mockito.when(req.formAttributes()).thenReturn(requestAttrs);
HttpServerResponse resp = Mockito.mock(HttpServerResponse.class);
Mockito.when(ctx.response()).thenReturn(resp);
- IFlightRecorderService service = Mockito.mock(IFlightRecorderService.class);
+ CryostatFlightRecorderService service = Mockito.mock(CryostatFlightRecorderService.class);
Mockito.when(jfrConnection.getService()).thenReturn(service);
handler.handleAuthenticated(ctx);
@@ -182,7 +182,7 @@ public Map answer(InvocationOnMock args) throws Throwable {
Mockito.when(req.formAttributes()).thenReturn(requestAttrs);
HttpServerResponse resp = Mockito.mock(HttpServerResponse.class);
Mockito.when(ctx.response()).thenReturn(resp);
- IFlightRecorderService service = Mockito.mock(IFlightRecorderService.class);
+ CryostatFlightRecorderService service = Mockito.mock(CryostatFlightRecorderService.class);
Mockito.when(jfrConnection.getService()).thenReturn(service);
handler.handleAuthenticated(ctx);
diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingUploadPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingUploadPostHandlerTest.java
index 69bda34d8a..6c18b04a0a 100644
--- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingUploadPostHandlerTest.java
+++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingUploadPostHandlerTest.java
@@ -22,11 +22,11 @@
import java.util.Set;
import java.util.concurrent.CompletableFuture;
-import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService;
import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor;
import io.cryostat.configuration.CredentialsManager;
import io.cryostat.core.log.Logger;
+import io.cryostat.core.net.CryostatFlightRecorderService;
import io.cryostat.core.net.JFRConnection;
import io.cryostat.core.sys.Environment;
import io.cryostat.core.sys.FileSystem;
@@ -164,7 +164,7 @@ void shouldThrowExceptionIfRecordingNotFound() throws Exception {
((TargetConnectionManager.ConnectedTask