Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): mutation operations #175

Merged
merged 48 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
7b774d9
feat(api): enable dynamic JFR start (#165)
andrewazores Jul 27, 2023
08a200f
fix license header
andrewazores Aug 4, 2023
1a2acaa
end paths with / so as to not match by prefix
andrewazores Jul 31, 2023
18f3742
refactor: extract methods
andrewazores Jul 31, 2023
6577734
add utility for extracting ID from path
andrewazores Jul 31, 2023
8789208
fixup! add utility for extracting ID from path
andrewazores Jul 31, 2023
7b62753
add handling for stopping recording
andrewazores Jul 31, 2023
b8d10b8
add handling for closing (deleting) recording
andrewazores Jul 31, 2023
15195e7
fixup! add handling for closing (deleting) recording
andrewazores Jul 31, 2023
8cc9a87
fixup! add handling for stopping recording
andrewazores Jul 31, 2023
e4df526
only allow GET requests if write-operations are not enabled
andrewazores Jul 31, 2023
0bcaf25
extract HTTP response body length constants
andrewazores Aug 9, 2023
5fc71f9
cleanup
andrewazores Aug 14, 2023
6154aa8
feat(api): enable dynamic JFR stop, delete (#176)
andrewazores Aug 14, 2023
86a2c02
use longs for IDs
andrewazores Aug 4, 2023
abcb2b9
feat(api): implement GET /recordings/:id for streaming files
andrewazores Aug 4, 2023
42ecb61
refactor to use constants
andrewazores Aug 14, 2023
06a8cc1
feat(api): enable dynamic JFR start (#165)
andrewazores Jul 27, 2023
8039907
fix license header
andrewazores Aug 4, 2023
82002e8
feat(api): enable dynamic JFR stop, delete (#176)
andrewazores Aug 14, 2023
05ead99
feat(api): implement GET /recordings/:id for streaming files
andrewazores Aug 14, 2023
d360a51
accept snapshot
mwangggg Aug 16, 2023
f80cb46
update
mwangggg Aug 16, 2023
5e42c4c
clean up
mwangggg Aug 24, 2023
39fcfb6
check recording is valid
mwangggg Aug 24, 2023
2cc7720
update or stop
mwangggg Aug 31, 2023
103361d
cleanup
mwangggg Sep 1, 2023
1a8f8da
updates
mwangggg Sep 7, 2023
95a9430
mvn spotless:apply
andrewazores Sep 12, 2023
8fe5372
use textValue() instead of toString()
andrewazores Sep 12, 2023
1a0d3b0
feat(api): enable dynamic JFR start (#165)
andrewazores Jul 27, 2023
9dd66f1
update license header
andrewazores Sep 13, 2023
df6b1f5
remove redundant condition
andrewazores Sep 13, 2023
f08e792
Merge remote-tracking branch 'origin/http-recording-stop-delete' into…
andrewazores Sep 13, 2023
ef0b747
Merge remote-tracking branch 'origin/http-recording-stream' into 124-…
andrewazores Sep 13, 2023
1d2bdd1
Merge remote-tracking branch 'miwan/http-recording-snapshot' into 124…
andrewazores Sep 13, 2023
b857ff4
fixup
andrewazores Sep 13, 2023
baa3e37
attempt to open all resources before sending response header
andrewazores Sep 13, 2023
5268e58
remove redundant condition
andrewazores Sep 13, 2023
30c2ab1
cleanup
andrewazores Sep 14, 2023
271e659
do not process blank name updates
andrewazores Sep 14, 2023
f5e9b1c
correct response status code when unknown PATCH body key is processed
andrewazores Sep 14, 2023
77aa92e
do not continue processing and attempting to send response headers af…
andrewazores Sep 14, 2023
196ee75
preserve recording's original settings before updating
andrewazores Sep 14, 2023
c3ce2d5
apply state changes (recording stop) last after other updates
andrewazores Sep 14, 2023
98b54c6
only re-set duration setting if the recording is not already stopped
andrewazores Sep 14, 2023
9317c9b
remove troubleshooting logging
andrewazores Sep 14, 2023
5333964
handle STOPPED state request case-insensitively
andrewazores Sep 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading