Skip to content
Open
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
24 changes: 20 additions & 4 deletions devolutions-gateway/src/api/jrec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use crate::api::heartbeat::recording_storage_health;
use crate::artifacts::JrecArtifacts;
use crate::extract::{JrecToken, RecordingDeleteScope, RecordingsReadScope};
use crate::http::{HttpError, HttpErrorBuilder};
use crate::recording::{PushOutcome, RecordingMessageSender};
use crate::recording::{PushOutcome, PushTarget, RecordingMessageSender};
use crate::token::{JrecTokenClaims, RecordingFileType, RecordingOperation};

/// Read chunk size when streaming a finished session ZIP from the temp file.
Expand Down Expand Up @@ -66,6 +66,14 @@ pub fn make_router<S>(state: DgwState) -> Router<S> {
#[serde(rename_all = "camelCase")]
struct JrecPushQueryParam {
file_type: RecordingFileType,
artifact: Option<PushArtifact>,
}

/// An artifact a recorder pushes next to its recording, instead of a recording clip.
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
enum PushArtifact {
Events,
}

#[derive(Deserialize)]
Expand All @@ -92,6 +100,14 @@ async fn jrec_push(
return Err(HttpError::forbidden().msg("expected push operation"));
}

let target = match (query.artifact, query.file_type) {
(None, file_type) => PushTarget::Clip(file_type),
(Some(PushArtifact::Events), RecordingFileType::SessionRecordingLog) => PushTarget::Events,
(Some(PushArtifact::Events), _) => {
return Err(HttpError::bad_request().msg("the events artifact is a session recording log"));
}
};

let conf = conf_handle.get_conf();

// Pre-flight: refuse the upgrade up-front when the recording storage cannot accept
Expand Down Expand Up @@ -138,7 +154,7 @@ async fn jrec_push(
recordings,
shutdown_signal,
claims,
query.file_type,
target,
session_id,
source_addr,
Duration::from_secs(conf_handle.get_conf().debug.ws_keep_alive_interval),
Expand All @@ -154,7 +170,7 @@ async fn handle_jrec_push(
recordings: RecordingMessageSender,
shutdown_signal: ShutdownSignal,
claims: JrecTokenClaims,
file_type: RecordingFileType,
target: PushTarget,
session_id: Uuid,
source_addr: SocketAddr,
keep_alive_interval: Duration,
Expand All @@ -169,7 +185,7 @@ async fn handle_jrec_push(
.client_stream(stream)
.recordings(recordings)
.claims(claims)
.file_type(file_type)
.target(target)
.session_id(session_id)
.shutdown_signal(shutdown_signal)
.build()
Expand Down
25 changes: 17 additions & 8 deletions devolutions-gateway/src/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,25 @@ use crate::token::RecordingFileType;
pub(crate) struct JrecArtifacts {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
ai_analysis: Vec<JrecArtifact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
events: Vec<JrecArtifact>,
}

impl JrecArtifacts {
pub(crate) fn is_empty(&self) -> bool {
let Self { ai_analysis } = self;
ai_analysis.is_empty()
let Self { ai_analysis, events } = self;
ai_analysis.is_empty() && events.is_empty()
}

pub(crate) fn into_file_names(self) -> impl IntoIterator<Item = String> {
let Self { ai_analysis } = self;
ai_analysis.into_iter().map(|artifact| artifact.file_name)
let Self { ai_analysis, events } = self;
ai_analysis.into_iter().chain(events).map(|artifact| artifact.file_name)
}

pub(crate) fn of_kind_mut(&mut self, kind: ArtifactKind) -> &mut Vec<JrecArtifact> {
match kind {
ArtifactKind::AiAnalysis => &mut self.ai_analysis,
ArtifactKind::Events => &mut self.events,
}
}
}
Expand All @@ -39,18 +42,21 @@ pub(crate) struct JrecArtifact {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactKind {
AiAnalysis,
/// The session events log a recorder pushes next to its recording.
Events,
}

impl ArtifactKind {
pub const fn as_str(self) -> &'static str {
match self {
ArtifactKind::AiAnalysis => "ai-analysis",
ArtifactKind::Events => "events",
}
}

pub(crate) const fn file_type(self) -> RecordingFileType {
match self {
ArtifactKind::AiAnalysis => RecordingFileType::SessionRecordingLog,
ArtifactKind::AiAnalysis | ArtifactKind::Events => RecordingFileType::SessionRecordingLog,
}
}

Expand All @@ -62,11 +68,14 @@ impl ArtifactKind {

#[cfg(test)]
mod tests {
use rstest::rstest;

use super::*;

#[test]
fn manifest_key_is_the_kind_name() {
let kind = ArtifactKind::AiAnalysis;
#[rstest]
#[case::ai_analysis(ArtifactKind::AiAnalysis)]
#[case::events(ArtifactKind::Events)]
fn manifest_key_is_the_kind_name(#[case] kind: ArtifactKind) {
let mut artifacts = JrecArtifacts::default();
artifacts.of_kind_mut(kind).push(JrecArtifact {
file_name: "file".to_owned(),
Expand Down
11 changes: 8 additions & 3 deletions devolutions-gateway/src/recording.intent.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# Recording
Gateway recording is a continuous stream of bytes pushed from the client who pocesses the valid recording token.
Gateway recording is a continuous stream of bytes pushed from the client who possesses the valid recording token.
The url is `/jet/jrec/push/{sessionId}?fileType={fileType}`.
We expect the `fileType` to be one of the following file types: `webm`, `cast`, `trp` and `slog`, which must be specified.
When connection is established with request of recordings for the session, if recording is not enabled within a short period of time, the connection will be closed with indication of violation of the recording policy.
Gateway only allows one recording to be pushed at a time per session.


# Artifacts
Artifacts are files that are not recordings, currently only have `ai-analysis` with combination to `slog` file type.
Currently, we do not have an api endpoint to push artifacts, but we will have one in the future.
Artifacts are files that are not recordings, listed under `artifacts` in manifest instead of `files`.
The url is `/jet/jrec/push/{sessionId}?fileType=slog&artifact=events`; `events` is the only artifact that can be pushed, as the session events log.
`ai-analysis` is not pushed: it is only kept so older manifests still list it.
Artifacts of file type slog can be pushed by producers alongside the recordings, one artifact at a time.
Pushing an artifact does not satisfy the recording policy.

Loading
Loading