Skip to content

Commit

Permalink
feat(api): mutation operations (cryostatio#175)
Browse files Browse the repository at this point in the history
* end paths with / so as to not match by prefix

* only allow GET requests if write-operations are not enabled

* feat(api): implement GET /recordings/:id for streaming files

* feat(api): enable dynamic JFR start (cryostatio#165)

* feat(api): enable dynamic JFR stop, delete (cryostatio#176)

Co-authored-by: Ming Wang <miwan@redhat.com>
  • Loading branch information
andrewazores and mwangggg authored Sep 19, 2023
1 parent 8db9b4c commit 7132572
Show file tree
Hide file tree
Showing 11 changed files with 663 additions and 142 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ and how it advertises itself to a Cryostat server instance. Required properties

- [x] `cryostat.agent.baseuri` [`java.net.URI`]: the URL location of the Cryostat server backend that this agent advertises itself to.
- [x] `cryostat.agent.callback` [`java.net.URI`]: a URL pointing back to this agent, ex. `"https://12.34.56.78:1234/"`. Cryostat will use this URL to perform health checks and request updates from the agent. This reflects the externally-visible IP address/hostname and port where this application and agent can be found.
- [ ] `cryostat.agent.api.writes-enabled` [`boolean`]: Control whether the agent accepts "write" or mutating operations on its HTTP API. Requests for remote operations such as dynamically starting Flight Recordings will be rejected unless this is set. Default `false`.
- [ ] `cryostat.agent.instance-id` [`String`]: a unique ID for this agent instance. This will be used to uniquely identify the agent in the Cryostat discovery database, as well as to unambiguously match its encrypted stored credentials. The default is a random UUID string. It is not recommended to override this value.
- [ ] `cryostat.agent.hostname` [`String`]: the hostname for this application instance. This will be used for the published JMX connection URL. If not provided then the default is to attempt to resolve the localhost hostname.
- [ ] `cryostat.agent.realm` [`String`]: the Cryostat Discovery API "realm" that this agent belongs to. This should be unique per agent instance. The default is the value of `cryostat.agent.app.name`.
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/io/cryostat/agent/ConfigModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ public abstract class ConfigModule {
public static final String CRYOSTAT_AGENT_HARVESTER_MAX_SIZE_B =
"cryostat.agent.harvester.max-size-b";

public static final String CRYOSTAT_AGENT_API_WRITES_ENABLED =
"cryostat.agent.api.writes-enabled";

@Provides
@Singleton
public static SmallRyeConfig provideConfig() {
Expand Down
73 changes: 62 additions & 11 deletions src/main/java/io/cryostat/agent/MainModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
*/
package io.cryostat.agent;

import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
Expand All @@ -39,6 +41,7 @@
import io.cryostat.core.net.JFRConnectionToolkit;
import io.cryostat.core.sys.Environment;
import io.cryostat.core.sys.FileSystem;
import io.cryostat.core.templates.LocalStorageTemplateService;
import io.cryostat.core.tui.ClientWriter;

import com.fasterxml.jackson.databind.DeserializationFeature;
Expand All @@ -65,6 +68,7 @@ public abstract class MainModule {
// one for outbound HTTP requests, one for incoming HTTP requests, and one as a general worker
private static final int NUM_WORKER_THREADS = 3;
private static final String JVM_ID = "JVM_ID";
private static final String TEMPLATES_PATH = "TEMPLATES_PATH";

@Provides
@Singleton
Expand Down Expand Up @@ -268,21 +272,61 @@ public static Harvester provideHarvester(
registration);
}

@Provides
@Singleton
public static FileSystem provideFileSystem() {
return new FileSystem();
}

@Provides
@Singleton
@Named(TEMPLATES_PATH)
public static Path provideTemplatesTmpPath(FileSystem fs) {
try {
return fs.createTempDirectory(null);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Provides
@Singleton
public static Environment provideEnvironment(@Named(TEMPLATES_PATH) Path templatesTmp) {
return new Environment() {
@Override
public String getEnv(String key) {
if (LocalStorageTemplateService.TEMPLATE_PATH.equals(key)) {
return templatesTmp.toString();
}
return super.getEnv(key);
}
};
}

@Provides
@Singleton
public static ClientWriter provideClientWriter() {
Logger log = LoggerFactory.getLogger(JFRConnectionToolkit.class);
return new ClientWriter() {
@Override
public void print(String msg) {
log.info(msg);
}
};
}

@Provides
@Singleton
public static JFRConnectionToolkit provideJfrConnectionToolkit(
ClientWriter cw, FileSystem fs, Environment env) {
return new JFRConnectionToolkit(cw, fs, env);
}

@Provides
@Singleton
@Named(JVM_ID)
public static String provideJvmId() {
public static String provideJvmId(JFRConnectionToolkit tk) {
Logger log = LoggerFactory.getLogger(JFRConnectionToolkit.class);
JFRConnectionToolkit tk =
new JFRConnectionToolkit(
new ClientWriter() {
@Override
public void print(String msg) {
log.warn(msg);
}
},
new FileSystem(),
new Environment());
try {
try (JFRConnection connection = tk.connect(tk.createServiceURL("localhost", 0))) {
String id = connection.getJvmId();
Expand All @@ -293,4 +337,11 @@ public void print(String msg) {
throw new RuntimeException(e);
}
}

@Provides
@Singleton
public static LocalStorageTemplateService provideLocalStorageTemplateService(
FileSystem fs, Environment env) {
return new LocalStorageTemplateService(fs, env);
}
}
72 changes: 46 additions & 26 deletions src/main/java/io/cryostat/agent/WebServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import com.sun.net.httpserver.Filter;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import dagger.Lazy;
import org.apache.http.HttpStatus;
Expand Down Expand Up @@ -97,13 +98,15 @@ void start() throws IOException, NoSuchAlgorithmException {

Set<RemoteContext> mergedContexts = new HashSet<>(remoteContexts.get());
mergedContexts.add(new PingContext(registration));
mergedContexts.forEach(
rc -> {
HttpContext ctx = this.http.createContext(rc.path(), rc::handle);
ctx.setAuthenticator(agentAuthenticator);
ctx.getFilters().add(requestLoggingFilter);
ctx.getFilters().add(compressionFilter);
});
mergedContexts.stream()
.filter(RemoteContext::available)
.forEach(
rc -> {
HttpContext ctx = this.http.createContext(rc.path(), wrap(rc::handle));
ctx.setAuthenticator(agentAuthenticator);
ctx.getFilters().add(requestLoggingFilter);
ctx.getFilters().add(compressionFilter);
});

this.http.start();
}
Expand Down Expand Up @@ -145,6 +148,19 @@ CompletableFuture<Void> generateCredentials() throws NoSuchAlgorithmException {
});
}

private HttpHandler wrap(HttpHandler handler) {
return x -> {
try {
handler.handle(x);
} catch (Exception e) {
log.error("Unhandled exception", e);
x.sendResponseHeaders(
HttpStatus.SC_INTERNAL_SERVER_ERROR, RemoteContext.BODY_LENGTH_NONE);
x.close();
}
};
}

private class PingContext implements RemoteContext {

private final Lazy<Registration> registration;
Expand All @@ -160,25 +176,29 @@ public String path() {

@Override
public void handle(HttpExchange exchange) throws IOException {
String mtd = exchange.getRequestMethod();
switch (mtd) {
case "POST":
synchronized (WebServer.this.credentials) {
exchange.sendResponseHeaders(HttpStatus.SC_NO_CONTENT, -1);
exchange.close();
this.registration
.get()
.notify(Registration.RegistrationEvent.State.REFRESHING);
}
break;
case "GET":
exchange.sendResponseHeaders(HttpStatus.SC_NO_CONTENT, -1);
exchange.close();
break;
default:
exchange.sendResponseHeaders(HttpStatus.SC_NOT_FOUND, -1);
exchange.close();
break;
try {
String mtd = exchange.getRequestMethod();
switch (mtd) {
case "POST":
synchronized (WebServer.this.credentials) {
exchange.sendResponseHeaders(
HttpStatus.SC_NO_CONTENT, BODY_LENGTH_NONE);
this.registration
.get()
.notify(Registration.RegistrationEvent.State.REFRESHING);
}
break;
case "GET":
exchange.sendResponseHeaders(HttpStatus.SC_NO_CONTENT, BODY_LENGTH_NONE);
break;
default:
log.warn("Unknown request method {}", mtd);
exchange.sendResponseHeaders(
HttpStatus.SC_METHOD_NOT_ALLOWED, BODY_LENGTH_NONE);
break;
}
} finally {
exchange.close();
}
}
}
Expand Down
39 changes: 21 additions & 18 deletions src/main/java/io/cryostat/agent/remote/EventTemplatesContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,35 +43,38 @@ class EventTemplatesContext implements RemoteContext {

@Override
public String path() {
return "/event-templates";
return "/event-templates/";
}

@Override
public void handle(HttpExchange exchange) throws IOException {
String mtd = exchange.getRequestMethod();
switch (mtd) {
case "GET":
try {
exchange.sendResponseHeaders(HttpStatus.SC_OK, 0);
try (OutputStream response = exchange.getResponseBody()) {
try {
String mtd = exchange.getRequestMethod();
switch (mtd) {
case "GET":
try {
FlightRecorderMXBean bean =
ManagementFactory.getPlatformMXBean(FlightRecorderMXBean.class);
List<String> xmlTexts =
bean.getConfigurations().stream()
.map(ConfigurationInfo::getContents)
.collect(Collectors.toList());
mapper.writeValue(response, xmlTexts);
exchange.sendResponseHeaders(HttpStatus.SC_OK, BODY_LENGTH_UNKNOWN);
try (OutputStream response = exchange.getResponseBody()) {
mapper.writeValue(response, xmlTexts);
}
} catch (Exception e) {
log.error("events serialization failure", e);
}
} catch (Exception e) {
log.error("events serialization failure", e);
} finally {
exchange.close();
}
break;
default:
exchange.sendResponseHeaders(HttpStatus.SC_NOT_FOUND, -1);
exchange.close();
break;
break;
default:
log.warn("Unknown request method {}", mtd);
exchange.sendResponseHeaders(
HttpStatus.SC_METHOD_NOT_ALLOWED, BODY_LENGTH_NONE);
break;
}
} finally {
exchange.close();
}
}
}
41 changes: 24 additions & 17 deletions src/main/java/io/cryostat/agent/remote/EventTypesContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,37 @@ class EventTypesContext implements RemoteContext {

@Override
public String path() {
return "/event-types";
return "/event-types/";
}

@Override
public void handle(HttpExchange exchange) throws IOException {
String mtd = exchange.getRequestMethod();
switch (mtd) {
case "GET":
try {
List<EventInfo> events = getEventTypes();
exchange.sendResponseHeaders(HttpStatus.SC_OK, 0);
try {
String mtd = exchange.getRequestMethod();
switch (mtd) {
case "GET":
List<EventInfo> events = new ArrayList<>();
try {
events.addAll(getEventTypes());
} catch (Exception e) {
log.error("events serialization failure", e);
exchange.sendResponseHeaders(
HttpStatus.SC_INTERNAL_SERVER_ERROR, BODY_LENGTH_NONE);
break;
}
exchange.sendResponseHeaders(HttpStatus.SC_OK, BODY_LENGTH_UNKNOWN);
try (OutputStream response = exchange.getResponseBody()) {
mapper.writeValue(response, events);
}
} catch (Exception e) {
log.error("events serialization failure", e);
} finally {
exchange.close();
}
break;
default:
exchange.sendResponseHeaders(HttpStatus.SC_NOT_FOUND, -1);
exchange.close();
break;
break;
default:
log.warn("Unknown request method {}", mtd);
exchange.sendResponseHeaders(
HttpStatus.SC_METHOD_NOT_ALLOWED, BODY_LENGTH_NONE);
break;
}
} finally {
exchange.close();
}
}

Expand Down
41 changes: 22 additions & 19 deletions src/main/java/io/cryostat/agent/remote/MBeanContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,30 +59,33 @@ class MBeanContext implements RemoteContext {

@Override
public String path() {
return "/mbean-metrics";
return "/mbean-metrics/";
}

@Override
public void handle(HttpExchange exchange) throws IOException {
String mtd = exchange.getRequestMethod();
switch (mtd) {
case "GET":
try {
MBeanMetrics metrics = getMBeanMetrics();
exchange.sendResponseHeaders(HttpStatus.SC_OK, 0);
try (OutputStream response = exchange.getResponseBody()) {
mapper.writeValue(response, metrics);
try {
String mtd = exchange.getRequestMethod();
switch (mtd) {
case "GET":
try {
MBeanMetrics metrics = getMBeanMetrics();
exchange.sendResponseHeaders(HttpStatus.SC_OK, BODY_LENGTH_UNKNOWN);
try (OutputStream response = exchange.getResponseBody()) {
mapper.writeValue(response, metrics);
}
} catch (Exception e) {
log.error("mbean serialization failure", e);
}
} catch (Exception e) {
log.error("mbean serialization failure", e);
} finally {
exchange.close();
}
break;
default:
exchange.sendResponseHeaders(HttpStatus.SC_NOT_FOUND, -1);
exchange.close();
break;
break;
default:
log.warn("Unknown request method {}", mtd);
exchange.sendResponseHeaders(
HttpStatus.SC_METHOD_NOT_ALLOWED, BODY_LENGTH_NONE);
break;
}
} finally {
exchange.close();
}
}

Expand Down
Loading

0 comments on commit 7132572

Please sign in to comment.