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 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) arg0.getArgument(1)) .execute(conn)); - IFlightRecorderService svc = Mockito.mock(IFlightRecorderService.class); + CryostatFlightRecorderService svc = Mockito.mock(CryostatFlightRecorderService.class); Mockito.when(conn.getService()).thenReturn(svc); Mockito.when(svc.getAvailableRecordings()).thenReturn(Collections.emptyList()); Mockito.when(env.getEnv("GRAFANA_DATASOURCE_URL")).thenReturn(DATASOURCE_URL); @@ -195,7 +195,7 @@ void shouldDoUpload() throws Exception { ((TargetConnectionManager.ConnectedTask) arg0.getArgument(1)) .execute(conn)); - IFlightRecorderService svc = Mockito.mock(IFlightRecorderService.class); + CryostatFlightRecorderService svc = Mockito.mock(CryostatFlightRecorderService.class); IRecordingDescriptor rec = Mockito.mock(IRecordingDescriptor.class); InputStream stream = Mockito.mock(InputStream.class); Mockito.when(conn.getService()).thenReturn(svc); @@ -266,7 +266,7 @@ void shouldHandleInvalidResponseStatusCode() throws Exception { ((TargetConnectionManager.ConnectedTask) arg0.getArgument(1)) .execute(conn)); - IFlightRecorderService svc = Mockito.mock(IFlightRecorderService.class); + CryostatFlightRecorderService svc = Mockito.mock(CryostatFlightRecorderService.class); IRecordingDescriptor rec = Mockito.mock(IRecordingDescriptor.class); InputStream stream = Mockito.mock(InputStream.class); Mockito.when(conn.getService()).thenReturn(svc); @@ -340,7 +340,7 @@ void shouldHandleNullStatusMessage() throws Exception { ((TargetConnectionManager.ConnectedTask) arg0.getArgument(1)) .execute(conn)); - IFlightRecorderService svc = Mockito.mock(IFlightRecorderService.class); + CryostatFlightRecorderService svc = Mockito.mock(CryostatFlightRecorderService.class); IRecordingDescriptor rec = Mockito.mock(IRecordingDescriptor.class); InputStream stream = Mockito.mock(InputStream.class); Mockito.when(conn.getService()).thenReturn(svc); @@ -414,7 +414,7 @@ void shouldHandleNullResponseBody() throws Exception { ((TargetConnectionManager.ConnectedTask) arg0.getArgument(1)) .execute(conn)); - IFlightRecorderService svc = Mockito.mock(IFlightRecorderService.class); + CryostatFlightRecorderService svc = Mockito.mock(CryostatFlightRecorderService.class); IRecordingDescriptor rec = Mockito.mock(IRecordingDescriptor.class); InputStream stream = Mockito.mock(InputStream.class); Mockito.when(conn.getService()).thenReturn(svc); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsGetHandlerTest.java index 36250d8101..66468f0c29 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsGetHandlerTest.java @@ -21,12 +21,12 @@ import org.openjdk.jmc.common.unit.IQuantity; import org.openjdk.jmc.common.unit.QuantityConversionException; -import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService; import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor; 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.HyperlinkedSerializableRecordingDescriptor; import io.cryostat.net.AuthManager; @@ -120,7 +120,7 @@ void shouldRespondWithErrorIfExceptionThrown() throws Exception { @Test void shouldRespondWithRecordingsList() throws Exception { JFRConnection connection = Mockito.mock(JFRConnection.class); - IFlightRecorderService service = Mockito.mock(IFlightRecorderService.class); + CryostatFlightRecorderService service = Mockito.mock(CryostatFlightRecorderService.class); Mockito.when( connectionManager.executeConnectedTask( diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandlerTest.java index bc0726d60d..2580ce05ca 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandlerTest.java @@ -25,12 +25,12 @@ import org.openjdk.jmc.common.unit.IQuantity; import org.openjdk.jmc.common.unit.QuantityConversionException; import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder; -import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService; import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor; 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.core.templates.TemplateService; import io.cryostat.core.templates.TemplateType; @@ -82,7 +82,7 @@ class TargetRecordingsPostHandlerTest { Gson gson = MainModule.provideGson(logger); @Mock JFRConnection connection; - @Mock IFlightRecorderService service; + @Mock CryostatFlightRecorderService service; @Mock TemplateService templateService; @Mock RoutingContext ctx; @Mock HttpServerRequest req; diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/TargetEventsGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/TargetEventsGetHandlerTest.java index 621d295536..2b6b896ba0 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/TargetEventsGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/TargetEventsGetHandlerTest.java @@ -27,11 +27,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; @@ -59,7 +59,7 @@ class TargetEventsGetHandlerTest { @Mock CredentialsManager credentialsManager; @Mock TargetConnectionManager targetConnectionManager; @Mock Logger logger; - @Mock IFlightRecorderService service; + @Mock CryostatFlightRecorderService service; @Mock JFRConnection connection; Gson gson = MainModule.provideGson(logger); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/TargetRecordingGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/TargetRecordingGetHandlerTest.java index fefa310de5..d6aa5b6f25 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/TargetRecordingGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/TargetRecordingGetHandlerTest.java @@ -23,11 +23,11 @@ import java.util.Random; import org.openjdk.jmc.rjmx.services.jfr.FlightRecorderException; -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.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; @@ -139,7 +139,7 @@ class Behaviour { @Mock RoutingContext ctx; @Mock JWT token; @Mock JFRConnection conn; - @Mock IFlightRecorderService svc; + @Mock CryostatFlightRecorderService svc; @Test void shouldRespond404IfNotFound() throws Exception { diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/TargetRecordingOptionsListGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/TargetRecordingOptionsListGetHandlerTest.java index ed576cb2e3..e60b68b1dc 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/TargetRecordingOptionsListGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/TargetRecordingOptionsListGetHandlerTest.java @@ -23,11 +23,11 @@ import java.util.concurrent.CompletableFuture; import org.openjdk.jmc.common.unit.IOptionDescriptor; -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; @@ -59,7 +59,7 @@ class TargetRecordingOptionsListGetHandlerTest { @Mock AuthManager auth; @Mock CredentialsManager credentialsManager; @Mock TargetConnectionManager targetConnectionManager; - @Mock IFlightRecorderService service; + @Mock CryostatFlightRecorderService service; @Mock JFRConnection connection; @Mock Logger logger; Gson gson = MainModule.provideGson(logger); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/graph/AbstractPermissionedDataFetcherTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/graph/AbstractPermissionedDataFetcherTest.java index e040582b24..27eaab0917 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/graph/AbstractPermissionedDataFetcherTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/graph/AbstractPermissionedDataFetcherTest.java @@ -21,7 +21,7 @@ import java.util.concurrent.CompletableFuture; import io.cryostat.net.AuthManager; -import io.cryostat.net.AuthorizationErrorException; +import io.cryostat.net.AuthenticationErrorException; import io.cryostat.net.security.ResourceAction; import graphql.GraphQLContext; @@ -65,8 +65,8 @@ void shouldThrowAuthorizationError() throws Exception { when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(false)); - AuthorizationErrorException ex = - Assertions.assertThrows(AuthorizationErrorException.class, () -> fetcher.get(env)); + AuthenticationErrorException ex = + Assertions.assertThrows(AuthenticationErrorException.class, () -> fetcher.get(env)); MatcherAssert.assertThat(ex.getMessage(), Matchers.equalTo("Unauthorized")); } diff --git a/src/test/java/io/cryostat/recordings/RecordingArchiveHelperTest.java b/src/test/java/io/cryostat/recordings/RecordingArchiveHelperTest.java index 5ce61543e0..3d694c8619 100644 --- a/src/test/java/io/cryostat/recordings/RecordingArchiveHelperTest.java +++ b/src/test/java/io/cryostat/recordings/RecordingArchiveHelperTest.java @@ -34,10 +34,10 @@ import javax.management.remote.JMXServiceURL; -import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService; import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor; import io.cryostat.core.log.Logger; +import io.cryostat.core.net.CryostatFlightRecorderService; import io.cryostat.core.net.JFRConnection; import io.cryostat.core.sys.Clock; import io.cryostat.core.sys.FileSystem; @@ -91,7 +91,7 @@ class RecordingArchiveHelperTest { @Mock Notification notification; @Mock Notification.Builder notificationBuilder; @Mock JFRConnection connection; - @Mock IFlightRecorderService service; + @Mock CryostatFlightRecorderService service; @Mock Vertx vertx; @Mock io.vertx.core.file.FileSystem vertxFs; diff --git a/src/test/java/io/cryostat/recordings/RecordingTargetHelperTest.java b/src/test/java/io/cryostat/recordings/RecordingTargetHelperTest.java index 1b330dece9..411a2c8c41 100644 --- a/src/test/java/io/cryostat/recordings/RecordingTargetHelperTest.java +++ b/src/test/java/io/cryostat/recordings/RecordingTargetHelperTest.java @@ -35,11 +35,12 @@ import org.openjdk.jmc.common.unit.QuantityConversionException; import org.openjdk.jmc.flightrecorder.configuration.events.EventOptionID; import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder; -import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService; 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.CryostatFlightRecorderService; import io.cryostat.core.net.JFRConnection; import io.cryostat.core.templates.TemplateService; import io.cryostat.core.templates.TemplateType; @@ -90,7 +91,7 @@ public class RecordingTargetHelperTest { @Mock Logger logger; @Mock JFRConnection connection; - @Mock IFlightRecorderService service; + @Mock CryostatFlightRecorderService service; @BeforeEach void setup() { @@ -710,13 +711,10 @@ public Object answer(InvocationOnMock invocation) throws Throwable { Mockito.when(service.getAvailableRecordings()) .thenReturn(Collections.emptyList(), List.of(recordingDescriptor)); - Mockito.when(service.start(Mockito.any(), Mockito.any())).thenReturn(recordingDescriptor); - - TemplateService templateService = Mockito.mock(TemplateService.class); - IConstrainedMap events = Mockito.mock(IConstrainedMap.class); - Mockito.when(connection.getTemplateService()).thenReturn(templateService); - Mockito.when(templateService.getEvents(Mockito.any(), Mockito.any())) - .thenReturn(Optional.of(events)); + Mockito.when( + service.start( + Mockito.any(), Mockito.eq(templateName), Mockito.eq(templateType))) + .thenReturn(recordingDescriptor); Mockito.when( recordingMetadataManager.setRecordingMetadata( @@ -739,7 +737,8 @@ public Future answer(InvocationOnMock invocation) metadata, false); - Mockito.verify(service).start(Mockito.any(), Mockito.any()); + Mockito.verify(service) + .start(Mockito.any(), Mockito.eq(templateName), Mockito.eq(templateType)); HyperlinkedSerializableRecordingDescriptor linkedDesc = new HyperlinkedSerializableRecordingDescriptor(recordingDescriptor, null, null); @@ -810,7 +809,8 @@ void shouldReplaceExistingRecording() throws Exception { List existingRecordings = List.of(existingRecording); Mockito.when(service.getAvailableRecordings()).thenReturn(existingRecordings); - Mockito.when(service.start(Mockito.any(), Mockito.any())).thenReturn(existingRecording); + Mockito.when(service.start(Mockito.any(), Mockito.anyString(), Mockito.any())) + .thenReturn(existingRecording); Mockito.when(existingRecording.getState()).thenReturn(RecordingState.STOPPED); @@ -840,7 +840,7 @@ void shouldReplaceExistingRecording() throws Exception { false); Mockito.verify(service).close(existingRecording); - Mockito.verify(service).start(Mockito.any(), Mockito.any()); + Mockito.verify(service).start(Mockito.any(), Mockito.anyString(), Mockito.any()); // Verify notification not sent because recording exists and no new recording is created Mockito.verify(notification, Mockito.times(0)).send(); @@ -872,16 +872,11 @@ void shouldCloseAndRecreateIfRecordingExistsAndIsRunning() throws Exception { List existingRecordings = List.of(existingRecording); Mockito.when(service.getAvailableRecordings()).thenReturn(existingRecordings); - Mockito.when(service.start(Mockito.any(), Mockito.any())).thenReturn(existingRecording); + Mockito.when(service.start(Mockito.any(), Mockito.anyString(), Mockito.any())) + .thenReturn(existingRecording); Mockito.when(existingRecording.getState()).thenReturn(RecordingState.RUNNING); - TemplateService templateService = Mockito.mock(TemplateService.class); - IConstrainedMap events = Mockito.mock(IConstrainedMap.class); - Mockito.when(connection.getTemplateService()).thenReturn(templateService); - Mockito.when(templateService.getEvents(Mockito.any(), Mockito.any())) - .thenReturn(Optional.of(events)); - Mockito.when( recordingMetadataManager.setRecordingMetadata( Mockito.any(), Mockito.anyString(), Mockito.any(Metadata.class))) @@ -902,7 +897,7 @@ void shouldCloseAndRecreateIfRecordingExistsAndIsRunning() throws Exception { false); Mockito.verify(service).close(existingRecording); - Mockito.verify(service).start(Mockito.any(), Mockito.any()); + Mockito.verify(service).start(Mockito.any(), Mockito.anyString(), Mockito.any()); } @Test @@ -931,16 +926,11 @@ void shouldRestartRecordingWhenRecordingExistsAndIsStopped() throws Exception { List existingRecordings = List.of(existingRecording); Mockito.when(service.getAvailableRecordings()).thenReturn(existingRecordings); - Mockito.when(service.start(Mockito.any(), Mockito.any())).thenReturn(existingRecording); + Mockito.when(service.start(Mockito.any(), Mockito.anyString(), Mockito.any())) + .thenReturn(existingRecording); Mockito.when(existingRecording.getState()).thenReturn(RecordingState.STOPPED); - TemplateService templateService = Mockito.mock(TemplateService.class); - IConstrainedMap events = Mockito.mock(IConstrainedMap.class); - Mockito.when(connection.getTemplateService()).thenReturn(templateService); - Mockito.when(templateService.getEvents(Mockito.any(), Mockito.any())) - .thenReturn(Optional.of(events)); - Mockito.when( recordingMetadataManager.setRecordingMetadata( Mockito.any(), Mockito.anyString(), Mockito.any(Metadata.class))) @@ -961,7 +951,7 @@ void shouldRestartRecordingWhenRecordingExistsAndIsStopped() throws Exception { false); Mockito.verify(service).close(existingRecording); - Mockito.verify(service).start(Mockito.any(), Mockito.any()); + Mockito.verify(service).start(Mockito.any(), Mockito.anyString(), Mockito.any()); Mockito.verify(notification).send(); Mockito.verifyNoMoreInteractions(recordingOptions); } diff --git a/src/test/java/io/cryostat/rules/RuleProcessorTest.java b/src/test/java/io/cryostat/rules/RuleProcessorTest.java index bcdc5c6a80..c3b1bf7de6 100644 --- a/src/test/java/io/cryostat/rules/RuleProcessorTest.java +++ b/src/test/java/io/cryostat/rules/RuleProcessorTest.java @@ -26,7 +26,6 @@ import org.openjdk.jmc.common.unit.IConstrainedMap; import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder; -import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService; import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor; import io.cryostat.FakeScheduledExecutorService; @@ -34,6 +33,7 @@ import io.cryostat.configuration.CredentialsManager.CredentialsEvent; import io.cryostat.core.log.Logger; import io.cryostat.core.net.Credentials; +import io.cryostat.core.net.CryostatFlightRecorderService; import io.cryostat.core.net.JFRConnection; import io.cryostat.core.net.discovery.JvmDiscoveryClient.EventKind; import io.cryostat.core.templates.TemplateType; @@ -82,7 +82,7 @@ class RuleProcessorTest { @Mock Logger logger; @Mock JFRConnection connection; - @Mock IFlightRecorderService service; + @Mock CryostatFlightRecorderService service; @BeforeEach void setup() {