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
1 change: 1 addition & 0 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/sessionspaces/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ kube = { workspace = true }
ldap3 = { version = "0.11.5", default-features = false, features = [
"tls-rustls",
] }
serde = { workspace = true }
serde_json = { workspace = true }
sqlx = { version = "0.8.6", features = [
"runtime-tokio",
Expand Down
2 changes: 2 additions & 0 deletions backend/sessionspaces/src/instruments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,6 @@ pub enum Instrument {
S03,
#[strum(serialize = "s04")]
S04,
#[strum(serialize = "t01")]
T01,
}
17 changes: 15 additions & 2 deletions backend/sessionspaces/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ pub mod permissionables;
/// Kubernetes resource templating
mod resources;

use crate::permissionables::Sessions;
use crate::permissionables::{static_sessions::StaticSessions, Sessions};
use clap::Parser;
use ldap3::LdapConnAsync;
use resources::{create_configmap, create_namespace, delete_namespace};
use sqlx::mysql::MySqlPoolOptions;
use std::{collections::BTreeSet, time::Duration};
use std::{collections::BTreeSet, path::PathBuf, time::Duration};
use telemetry::{setup_telemetry, TelemetryConfig};
use tokio::time::interval;
use tracing::{info, instrument, warn};
Expand All @@ -36,6 +36,9 @@ struct Cli {
/// The maximum allowable k8s API requests per second
#[clap(long, env, default_value = "10")]
request_rate: Option<u64>,
/// Optional path to a JSON file containing static sessions to always be present
#[clap(long, env)]
static_sessions: Option<PathBuf>,
/// Args to setup telemetry
#[command(flatten)]
telemetry_config: TelemetryConfig,
Expand Down Expand Up @@ -66,12 +69,22 @@ async fn main() {
}
builder.build()
};
let static_sessions = args
.static_sessions
.as_deref()
.map(StaticSessions::from_path)
.transpose()
.unwrap();

let mut current_sessions = Sessions::default();
let mut update_interval = interval(args.update_interval.into());
loop {
update_interval.tick().await;
match Sessions::fetch(&ispyb_pool, &mut ldap_connection).await {
Ok(mut new_sessions) => {
if let Some(ref statics) = static_sessions {
statics.merge_into(&mut new_sessions);
}
update_sessionspaces(&mut current_sessions, &mut new_sessions, &k8s_client).await;
}
Err(err) => warn!("Encountered error when fetching sessions: {err}"),
Expand Down
2 changes: 2 additions & 0 deletions backend/sessionspaces/src/permissionables/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ mod instrument_subjects;
mod posix_attributes;
/// Associations between proposals and subjects
mod proposal_subjects;
/// Statically-configured sessions for testing
pub mod static_sessions;

use self::{basic_info::BasicInfo, direct_subjects::DirectSubjects};
use crate::instruments::Instrument::{self, *};
Expand Down
120 changes: 120 additions & 0 deletions backend/sessionspaces/src/permissionables/static_sessions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use super::Session;
use crate::{instruments::Instrument, permissionables::Sessions};
use serde::Deserialize;
use std::{collections::BTreeSet, path::Path, str::FromStr};
use time::{macros::format_description, PrimitiveDateTime};

/// Deserialises an [`Instrument`] from its string representation (e.g. `"i03"`).
fn deserialize_instrument<'de, D>(deserializer: D) -> Result<Instrument, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Instrument::from_str(&s).map_err(serde::de::Error::custom)
}

/// Deserialises a [`PrimitiveDateTime`] from `"YYYY-MM-DD HH:MM:SS"` format.
fn deserialize_datetime<'de, D>(deserializer: D) -> Result<PrimitiveDateTime, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
PrimitiveDateTime::parse(&s, format).map_err(serde::de::Error::custom)
}

/// A single statically-defined visit session, deserialised from the config file.
/// Field names and semantics are identical to those of [`Session`], except `visits` accepts
/// multiple visit numbers so one entry can produce several namespaces sharing the same members.
#[derive(Debug, Deserialize)]
struct StaticSessionEntry {
/// The two-letter prefix code associated with the proposal (e.g. `"ks"`).
proposal_code: String,
/// The unique number of the proposal.
proposal_number: u32,
/// One or more visit numbers. Each produces a separate namespace (`{code}{number}-{visit}`).
visits: Vec<u32>,
/// The instrument with which the session is associated.
#[serde(deserialize_with = "deserialize_instrument")]
instrument: Instrument,
/// Set of usernames granted access. Defaults to empty if omitted.
#[serde(default)]
members: BTreeSet<String>,
/// Posix GID of the session group. `null` if no LDAP group exists for this session.
gid: Option<u32>,
/// Session start date and time.
#[serde(deserialize_with = "deserialize_datetime")]
start_date: PrimitiveDateTime,
/// Session end date and time.
#[serde(deserialize_with = "deserialize_datetime")]
end_date: PrimitiveDateTime,
}

impl StaticSessionEntry {
/// Expands this entry into one [`Session`] per visit number.
fn into_sessions(self) -> impl Iterator<Item = (String, Session)> {
self.visits.into_iter().map(move |visit| {
let name = format!("{}{}-{}", self.proposal_code, self.proposal_number, visit);
let session = Session {
proposal_code: self.proposal_code.clone(),
proposal_number: self.proposal_number,
visit,
instrument: self.instrument,
members: self.members.clone(),
gid: self.gid,
start_date: self.start_date,
end_date: self.end_date,
};
(name, session)
})
}
}

/// A set of statically-configured sessions that emulate ISPyB-driven visit namespaces.
///
/// Loaded once at startup from a JSON file (an array of session objects). On each reconcile
/// tick these sessions are merged into the live [`Sessions`] map so they pass through the
/// exact same create / update / delete lifecycle as real visits.
#[derive(Debug, Default, Clone)]
pub struct StaticSessions(Sessions);

impl StaticSessions {
/// Reads and parses a JSON file at `path`.
///
/// The file must contain a JSON array. Each element may list multiple visit numbers;
/// one namespace is created per visit, all sharing the same members.
/// ```json
/// {
/// "proposal_code": "ks",
/// "proposal_number": 10000,
/// "visits": [1, 2, 3, 4, 5],
/// "instrument": "t01",
/// "members": ["fed12345"],
/// "gid": null,
/// "start_date": "2024-01-01 00:00:00",
/// "end_date": "2099-12-31 23:59:59"
/// }
/// ```
pub fn from_path(path: &Path) -> Result<Self, anyhow::Error> {
let content = std::fs::read_to_string(path)?;
let entries: Vec<StaticSessionEntry> = serde_json::from_str(&content)?;
let mut sessions = Sessions::default();
for entry in entries {
for (name, session) in entry.into_sessions() {
sessions.insert(name, session);
}
}
Ok(Self(sessions))
}

/// Inserts each static session into `target`, using the same namespace naming scheme as
/// ISPyB sessions (`{proposal_code}{proposal_number}-{visit}`). Real ISPyB sessions with
/// the same name take precedence; static entries are skipped if the name already exists.
pub fn merge_into(&self, target: &mut Sessions) {
for (name, session) in self.0.iter() {
target
.entry(name.clone())
.or_insert_with(|| session.clone());
}
}
}
2 changes: 1 addition & 1 deletion charts/sessionspaces/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v2
name: sessionspaces
description: Namespace controller for creating session namespaces
type: application
version: 0.3.23
version: 0.3.24
appVersion: 0.1.5
dependencies:
- name: common
Expand Down
11 changes: 11 additions & 0 deletions charts/sessionspaces/staging-values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
staticSessions:
enabled: true
sessions:
- proposal_code: ks
proposal_number: 10000
visits: [1, 2, 3, 4, 5]
instrument: t01
members: []
gid: null
start_date: "2024-01-01 00:00:00"
end_date: "2099-12-31 23:59:59"
16 changes: 16 additions & 0 deletions charts/sessionspaces/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ spec:
- name: TRACING_ENDPOINT
value: {{ . }}
{{- end }}
{{- if $.Values.staticSessions.enabled }}
- name: STATIC_SESSIONS
value: /etc/sessionspaces/static-sessions.json
{{- end }}
{{- if $.Values.staticSessions.enabled }}
volumeMounts:
- name: static-sessions
mountPath: /etc/sessionspaces
readOnly: true
{{- end }}
resources:
{{- $.Values.deployment.resources | toYaml | nindent 12 }}
{{- with $.Values.deployment.nodeSelector }}
Expand All @@ -79,3 +89,9 @@ spec:
tolerations:
{{- . | toYaml | nindent 8 }}
{{- end }}
{{- if $.Values.staticSessions.enabled }}
volumes:
- name: static-sessions
configMap:
name: {{ include "common.names.fullname" $ }}-static-sessions
{{- end }}
11 changes: 11 additions & 0 deletions charts/sessionspaces/templates/static-sessions-configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{- if $.Values.staticSessions.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "common.names.fullname" $ }}-static-sessions
namespace: {{ .Release.Namespace }}
labels:
{{- include "common.labels.standard" $ | nindent 4 }}
data:
static-sessions.json: {{ $.Values.staticSessions.sessions | toJson | quote }}
{{- end }}
4 changes: 4 additions & 0 deletions charts/sessionspaces/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ telemetry:
level: Info
metricsEndpoint: https://otelcollector.workflows.diamond.ac.uk/v1/metrics
tracingEndpoint: https://otelcollector.workflows.diamond.ac.uk/v1/traces

staticSessions:
enabled: false
sessions: []
Loading