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(recordings): object storage URL may be different interally vs externally #234

Merged
merged 13 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 0 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@

<org.apache.commons.codec.version>1.16.0</org.apache.commons.codec.version>
<org.apache.commons.io.version>2.13.0</org.apache.commons.io.version>
<org.apache.httpcomponents.version>4.5.14</org.apache.httpcomponents.version>
<org.apache.httpcomponents.version>5.2.1</org.apache.httpcomponents.version>
<org.apache.commons.lang3.version>3.13.0</org.apache.commons.lang3.version>
<org.apache.commons.validator.version>1.7</org.apache.commons.validator.version>
Expand Down
35 changes: 27 additions & 8 deletions smoketest.bash
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ display_usage() {
echo -e "\t-h\t\t\t\t\t\tprint this Help text."
echo -e "\t-O\t\t\t\t\t\tOffline mode, do not attempt to pull container images."
echo -e "\t-p\t\t\t\t\t\tDisable auth Proxy."
echo -e "\t-s [minio|seaweed|cloudserver|localstack]\tS3 implementation to spin up (default \"minio\")."
echo -e "\t-s [seaweed|minio|cloudserver|localstack]\tS3 implementation to spin up (default \"seaweed\")."
echo -e "\t-g\t\t\t\t\t\tinclude Grafana dashboard and jfr-datasource in deployment."
echo -e "\t-r\t\t\t\t\t\tconfigure a cryostat-Reports sidecar instance"
echo -e "\t-t\t\t\t\t\t\tinclude sample applications for Testing."
echo -e "\t-V\t\t\t\t\t\tdo not discard data storage Volumes on exit."
echo -e "\t-X\t\t\t\t\t\tdeploy additional development aid tools."
echo -e "\t-c [podman|docker]\t\t\t\tUse Podman or Docker Container Engine (default \"podman\")."
echo -e "\t-b\t\t\t\t\t\tOpen a Browser tab for each running service's first mapped port (ex. Cryostat web client, Minio console)"
echo -e "\t-b\t\t\t\t\t\tOpen a Browser tab for each running service's first mapped port (ex. auth proxy login, database viewer)"
}

s3=minio
s3=seaweed
ce=podman
while getopts "hs:prgtOVXcb" opt; do
case $opt in
Expand Down Expand Up @@ -86,7 +86,7 @@ if [ "${USE_PROXY}" = "true" ]; then
CRYOSTAT_HTTP_PORT=8181
GRAFANA_DASHBOARD_EXT_URL=http://localhost:8080/grafana/
else
FILES+=("${DIR}/smoketest/compose/no_proxy.yml")
FILES+=("${DIR}/smoketest/compose/no_proxy.yml" "${DIR}/smoketest/compose/s3_no_proxy.yml")
if [ "${DEPLOY_GRAFANA}" = "true" ]; then
FILES+=("${DIR}/smoketest/compose/grafana_no_proxy.yml")
fi
Expand All @@ -96,6 +96,8 @@ export CRYOSTAT_HTTP_PORT
export GRAFANA_DASHBOARD_EXT_URL

s3Manifest="${DIR}/smoketest/compose/s3-${s3}.yml"
STORAGE_PORT="$(yq '.services.*.expose[0]' "${s3Manifest}" | grep -v null)"
export STORAGE_PORT

