From 4231fccf97f8527f4cf48383e55170c3348d1f5c Mon Sep 17 00:00:00 2001 From: Thomas Binu Thomas Date: Fri, 1 May 2026 17:29:14 +0100 Subject: [PATCH 1/2] feat(backend): added test sessionspaces for development --- backend/Cargo.lock | 1 + backend/sessionspaces/Cargo.toml | 1 + backend/sessionspaces/src/instruments.rs | 2 + backend/sessionspaces/src/main.rs | 19 +++- .../sessionspaces/src/permissionables/mod.rs | 2 + .../src/permissionables/static_sessions.rs | 103 ++++++++++++++++++ charts/sessionspaces/Chart.yaml | 2 +- charts/sessionspaces/staging-values.yaml | 11 ++ .../sessionspaces/templates/deployment.yaml | 16 +++ .../templates/static-sessions-configmap.yaml | 11 ++ charts/sessionspaces/values.yaml | 4 + 11 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 backend/sessionspaces/src/permissionables/static_sessions.rs create mode 100644 charts/sessionspaces/templates/static-sessions-configmap.yaml diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 056000415..516a74458 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -6298,6 +6298,7 @@ dependencies = [ "k8s-openapi", "kube", "ldap3", + "serde", "serde_json", "sqlx", "strum 0.27.2", diff --git a/backend/sessionspaces/Cargo.toml b/backend/sessionspaces/Cargo.toml index 0471bd1ba..010e71c4d 100644 --- a/backend/sessionspaces/Cargo.toml +++ b/backend/sessionspaces/Cargo.toml @@ -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", diff --git a/backend/sessionspaces/src/instruments.rs b/backend/sessionspaces/src/instruments.rs index 5922eb023..fb1170518 100644 --- a/backend/sessionspaces/src/instruments.rs +++ b/backend/sessionspaces/src/instruments.rs @@ -161,4 +161,6 @@ pub enum Instrument { S03, #[strum(serialize = "s04")] S04, + #[strum(serialize = "t01")] + T01, } diff --git a/backend/sessionspaces/src/main.rs b/backend/sessionspaces/src/main.rs index 6ee9d7ad0..d574295cb 100644 --- a/backend/sessionspaces/src/main.rs +++ b/backend/sessionspaces/src/main.rs @@ -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}; @@ -30,12 +30,15 @@ struct Cli { /// The URL of the LDAP database where the posix attributes are stored #[clap(long, env)] ldap_url: Url, - /// The period to wait after a succesful bundle server request + /// The period to wait after a successful bundle server request #[clap(long, env, default_value = "60s")] update_interval: humantime::Duration, /// The maximum allowable k8s API requests per second #[clap(long, env, default_value = "10")] request_rate: Option, + /// Optional path to a JSON file containing static sessions to always be present + #[clap(long, env)] + static_sessions: Option, /// Args to setup telemetry #[command(flatten)] telemetry_config: TelemetryConfig, @@ -66,12 +69,22 @@ async fn main() { } builder.build() }; + let static_sessions = args + .static_sessions + .as_deref() + .map(StaticSessions::from_path) + .transpose() + .expect("Failed to laod static sessions from file"); + 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 statistics) = static_sessions { + statistics.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}"), diff --git a/backend/sessionspaces/src/permissionables/mod.rs b/backend/sessionspaces/src/permissionables/mod.rs index f973be727..8820ff58a 100644 --- a/backend/sessionspaces/src/permissionables/mod.rs +++ b/backend/sessionspaces/src/permissionables/mod.rs @@ -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, *}; diff --git a/backend/sessionspaces/src/permissionables/static_sessions.rs b/backend/sessionspaces/src/permissionables/static_sessions.rs new file mode 100644 index 000000000..fafadc010 --- /dev/null +++ b/backend/sessionspaces/src/permissionables/static_sessions.rs @@ -0,0 +1,103 @@ +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 +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 +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, + /// 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, + /// Posix GID of the session group. `null` if no LDAP group exists for this session. + gid: Option, + /// 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 { + 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 { + pub fn from_path(path: &Path) -> Result { + let content = std::fs::read_to_string(path)?; + let entries: Vec = 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()); + } + } +} diff --git a/charts/sessionspaces/Chart.yaml b/charts/sessionspaces/Chart.yaml index cae8dcf88..5a169d843 100644 --- a/charts/sessionspaces/Chart.yaml +++ b/charts/sessionspaces/Chart.yaml @@ -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 diff --git a/charts/sessionspaces/staging-values.yaml b/charts/sessionspaces/staging-values.yaml index e69de29bb..cd87b4276 100644 --- a/charts/sessionspaces/staging-values.yaml +++ b/charts/sessionspaces/staging-values.yaml @@ -0,0 +1,11 @@ +staticSessions: + enabled: true + sessions: + - proposal_code: ks + proposal_number: 10000 + visits: [1, 2, 3, 4, 5] + instrument: t01 + members: [] + gid: 123456 + start_date: "2024-01-01 00:00:00" + end_date: "2099-12-31 23:59:59" diff --git a/charts/sessionspaces/templates/deployment.yaml b/charts/sessionspaces/templates/deployment.yaml index 5fcae3ebc..1678c874a 100644 --- a/charts/sessionspaces/templates/deployment.yaml +++ b/charts/sessionspaces/templates/deployment.yaml @@ -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 }} @@ -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 }} diff --git a/charts/sessionspaces/templates/static-sessions-configmap.yaml b/charts/sessionspaces/templates/static-sessions-configmap.yaml new file mode 100644 index 000000000..bcd74d962 --- /dev/null +++ b/charts/sessionspaces/templates/static-sessions-configmap.yaml @@ -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 }} diff --git a/charts/sessionspaces/values.yaml b/charts/sessionspaces/values.yaml index 3844cc165..8ced86e1c 100644 --- a/charts/sessionspaces/values.yaml +++ b/charts/sessionspaces/values.yaml @@ -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: [] From 653bbba7ac8a3a11fcfcd0d3f86c5a17bff4928a Mon Sep 17 00:00:00 2001 From: Thomas Binu Thomas Date: Tue, 12 May 2026 15:03:43 +0100 Subject: [PATCH 2/2] feat: to-delete - point sessionspaces to dev] --- charts/sessionspaces/staging-values.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/charts/sessionspaces/staging-values.yaml b/charts/sessionspaces/staging-values.yaml index cd87b4276..c1444b2f0 100644 --- a/charts/sessionspaces/staging-values.yaml +++ b/charts/sessionspaces/staging-values.yaml @@ -9,3 +9,10 @@ staticSessions: gid: 123456 start_date: "2024-01-01 00:00:00" end_date: "2099-12-31 23:59:59" + +image: + repository: ghcr.io/diamondlightsource/workflows-sessionspaces + pullPolicy: Always + imagePullSecrets: [] + tag: "dev" +