-
Notifications
You must be signed in to change notification settings - Fork 5
feat(backend): added test sessionspaces for development #1275
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -161,4 +161,6 @@ pub enum Instrument { | |
| S03, | ||
| #[strum(serialize = "s04")] | ||
| S04, | ||
| #[strum(serialize = "t01")] | ||
| T01, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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}; | ||
|
|
@@ -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, | ||
|
|
@@ -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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what prevents these "static" sessions being repeatedly re-applied each loop? |
||
| } | ||
| update_sessionspaces(&mut current_sessions, &mut new_sessions, &k8s_client).await; | ||
| } | ||
| Err(err) => warn!("Encountered error when fetching sessions: {err}"), | ||
|
|
||
| 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()); | ||
| } | ||
| } | ||
| } |
| 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] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why 5? |
||
| instrument: t01 | ||
| members: [] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a session with no members is not very useful? |
||
| gid: null | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it OK to have a null gid? |
||
| start_date: "2024-01-01 00:00:00" | ||
| end_date: "2099-12-31 23:59:59" | ||
| 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 }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expect is preferred over unwrap