if [ ! -f "${s3Manifest}" ]; then
echo "Unknown S3 selection: ${s3}"
Expand Down Expand Up @@ -142,8 +144,10 @@ cleanup() {
docker-compose \
"${CMD[@]}" \
down "${downFlags[@]}"
${container_engine} rm proxy_cfg_helper
${container_engine} volume rm auth_proxy_cfg
${container_engine} rm proxy_cfg_helper || true
${container_engine} volume rm auth_proxy_cfg || true
${container_engine} rm seaweed_cfg_helper || true
${container_engine} volume rm seaweed_cfg || true
# podman kill hoster || true
truncate -s 0 "${HOSTSFILE}"
for i in "${PIDS[@]}"; do
Expand All @@ -157,10 +161,25 @@ cleanup
createProxyCfgVolume() {
"${container_engine}" volume create auth_proxy_cfg
"${container_engine}" container create --name proxy_cfg_helper -v auth_proxy_cfg:/tmp busybox
local cfg
cfg="$(mktemp)"
chmod 644 "${cfg}"
envsubst '$STORAGE_PORT' < "${DIR}/smoketest/compose/auth_proxy_alpha_config.yaml" > "${cfg}"
"${container_engine}" cp "${DIR}/smoketest/compose/auth_proxy_htpasswd" proxy_cfg_helper:/tmp/auth_proxy_htpasswd
"${container_engine}" cp "${DIR}/smoketest/compose/auth_proxy_alpha_config.yaml" proxy_cfg_helper:/tmp/auth_proxy_alpha_config.yaml
"${container_engine}" cp "${cfg}" proxy_cfg_helper:/tmp/auth_proxy_alpha_config.yaml
}
createProxyCfgVolume
if [ "${USE_PROXY}" = "true" ]; then
createProxyCfgVolume
fi

createSeaweedConfigVolume() {
"${container_engine}" volume create seaweed_cfg
"${container_engine}" container create --name seaweed_cfg_helper -v seaweed_cfg:/tmp busybox
"${container_engine}" cp "${DIR}/smoketest/compose/seaweed_cfg.json" seaweed_cfg_helper:/tmp/seaweed_cfg.json
}
if [ "${s3}" = "seaweed" ]; then
createSeaweedConfigVolume
fi

setupUserHosts() {
# FIXME this is broken: it puts the containers' bridge-internal IP addresses
Expand Down
6 changes: 6 additions & 0 deletions smoketest/compose/auth_proxy_alpha_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ upstreamConfig:
- id: grafana
path: /grafana/
uri: http://grafana:3000
- id: storage
path: ^/storage/(.*)$
rewriteTarget: /$1
uri: http://s3:${STORAGE_PORT}
passHostHeader: false
proxyWebSockets: false
providers:
- id: dummy
name: Unused - Sign In Below
Expand Down
3 changes: 1 addition & 2 deletions smoketest/compose/s3-cloudserver.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ services:
environment:
STORAGE_BUCKETS_ARCHIVES_NAME: archivedrecordings
QUARKUS_S3_ENDPOINT_OVERRIDE: http://s3:8000
STORAGE_EXT_URL: http://localhost:8080/storage/
QUARKUS_S3_PATH_STYLE_ACCESS: "true" # needed since compose setup does not support DNS subdomain resolution
QUARKUS_S3_AWS_REGION: us-east-1
QUARKUS_S3_AWS_CREDENTIALS_TYPE: static
Expand All @@ -14,8 +15,6 @@ services:
s3:
image: ${CLOUDSERVER_IMAGE:-docker.io/zenko/cloudserver:latest}
hostname: s3
ports:
- "8000:8000"
expose:
- "8000"
environment:
Expand Down
3 changes: 1 addition & 2 deletions smoketest/compose/s3-localstack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ services:
environment:
STORAGE_BUCKETS_ARCHIVES_NAME: archivedrecordings
QUARKUS_S3_ENDPOINT_OVERRIDE: http://s3:4566
STORAGE_EXT_URL: http://localhost:8080/storage/
QUARKUS_S3_PATH_STYLE_ACCESS: "true" # needed since compose setup does not support DNS subdomain resolution
QUARKUS_S3_AWS_REGION: us-east-1
QUARKUS_S3_AWS_CREDENTIALS_TYPE: static
Expand All @@ -14,8 +15,6 @@ services:
s3:
image: ${LOCALSTACK_IMAGE:-docker.io/localstack/localstack:latest}
hostname: s3
ports:
- "4566:4566"
expose:
- "4566"
environment:
Expand Down
5 changes: 1 addition & 4 deletions smoketest/compose/s3-minio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ services:
environment:
STORAGE_BUCKETS_ARCHIVES_NAME: archivedrecordings
QUARKUS_S3_ENDPOINT_OVERRIDE: http://s3:9000
STORAGE_EXT_URL: http://localhost:8080/storage/
QUARKUS_S3_PATH_STYLE_ACCESS: "true" # needed since compose setup does not support DNS subdomain resolution
QUARKUS_S3_AWS_REGION: us-east-1
QUARKUS_S3_AWS_CREDENTIALS_TYPE: static
Expand All @@ -14,12 +15,8 @@ services:
s3:
image: ${MINIO_IMAGE:-docker.io/minio/minio:latest}
hostname: s3
ports:
- "9001:9001"
- "9000:9000"
expose:
- "9000"
- "9001"
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioroot
Expand Down
25 changes: 18 additions & 7 deletions smoketest/compose/s3-seaweed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@ services:
environment:
STORAGE_BUCKETS_ARCHIVES_NAME: archivedrecordings
QUARKUS_S3_ENDPOINT_OVERRIDE: http://s3:8333
STORAGE_EXT_URL: http://localhost:8080/storage/
QUARKUS_S3_PATH_STYLE_ACCESS: "true" # needed since compose setup does not support DNS subdomain resolution
QUARKUS_S3_AWS_REGION: us-east-1
QUARKUS_S3_AWS_CREDENTIALS_TYPE: static
QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_ACCESS_KEY_ID: unused
QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_SECRET_ACCESS_KEY: unused
AWS_ACCESS_KEY_ID: unused
AWS_SECRET_ACCESS_KEY: unused
QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_ACCESS_KEY_ID: access_key
QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_SECRET_ACCESS_KEY: secret_key
AWS_ACCESS_KEY_ID: access_key
AWS_SECRET_ACCESS_KEY: secret_key
s3:
image: ${SEAWEEDFS_IMAGE:-docker.io/chrislusf/seaweedfs:latest}
command: server -s3
hostname: s3
ports:
- "8333:8333"
command: server -dir=/data -s3 -s3.config=/opt/seaweed_cfg.json
environment:
IP_BIND: 0.0.0.0
WEED_V: '4' # glog logging level
volumes:
- seaweed_data:/data
- seaweed_cfg:/opt
expose:
- "8333"
labels:
Expand All @@ -33,3 +38,9 @@ services:
retries: 3
start_period: 30s
timeout: 5s

volumes:
seaweed_data:
driver: local
seaweed_cfg:
external: true
8 changes: 8 additions & 0 deletions smoketest/compose/s3_no_proxy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: "3"
services:
s3:
ports:
- "${STORAGE_PORT}:${STORAGE_PORT}"
cryostat:
environment:
STORAGE_EXT_URL: ''
28 changes: 28 additions & 0 deletions smoketest/compose/seaweed_cfg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"identities": [
{
"name": "anonymous",
"actions": [
"Read"
]
},
{
"name": "cryostat",
"credentials": [
{
"accessKey": "access_key",
"secretKey": "secret_key"
}
],
"actions": [
"Admin",
"Read",
"ReadAcp",
"List",
"Tagging",
"Write",
"WriteAcp"
]
}
]
}
2 changes: 2 additions & 0 deletions src/main/java/io/cryostat/ConfigProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ public class ConfigProperties {
public static final String GRAFANA_DASHBOARD_URL = "grafana-dashboard.url";
public static final String GRAFANA_DASHBOARD_EXT_URL = "grafana-dashboard-ext.url";
public static final String GRAFANA_DATASOURCE_URL = "grafana-datasource.url";

public static final String STORAGE_EXT_URL = "storage-ext.url";
}
10 changes: 10 additions & 0 deletions src/main/java/io/cryostat/recordings/RecordingHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,11 @@ public String saveRecording(ActiveRecording recording) throws Exception {
}

public String saveRecording(ActiveRecording recording, Instant expiry) throws Exception {
return saveRecording(recording, null, expiry);
}

public String saveRecording(ActiveRecording recording, String savename, Instant expiry)
throws Exception {
// AWS object key name guidelines advise characters to avoid (% so we should not pass url
// encoded characters)
String transformedAlias =
Expand All @@ -441,6 +446,9 @@ public String saveRecording(ActiveRecording recording, Instant expiry) throws Ex
clock.now().truncatedTo(ChronoUnit.SECONDS).toString().replaceAll("[-:]+", "");
String filename =
String.format("%s_%s_%s.jfr", transformedAlias, recording.name, timestamp);
if (StringUtils.isBlank(savename)) {
savename = filename;
}
int mib = 1024 * 1024;
String key = archivedRecordingKey(recording.target.jvmId, filename);
String multipartId = null;
Expand All @@ -453,6 +461,8 @@ public String saveRecording(ActiveRecording recording, Instant expiry) throws Ex
.bucket(archiveBucket)
.key(key)
.contentType(JFR_MIME)
.contentDisposition(
String.format("attachment; filename=\"%s\"", savename))
.tagging(createActiveRecordingTagging(recording, expiry));
if (expiry != null && expiry.isAfter(Instant.now())) {
builder = builder.expires(expiry);
Expand Down
55 changes: 48 additions & 7 deletions src/main/java/io/cryostat/recordings/Recordings.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
Expand Down Expand Up @@ -80,6 +81,7 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import jdk.jfr.RecordingState;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
Expand All @@ -90,6 +92,7 @@
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestQuery;
import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.multipart.FileUpload;
import software.amazon.awssdk.core.sync.RequestBody;
Expand Down Expand Up @@ -134,6 +137,9 @@ public class Recordings {
@ConfigProperty(name = ConfigProperties.GRAFANA_DATASOURCE_URL)
Optional<String> grafanaDatasourceURL;

@ConfigProperty(name = ConfigProperties.STORAGE_EXT_URL)
Optional<String> externalStorageUrl;

void onStart(@Observes StartupEvent evt) {
boolean exists = false;
try {
Expand Down Expand Up @@ -959,23 +965,35 @@ public Response createAndRedirectPresignedDownload(@RestPath long id) throws Exc
if (recording == null) {
throw new NotFoundException();
}
String savename = recording.name;
String filename =
recordingHelper.saveRecording(
recording, Instant.now().plusSeconds(60)); // TODO make expiry configurable
recording,
savename,
Instant.now().plusSeconds(60)); // TODO make expiry configurable
String encodedKey = recordingHelper.encodedKey(recording.target.jvmId, filename);
if (!savename.endsWith(".jfr")) {
savename += ".jfr";
}
return Response.status(RestResponse.Status.PERMANENT_REDIRECT)
.header(
HttpHeaders.CONTENT_DISPOSITION,
String.format("attachment; filename=\"%s\"", filename))
.location(URI.create(String.format("/api/v3/download/%s", encodedKey)))
String.format("attachment; filename=\"%s\"", savename))
.location(
URI.create(
String.format(
"/api/v3/download/%s?f=%s",
encodedKey,
base64Url.encodeAsString(
savename.getBytes(StandardCharsets.UTF_8)))))
.build();
}

@GET
@Blocking
@Path("/api/v3/download/{encodedKey}")
@RolesAllowed("read")
public Response redirectPresignedDownload(@RestPath String encodedKey)
public Response redirectPresignedDownload(@RestPath String encodedKey, @RestQuery String f)
throws URISyntaxException {
Pair<String, String> pair = recordingHelper.decodedKey(encodedKey);
logger.infov("Handling presigned download request for {0}", pair);
Expand All @@ -990,9 +1008,32 @@ public Response redirectPresignedDownload(@RestPath String encodedKey)
.getObjectRequest(getRequest)
.build();
PresignedGetObjectRequest presignedRequest = presigner.presignGetObject(presignRequest);
return Response.status(RestResponse.Status.PERMANENT_REDIRECT)
.location(presignedRequest.url().toURI())
.build();
URI uri = presignedRequest.url().toURI();
if (externalStorageUrl.isPresent()) {
String extUrl = externalStorageUrl.get();
if (StringUtils.isNotBlank(extUrl)) {
URI extUri = new URI(extUrl);
uri =
new URI(
extUri.getScheme(),
extUri.getAuthority(),
URI.create(String.format("%s/%s", extUri.getPath(), uri.getPath()))
.normalize()
.getPath(),
uri.getQuery(),
uri.getFragment());
}
}
ResponseBuilder response = Response.status(RestResponse.Status.PERMANENT_REDIRECT);
if (StringUtils.isNotBlank(f)) {
response =
response.header(
HttpHeaders.CONTENT_DISPOSITION,
String.format(
"attachment; filename=\"%s\"",
new String(base64Url.decode(f), StandardCharsets.UTF_8)));
}
return response.location(uri).build();
}

private static Map<String, Object> getRecordingOptions(
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ quarkus.http.filter.static.matches=/static/.+
quarkus.http.filter.static.methods=GET
quarkus.http.filter.static.order=1

storage-ext.url=
storage.buckets.archives.name=archivedrecordings
storage.buckets.archives.expiration-label=expiration

Expand Down
Loading