From 0d27e3cd8e9bb9413ac90882dcb8d680abae97fb Mon Sep 17 00:00:00 2001 From: Daniel Choi Date: Wed, 18 Jun 2025 03:20:10 -0400 Subject: [PATCH 01/30] refactor kube mods --- .github/workflows/merge.yml | 2 +- Cargo.lock | 16 +- Cargo.toml | 3 +- src/kube/mod.rs | 151 +------ src/kube/models.rs | 403 +++++------------- src/kube/notebook/mod.rs | 323 ++++++++++++++ src/kube/{ => notebook}/notebook_lifecycle.rs | 4 +- src/kube/openwebui/mod.rs | 0 src/web/helper.rs | 9 +- src/web/route/notebook/mod.rs | 38 +- src/web/route/portal/helper.rs | 16 +- 11 files changed, 499 insertions(+), 466 deletions(-) create mode 100644 src/kube/notebook/mod.rs rename src/kube/{ => notebook}/notebook_lifecycle.rs (99%) create mode 100644 src/kube/openwebui/mod.rs diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index cb7817ca..42dcfc99 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -53,7 +53,7 @@ jobs: run: | just certs just gen_curve - cargo clippy --features notebook,lifecycle -- -D warnings + cargo clippy --features full -- -D warnings - name: Run tests run: | diff --git a/Cargo.lock b/Cargo.lock index 553ea5ba..a3e67903 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -396,9 +396,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" @@ -2335,9 +2335,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.173" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libloading" @@ -5446,18 +5446,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 271743a0..a03714be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,11 @@ members = ["utils"] [features] notebook = ["dep:kube", "dep:k8s-openapi", "dep:schemars", "dep:either"] +openwebui = ["dep:kube", "dep:k8s-openapi", "dep:schemars", "dep:either"] lifecycle = ["notebook"] observe = [] mcp = [] -full = ["notebook", "lifecycle", "observe", "mcp"] +full = ["notebook", "lifecycle", "observe", "mcp", "openwebui"] [dependencies] # Async dep diff --git a/src/kube/mod.rs b/src/kube/mod.rs index f8c94e92..14fbc863 100644 --- a/src/kube/mod.rs +++ b/src/kube/mod.rs @@ -1,143 +1,12 @@ -use std::{fmt::Debug, sync::OnceLock}; - -use k8s_openapi::{ - NamespaceResourceScope, - api::core::v1::{Namespace, PersistentVolumeClaim, Pod}, -}; -use kube::{ - Api, Client, Resource, - api::{DeleteParams, ObjectMeta, PostParams}, -}; -use reqwest::StatusCode; -use serde::{Deserialize, Serialize}; -use tracing::info; - -use crate::errors::{BridgeError, Result}; - mod models; -pub use models::{NAMESPACE, Notebook, NotebookSpec, PVCSpec}; - -mod notebook_lifecycle; -pub use notebook_lifecycle::{LifecycleStream, Medium, notebook_lifecycle}; - -pub struct KubeAPI { - model: M, -} - -static KUBECLIENT: OnceLock = OnceLock::new(); - -pub async fn init_once() { - // ok to fail since we should not start if we can't connect to k8s - let client = Client::try_default() - .await - .expect("Failed to connect to k8s"); - info!("Connected to k8s"); - KUBECLIENT.get_or_init(|| client); -} - -impl KubeAPI -where - M: Resource - + Clone - + Debug - + for<'a> Deserialize<'a> // hrtb - + Serialize, - ::DynamicType: Default, -{ - pub fn new(model: M) -> Self { - Self { model } - } - - pub fn get_kube_client() -> Result<&'static Client> { - KUBECLIENT.get().ok_or(BridgeError::KubeClientError( - "Could not get kube client".to_string(), - )) - } - - pub async fn create(&self) -> Result { - let crd = Api::::namespaced(Self::get_kube_client()?.clone(), *NAMESPACE); - let pp = PostParams::default(); - let res = match crd.create(&pp, &self.model).await { - Ok(res) => res, - Err(e) => match e { - kube::Error::Api(ref error_response) => { - if error_response.reason == "AlreadyExists" { - return Err(BridgeError::NotebookExistsError( - "AlreadyExists".to_string(), - )); - } - Err(e)? - } - _ => Err(e)?, - }, - }; - Ok(res) - } - - pub async fn delete(name: &str) -> Result { - let crd = Api::::namespaced(Self::get_kube_client()?.clone(), *NAMESPACE); - let dp = DeleteParams::default(); - let status = match crd.delete(name, &dp).await? { - // resource is in the process of being deleted - either::Either::Left(_) => StatusCode::PROCESSING, - // resource has been deleted - either::Either::Right(_) => StatusCode::OK, - }; - Ok(status) - } - - /// Create a namespace if it does not exist. Returns `None` if the namespace already exists. - /// Returns `Some(())` if the namespace was created. - pub async fn make_namespace(name: &str) -> Result> { - let ns = Api::::all(Self::get_kube_client()?.clone()); - if ns.get_opt(name).await?.is_some() { - return Ok(None); - } - - let new_ns = Namespace { - metadata: ObjectMeta { - name: Some(name.to_string()), - ..Default::default() - }, - spec: Default::default(), - status: Default::default(), - }; - - ns.create(&PostParams::default(), &new_ns).await?; - Ok(Some(())) - } - - pub async fn check_pod_running(name: &str) -> Result { - let pods = Api::::namespaced(Self::get_kube_client()?.clone(), *NAMESPACE); - let pod = pods.get(name).await?; - Ok(pod - .status - .as_ref() - .map(|status| status.phase == Some("Running".to_string())) - .unwrap_or(false)) - } - - pub async fn check_pvc_exists(name: &str) -> Result { - let pvcs = - Api::::namespaced(Self::get_kube_client()?.clone(), *NAMESPACE); - let pvc = pvcs.get(name).await; - Ok(pvc.is_ok()) - } - - pub async fn get_all_pods() -> Result> { - let pods = Api::::namespaced(Self::get_kube_client()?.clone(), *NAMESPACE); - let list = pods.list(&Default::default()).await?; - Ok(list.items) - } +pub use models::*; + +#[cfg(feature = "notebook")] +mod notebook; +#[cfg(feature = "notebook")] +pub use notebook::{ + LifecycleStream, Medium, NOTEBOOK_NAMESPACE, Notebook, NotebookSpec, PVCSpec, + notebook_lifecycle, +}; - pub async fn get_pod_ip(name: &str) -> Result { - let pods = Api::::namespaced(Self::get_kube_client()?.clone(), *NAMESPACE); - let pod = pods.get(name).await?; - Ok(pod - .status - .as_ref() - .and_then(|status| status.pod_ip.as_deref()) - .unwrap_or_default() - .to_string()) - } -} +mod openwebui; diff --git a/src/kube/models.rs b/src/kube/models.rs index 945ad6dc..d391484f 100644 --- a/src/kube/models.rs +++ b/src/kube/models.rs @@ -1,320 +1,137 @@ -use std::{collections::BTreeMap, sync::LazyLock}; +use std::{fmt::Debug, sync::OnceLock}; use k8s_openapi::{ - api::core::v1::{PersistentVolumeClaim, VolumeResourceRequirements}, - apimachinery::pkg::api::resource::Quantity, + NamespaceResourceScope, + api::core::v1::{Namespace, PersistentVolumeClaim, Pod}, }; -use kube::{CustomResource, api::ObjectMeta}; -use schemars::JsonSchema; +use kube::{ + Api, Client, Resource, + api::{DeleteParams, ObjectMeta, PostParams}, +}; +use reqwest::StatusCode; use serde::{Deserialize, Serialize}; +use tracing::info; -use crate::config::CONFIG; - -pub static NAMESPACE: LazyLock<&str> = LazyLock::new(|| &CONFIG.notebook_namespace); +use crate::errors::{BridgeError, Result}; -// Define the Notebook CRD struct -#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] -#[kube(group = "kubeflow.org", version = "v1", kind = "Notebook", namespaced)] -pub struct NotebookSpec { - template: NotebookTemplateSpec, +pub struct KubeAPI { + model: M, } -impl NotebookSpec { - pub fn new( - name: String, - notebook_image_name: &str, - volume_name: String, - notebook_start_url: &mut Option, - max_idle_time: &mut Option, - env_to_add: Vec<(String, String)>, - ) -> Self { - let notebook_image = CONFIG.notebooks.get(notebook_image_name).unwrap(); - let mut notebook_env = notebook_image.notebook_env.clone().unwrap_or_default(); - - notebook_env.push(format!( - "--ServerApp.base_url='notebook/{}/{}'", - *NAMESPACE, name - )); - - let mut env = vec![EnvVar { - name: "NOTEBOOK_ARGS".to_string(), - value: notebook_env.join(" "), - }]; +static KUBECLIENT: OnceLock = OnceLock::new(); - if !env_to_add.is_empty() { - env_to_add.into_iter().for_each(|(name, value)| { - env.push(EnvVar { name, value }); - }); - } +pub async fn init_once() { + // ok to fail since we should not start if we can't connect to k8s + let client = Client::try_default() + .await + .expect("Failed to connect to k8s"); + info!("Connected to k8s"); + KUBECLIENT.get_or_init(|| client); +} - // TODO: look into removing this clone - if let Some(envs) = notebook_image.env.clone() { - envs.into_iter().for_each(|(name, value)| { - env.push(EnvVar { name, value }); - }); - } +impl KubeAPI +where + M: Resource + + Clone + + Debug + + for<'a> Deserialize<'a> // hrtb + + Serialize, + ::DynamicType: Default, +{ + pub fn new(model: M) -> Self { + Self { model } + } - *notebook_start_url = notebook_image.start_up_url.clone(); - *max_idle_time = notebook_image.max_idle_time; + pub fn get_kube_client() -> Result<&'static Client> { + KUBECLIENT.get().ok_or(BridgeError::KubeClientError( + "Could not get kube client".to_string(), + )) + } - NotebookSpec { - template: NotebookTemplateSpec { - spec: PodSpec { - containers: vec![ContainerSpec { - name, - image: notebook_image.url.clone(), - resources: Some(ResourceRequirements { - requests: BTreeMap::from([ - ("cpu".to_string(), "2".to_string()), - ("memory".to_string(), "4Gi".to_string()), - ]), - limits: BTreeMap::from([ - ("cpu".to_string(), "2".to_string()), - ("memory".to_string(), "4Gi".to_string()), - ]), - }), - image_pull_policy: notebook_image.pull_policy.clone(), - volume_mounts: Some(vec![VolumeMount { - name: volume_name.clone(), - mount_path: notebook_image.volume_mnt_path.clone().unwrap_or_default(), - }]), - command: notebook_image.command.clone(), - args: notebook_image.args.clone(), - workingdir: notebook_image.working_dir.clone(), - env: Some(env), - }], - image_pull_secrets: notebook_image - .secret - .clone() - .map(|secret| vec![ImagePullSecret { name: secret }]), - volumes: Some(vec![VolumeSpec { - name: volume_name.clone(), - persistent_volume_claim: Some(PersistentVolumeClaimSpec { - claim_name: volume_name, - read_only: None, - }), - config_map: None, - }]), - }, + pub async fn create(&self, namespace: &str) -> Result { + let crd = Api::::namespaced(Self::get_kube_client()?.clone(), namespace); + let pp = PostParams::default(); + let res = match crd.create(&pp, &self.model).await { + Ok(res) => res, + Err(e) => match e { + kube::Error::Api(ref error_response) => { + if error_response.reason == "AlreadyExists" { + return Err(BridgeError::NotebookExistsError( + "AlreadyExists".to_string(), + )); + } + Err(e)? + } + _ => Err(e)?, }, - } + }; + Ok(res) } -} - -#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] -pub struct NotebookTemplateSpec { - spec: PodSpec, -} - -#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] -pub struct PodSpec { - containers: Vec, - #[serde(rename = "imagePullSecrets")] - image_pull_secrets: Option>, - volumes: Option>, -} - -#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] -pub struct ImagePullSecret { - name: String, -} - -#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] -pub struct ContainerSpec { - name: String, - image: String, - resources: Option, - #[serde(rename = "workingDir")] - workingdir: Option, - #[serde(rename = "imagePullPolicy")] - image_pull_policy: String, - #[serde(rename = "volumeMounts")] - volume_mounts: Option>, - command: Option>, - args: Option>, - env: Option>, -} - -#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] -struct ResourceRequirements { - requests: BTreeMap, - limits: BTreeMap, -} - -#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] -pub struct EnvVar { - name: String, - value: String, -} -#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] -pub struct VolumeMount { - name: String, - #[serde(rename = "mountPath")] - mount_path: String, -} - -#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] -pub struct VolumeSpec { - name: String, - #[serde(rename = "persistentVolumeClaim")] - #[serde(skip_serializing_if = "Option::is_none")] - persistent_volume_claim: Option, - #[serde(rename = "configMap")] - #[serde(skip_serializing_if = "Option::is_none")] - config_map: Option, -} - -#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] -pub struct ConfigMapSpec { - pub name: String, -} - -#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] -pub struct PersistentVolumeClaimSpec { - #[serde(rename = "claimName")] - claim_name: String, - #[serde(rename = "readOnly")] - #[serde(skip_serializing_if = "Option::is_none")] - read_only: Option, -} + pub async fn delete(name: &str, namespace: &str) -> Result { + let crd = Api::::namespaced(Self::get_kube_client()?.clone(), namespace); + let dp = DeleteParams::default(); + let status = match crd.delete(name, &dp).await? { + // resource is in the process of being deleted + either::Either::Left(_) => StatusCode::PROCESSING, + // resource has been deleted + either::Either::Right(_) => StatusCode::OK, + }; + Ok(status) + } -// Define the PVC Spec -pub struct PVCSpec { - pub spec: PersistentVolumeClaim, -} + /// Create a namespace if it does not exist. Returns `None` if the namespace already exists. + /// Returns `Some(())` if the namespace was created. + pub async fn make_namespace(name: &str) -> Result> { + let ns = Api::::all(Self::get_kube_client()?.clone()); + if ns.get_opt(name).await?.is_some() { + return Ok(None); + } -impl PVCSpec { - pub fn new(name: String, storage_size: usize) -> Self { - Self { - spec: PersistentVolumeClaim { - metadata: ObjectMeta { - name: Some(name), - ..Default::default() - }, - spec: Some(k8s_openapi::api::core::v1::PersistentVolumeClaimSpec { - access_modes: Some(vec!["ReadWriteOnce".to_string()]), - resources: Some(VolumeResourceRequirements { - requests: Some(BTreeMap::from([( - "storage".to_string(), - Quantity(storage_size.to_string() + "Gi"), - )])), - ..Default::default() - }), - ..Default::default() - }), + let new_ns = Namespace { + metadata: ObjectMeta { + name: Some(name.to_string()), ..Default::default() }, - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use serde_json::json; - - #[test] - fn test_notebook_spec() { - let name = "notebook".to_string(); - let volume_name = "notebook-volume".to_string(); - let mut start_url = None; - let mut max_idle_time = None; - - let spec = NotebookSpec::new( - name, - "open_ad_workbench", - volume_name, - &mut start_url, - &mut max_idle_time, - vec![], - ); - - // TODO: Remove this test or refactor, so it doesn't break all the time the notebok config - // is updated - let expected = json!({ - "template": { - "spec": { - "containers": [{ - "name": "notebook", - "image": "quay.io/ibmdpdev/openad_workbench_prod:latest", - "resources": { - "requests": { - "cpu": "2", - "memory": "4Gi" - }, - "limits": { - "cpu": "2", - "memory": "4Gi" - } - }, - "workingDir": "/opt/app-root/src", - "imagePullPolicy": "Always", - "volumeMounts": [ - { - "name": "notebook-volume", - "mountPath": "/opt/app-root/src" - } - ], - "command": null, - "args": null, - "env": [ - { - "name": "NOTEBOOK_ARGS", - "value": "--ServerApp.token='' --ServerApp.password='' --ServerApp.notebook_dir='/opt/app-root/src' --ServerApp.quit_button=False --LabApp.default_url='/lab/tree/start_menu.ipynb' --ServerApp.default_url='/lab/tree/start_menu.ipynb' --ServerApp.trust_xheaders=True --ServerApp.base_url='notebook/notebook/notebook'" - }, - { - - "name":"PROXY_URL", - "value": "https://open.accelerate.science/proxy" - } - ], - }], - "imagePullSecrets": [ - { - "name": "ibmdpdev-openad-pull-secret" - } - ], - "volumes": [{ - "name": "notebook-volume", - "persistentVolumeClaim": { - "claimName": "notebook-volume" - } - }] - }, - } - }); + spec: Default::default(), + status: Default::default(), + }; - let actual = serde_json::to_value(&spec).unwrap(); - assert_eq!(actual, expected); - assert_eq!(start_url, Some("lab/tree/start_menu.ipynb".to_string())); - assert_eq!(max_idle_time, Some(86400)); + ns.create(&PostParams::default(), &new_ns).await?; + Ok(Some(())) } - #[test] - fn test_pvc_spec() { - let name = "notebook-volume".to_string(); - let storage_size = 10; + pub async fn check_pod_running(name: &str, namespace: &str) -> Result { + let pods = Api::::namespaced(Self::get_kube_client()?.clone(), namespace); + let pod = pods.get(name).await?; + Ok(pod + .status + .as_ref() + .map(|status| status.phase == Some("Running".to_string())) + .unwrap_or(false)) + } - let spec = PVCSpec::new(name, storage_size); + pub async fn check_pvc_exists(name: &str, namespace: &str) -> Result { + let pvcs = + Api::::namespaced(Self::get_kube_client()?.clone(), namespace); + let pvc = pvcs.get(name).await; + Ok(pvc.is_ok()) + } - let expected = json!({ - "apiVersion": "v1", - "kind": "PersistentVolumeClaim", - "metadata": { - "name": "notebook-volume" - }, - "spec": { - "accessModes": ["ReadWriteOnce"], - "resources": { - "requests": { - "storage": "10Gi" - } - } - } - }); + pub async fn get_all_pods(namespace: &str) -> Result> { + let pods = Api::::namespaced(Self::get_kube_client()?.clone(), namespace); + let list = pods.list(&Default::default()).await?; + Ok(list.items) + } - let actual = serde_json::to_value(&spec.spec).unwrap(); - assert_eq!(actual, expected); + pub async fn get_pod_ip(name: &str, namespace: &str) -> Result { + let pods = Api::::namespaced(Self::get_kube_client()?.clone(), namespace); + let pod = pods.get(name).await?; + Ok(pod + .status + .as_ref() + .and_then(|status| status.pod_ip.as_deref()) + .unwrap_or_default() + .to_string()) } } diff --git a/src/kube/notebook/mod.rs b/src/kube/notebook/mod.rs new file mode 100644 index 00000000..29ccf39f --- /dev/null +++ b/src/kube/notebook/mod.rs @@ -0,0 +1,323 @@ +use std::{collections::BTreeMap, sync::LazyLock}; + +use k8s_openapi::{ + api::core::v1::{PersistentVolumeClaim, VolumeResourceRequirements}, + apimachinery::pkg::api::resource::Quantity, +}; +use kube::{CustomResource, api::ObjectMeta}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::config::CONFIG; + +pub static NOTEBOOK_NAMESPACE: LazyLock<&str> = LazyLock::new(|| &CONFIG.notebook_namespace); + +mod notebook_lifecycle; +pub use notebook_lifecycle::{LifecycleStream, Medium, notebook_lifecycle}; + +// Define the Notebook CRD struct +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[kube(group = "kubeflow.org", version = "v1", kind = "Notebook", namespaced)] +pub struct NotebookSpec { + template: NotebookTemplateSpec, +} + +impl NotebookSpec { + pub fn new( + name: String, + notebook_image_name: &str, + volume_name: String, + notebook_start_url: &mut Option, + max_idle_time: &mut Option, + env_to_add: Vec<(String, String)>, + ) -> Self { + let notebook_image = CONFIG.notebooks.get(notebook_image_name).unwrap(); + let mut notebook_env = notebook_image.notebook_env.clone().unwrap_or_default(); + + notebook_env.push(format!( + "--ServerApp.base_url='notebook/{}/{}'", + *NOTEBOOK_NAMESPACE, name + )); + + let mut env = vec![EnvVar { + name: "NOTEBOOK_ARGS".to_string(), + value: notebook_env.join(" "), + }]; + + if !env_to_add.is_empty() { + env_to_add.into_iter().for_each(|(name, value)| { + env.push(EnvVar { name, value }); + }); + } + + // TODO: look into removing this clone + if let Some(envs) = notebook_image.env.clone() { + envs.into_iter().for_each(|(name, value)| { + env.push(EnvVar { name, value }); + }); + } + + *notebook_start_url = notebook_image.start_up_url.clone(); + *max_idle_time = notebook_image.max_idle_time; + + NotebookSpec { + template: NotebookTemplateSpec { + spec: PodSpec { + containers: vec![ContainerSpec { + name, + image: notebook_image.url.clone(), + resources: Some(ResourceRequirements { + requests: BTreeMap::from([ + ("cpu".to_string(), "2".to_string()), + ("memory".to_string(), "4Gi".to_string()), + ]), + limits: BTreeMap::from([ + ("cpu".to_string(), "2".to_string()), + ("memory".to_string(), "4Gi".to_string()), + ]), + }), + image_pull_policy: notebook_image.pull_policy.clone(), + volume_mounts: Some(vec![VolumeMount { + name: volume_name.clone(), + mount_path: notebook_image.volume_mnt_path.clone().unwrap_or_default(), + }]), + command: notebook_image.command.clone(), + args: notebook_image.args.clone(), + workingdir: notebook_image.working_dir.clone(), + env: Some(env), + }], + image_pull_secrets: notebook_image + .secret + .clone() + .map(|secret| vec![ImagePullSecret { name: secret }]), + volumes: Some(vec![VolumeSpec { + name: volume_name.clone(), + persistent_volume_claim: Some(PersistentVolumeClaimSpec { + claim_name: volume_name, + read_only: None, + }), + config_map: None, + }]), + }, + }, + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct NotebookTemplateSpec { + spec: PodSpec, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct PodSpec { + containers: Vec, + #[serde(rename = "imagePullSecrets")] + image_pull_secrets: Option>, + volumes: Option>, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct ImagePullSecret { + name: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct ContainerSpec { + name: String, + image: String, + resources: Option, + #[serde(rename = "workingDir")] + workingdir: Option, + #[serde(rename = "imagePullPolicy")] + image_pull_policy: String, + #[serde(rename = "volumeMounts")] + volume_mounts: Option>, + command: Option>, + args: Option>, + env: Option>, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +struct ResourceRequirements { + requests: BTreeMap, + limits: BTreeMap, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct EnvVar { + name: String, + value: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct VolumeMount { + name: String, + #[serde(rename = "mountPath")] + mount_path: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct VolumeSpec { + name: String, + #[serde(rename = "persistentVolumeClaim")] + #[serde(skip_serializing_if = "Option::is_none")] + persistent_volume_claim: Option, + #[serde(rename = "configMap")] + #[serde(skip_serializing_if = "Option::is_none")] + config_map: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct ConfigMapSpec { + pub name: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct PersistentVolumeClaimSpec { + #[serde(rename = "claimName")] + claim_name: String, + #[serde(rename = "readOnly")] + #[serde(skip_serializing_if = "Option::is_none")] + read_only: Option, +} + +// Define the PVC Spec +pub struct PVCSpec { + pub spec: PersistentVolumeClaim, +} + +impl PVCSpec { + pub fn new(name: String, storage_size: usize) -> Self { + Self { + spec: PersistentVolumeClaim { + metadata: ObjectMeta { + name: Some(name), + ..Default::default() + }, + spec: Some(k8s_openapi::api::core::v1::PersistentVolumeClaimSpec { + access_modes: Some(vec!["ReadWriteOnce".to_string()]), + resources: Some(VolumeResourceRequirements { + requests: Some(BTreeMap::from([( + "storage".to_string(), + Quantity(storage_size.to_string() + "Gi"), + )])), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use serde_json::json; + + #[test] + fn test_notebook_spec() { + let name = "notebook".to_string(); + let volume_name = "notebook-volume".to_string(); + let mut start_url = None; + let mut max_idle_time = None; + + let spec = NotebookSpec::new( + name, + "open_ad_workbench", + volume_name, + &mut start_url, + &mut max_idle_time, + vec![], + ); + + // TODO: Remove this test or refactor, so it doesn't break all the time the notebok config + // is updated + let expected = json!({ + "template": { + "spec": { + "containers": [{ + "name": "notebook", + "image": "quay.io/ibmdpdev/openad_workbench_prod:latest", + "resources": { + "requests": { + "cpu": "2", + "memory": "4Gi" + }, + "limits": { + "cpu": "2", + "memory": "4Gi" + } + }, + "workingDir": "/opt/app-root/src", + "imagePullPolicy": "Always", + "volumeMounts": [ + { + "name": "notebook-volume", + "mountPath": "/opt/app-root/src" + } + ], + "command": null, + "args": null, + "env": [ + { + "name": "NOTEBOOK_ARGS", + "value": "--ServerApp.token='' --ServerApp.password='' --ServerApp.notebook_dir='/opt/app-root/src' --ServerApp.quit_button=False --LabApp.default_url='/lab/tree/start_menu.ipynb' --ServerApp.default_url='/lab/tree/start_menu.ipynb' --ServerApp.trust_xheaders=True --ServerApp.base_url='notebook/notebook/notebook'" + }, + { + + "name":"PROXY_URL", + "value": "https://open.accelerate.science/proxy" + } + ], + }], + "imagePullSecrets": [ + { + "name": "ibmdpdev-openad-pull-secret" + } + ], + "volumes": [{ + "name": "notebook-volume", + "persistentVolumeClaim": { + "claimName": "notebook-volume" + } + }] + }, + } + }); + + let actual = serde_json::to_value(&spec).unwrap(); + assert_eq!(actual, expected); + assert_eq!(start_url, Some("lab/tree/start_menu.ipynb".to_string())); + assert_eq!(max_idle_time, Some(86400)); + } + + #[test] + fn test_pvc_spec() { + let name = "notebook-volume".to_string(); + let storage_size = 10; + + let spec = PVCSpec::new(name, storage_size); + + let expected = json!({ + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "name": "notebook-volume" + }, + "spec": { + "accessModes": ["ReadWriteOnce"], + "resources": { + "requests": { + "storage": "10Gi" + } + } + } + }); + + let actual = serde_json::to_value(&spec.spec).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/src/kube/notebook_lifecycle.rs b/src/kube/notebook/notebook_lifecycle.rs similarity index 99% rename from src/kube/notebook_lifecycle.rs rename to src/kube/notebook/notebook_lifecycle.rs index af0958f5..7c5594b4 100644 --- a/src/kube/notebook_lifecycle.rs +++ b/src/kube/notebook/notebook_lifecycle.rs @@ -23,7 +23,7 @@ use crate::{ mongo::{DB, DBCONN, ObjectID}, }, errors::{BridgeError, Result}, - kube::KubeAPI, + kube::{KubeAPI, NOTEBOOK_NAMESPACE}, web::{ notebook_helper::{make_forward_url, make_notebook_name}, utils, @@ -234,7 +234,7 @@ pub async fn notebook_lifecycle(client: Client) -> Result<()> { .get() .ok_or(BridgeError::GeneralError("DB connection failed".into()))?; - let pods = KubeAPI::::get_all_pods().await?; + let pods = KubeAPI::::get_all_pods(*NOTEBOOK_NAMESPACE).await?; if pods.is_empty() { info!("No running notebooks found"); return Ok(()); diff --git a/src/kube/openwebui/mod.rs b/src/kube/openwebui/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/web/helper.rs b/src/web/helper.rs index adf1e99c..23c4fd16 100644 --- a/src/web/helper.rs +++ b/src/web/helper.rs @@ -343,7 +343,7 @@ pub mod utils { mongo::ObjectID, }, errors::Result, - kube::{KubeAPI, Notebook}, + kube::{KubeAPI, NOTEBOOK_NAMESPACE, Notebook}, web::{helper::bson, notebook_helper}, }; @@ -370,10 +370,13 @@ pub mod utils { { let name = notebook_helper::make_notebook_name(subject); let pvc_name = notebook_helper::make_notebook_volume_name(subject); - log_with_level!(KubeAPI::::delete(&name).await, error)?; + log_with_level!( + KubeAPI::::delete(&name, *NOTEBOOK_NAMESPACE).await, + error + )?; if !persist_pvc { log_with_level!( - KubeAPI::::delete(&pvc_name).await, + KubeAPI::::delete(&pvc_name, *NOTEBOOK_NAMESPACE).await, error )?; } diff --git a/src/web/route/notebook/mod.rs b/src/web/route/notebook/mod.rs index d40f2dfb..d1048aaa 100644 --- a/src/web/route/notebook/mod.rs +++ b/src/web/route/notebook/mod.rs @@ -34,7 +34,7 @@ use crate::{ mongo::{DB, ObjectID}, }, errors::{BridgeError, Result}, - kube::{KubeAPI, NAMESPACE, Notebook, NotebookSpec, PVCSpec}, + kube::{KubeAPI, NOTEBOOK_NAMESPACE, Notebook, NotebookSpec, PVCSpec}, web::{ bridge_middleware::{CookieCheck, Htmx, NotebookCookieCheck}, helper::{self, bson}, @@ -203,7 +203,7 @@ async fn notebook_create( )? .is_some() { - info!("Namespace {} has been created", *NAMESPACE) + info!("Namespace {} has been created", *NOTEBOOK_NAMESPACE) } // User is allowed to create a notebook, but notebook does not exist... so create one @@ -212,14 +212,19 @@ async fn notebook_create( if req.query_string().contains("clear") { helper::log_with_level!( - KubeAPI::::delete(&pvc_name).await, + KubeAPI::::delete(&pvc_name, *NOTEBOOK_NAMESPACE).await, error )?; // PVC takes time to delete... loop and check it is gone loop { let mut loop_cnt = 0; - if KubeAPI::::check_pvc_exists(&pvc_name).await? { + if KubeAPI::::check_pvc_exists( + &pvc_name, + *NOTEBOOK_NAMESPACE, + ) + .await? + { tokio::time::sleep(Duration::from_secs(2)).await; loop_cnt += 1; } else { @@ -240,7 +245,7 @@ async fn notebook_create( } let pvc = PVCSpec::new(pvc_name.clone(), 1); - if let Err(e) = KubeAPI::new(pvc.spec).create().await { + if let Err(e) = KubeAPI::new(pvc.spec).create(*NOTEBOOK_NAMESPACE).await { match e { BridgeError::NotebookExistsError(_) => info!( "PVC {} already exists, most likely persisted from last session, reusing", @@ -264,7 +269,10 @@ async fn notebook_create( vec![("PROXY_KEY".to_string(), notebook_token)], ), ); - helper::log_with_level!(KubeAPI::new(notebook).create().await, error)?; + helper::log_with_level!( + KubeAPI::new(notebook).create(*NOTEBOOK_NAMESPACE).await, + error + )?; let current_time = time::OffsetDateTime::now_utc(); db.update( @@ -464,12 +472,14 @@ async fn notebook_status( // check status on k8s let ready = KubeAPI::::check_pod_running( &(notebook_helper::make_notebook_name(&bridge_cookie.subject) + "-0"), + *NOTEBOOK_NAMESPACE, ) .await?; if ready { let ip = KubeAPI::::get_pod_ip( &(notebook_helper::make_notebook_name(&bridge_cookie.subject) + "-0"), + *NOTEBOOK_NAMESPACE, ) .await?; @@ -672,7 +682,7 @@ async fn notebook_forward( pub mod notebook_helper { use crate::{ db::models::{BridgeCookie, NotebookInfo, NotebookStatusCookie, User, UserNotebook}, - kube::NAMESPACE, + kube::NOTEBOOK_NAMESPACE, web::route::notebook::NOTEBOOK_PORT, }; @@ -694,11 +704,11 @@ pub mod notebook_helper { return match path { Some(p) => format!( "{}://localhost:{}/notebook/{}/{}/{}", - protocol, NOTEBOOK_PORT, *NAMESPACE, name, p + protocol, NOTEBOOK_PORT, *NOTEBOOK_NAMESPACE, name, p ), None => format!( "{}://localhost:{}/notebook/{}/{}", - protocol, NOTEBOOK_PORT, *NAMESPACE, name + protocol, NOTEBOOK_PORT, *NOTEBOOK_NAMESPACE, name ), }; } @@ -706,19 +716,19 @@ pub mod notebook_helper { // TODO: This is super cumbersome... FIX IT FIX IT! Some(p) => format!( "{}://{}:{}/notebook/{}/{}/{}", - protocol, ip, NOTEBOOK_PORT, *NAMESPACE, name, p + protocol, ip, NOTEBOOK_PORT, *NOTEBOOK_NAMESPACE, name, p ), None => format!( "{}://{}:{}/notebook/{}/{}", - protocol, ip, NOTEBOOK_PORT, *NAMESPACE, name + protocol, ip, NOTEBOOK_PORT, *NOTEBOOK_NAMESPACE, name ), } } pub(super) fn make_path(name: &str, path: Option<&str>) -> String { match path { - Some(p) => format!("/notebook/{}/{}/{}", *NAMESPACE, name, p), - None => format!("/notebook/{}/{}", *NAMESPACE, name), + Some(p) => format!("/notebook/{}/{}/{}", *NOTEBOOK_NAMESPACE, name, p), + None => format!("/notebook/{}/{}", *NOTEBOOK_NAMESPACE, name), } } @@ -774,7 +784,7 @@ pub mod notebook_helper { pub fn config_notebook(cfg: &mut web::ServiceConfig) { cfg.service( - web::scope(&("/notebook/".to_string() + *NAMESPACE)) + web::scope(&("/notebook/".to_string() + *NOTEBOOK_NAMESPACE)) .wrap(NotebookCookieCheck) .service(notebook_ws_subscribe) .service(notebook_ws_session) diff --git a/src/web/route/portal/helper.rs b/src/web/route/portal/helper.rs index eabccb7b..db565956 100644 --- a/src/web/route/portal/helper.rs +++ b/src/web/route/portal/helper.rs @@ -111,8 +111,11 @@ where user_notebook.status = nsc.status; } None => { + use crate::kube::NOTEBOOK_NAMESPACE; + let pvc = notebook_helper::make_notebook_volume_name(&user._id.to_string()); - if let Ok(true) = KubeAPI::::check_pvc_exists(&pvc).await { + if let Ok(true) = KubeAPI::::check_pvc_exists(&pvc, *NOTEBOOK_NAMESPACE).await + { bc.config = Some(crate::db::models::Config { notebook_persist_pvc: Some(true), }); @@ -124,13 +127,20 @@ where // notebook_cookie isn't there either... if let Some(nb_start) = &user.notebook { let sub = notebook_helper::make_notebook_name(&user._id.to_string()); - match KubeAPI::::check_pod_running(&(sub.clone() + "-0")).await { + match KubeAPI::::check_pod_running( + &(sub.clone() + "-0"), + *NOTEBOOK_NAMESPACE, + ) + .await + { Ok(running) => { if running { user_notebook.status = "Ready".to_string(); ctx.insert("notebook", &user_notebook); - let ip = KubeAPI::::get_pod_ip(&(sub + "-0")).await?; + let ip = + KubeAPI::::get_pod_ip(&(sub + "-0"), *NOTEBOOK_NAMESPACE) + .await?; let notebook_cookie = NotebookCookie { subject: user._id.to_string(), ip, From f90505a70404503554636746d9008342e63fc0a2 Mon Sep 17 00:00:00 2001 From: Daniel Choi Date: Thu, 19 Jun 2025 12:06:22 -0400 Subject: [PATCH 02/30] openwebui integration --- src/kube/mod.rs | 1 + src/kube/openwebui/mod.rs | 8 ++++++ src/web/route/mod.rs | 2 ++ src/web/route/openwebui/mod.rs | 52 ++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 src/web/route/openwebui/mod.rs diff --git a/src/kube/mod.rs b/src/kube/mod.rs index 14fbc863..626e8e20 100644 --- a/src/kube/mod.rs +++ b/src/kube/mod.rs @@ -9,4 +9,5 @@ pub use notebook::{ notebook_lifecycle, }; +#[cfg(feature = "openwebui")] mod openwebui; diff --git a/src/kube/openwebui/mod.rs b/src/kube/openwebui/mod.rs index e69de29b..fd570fa0 100644 --- a/src/kube/openwebui/mod.rs +++ b/src/kube/openwebui/mod.rs @@ -0,0 +1,8 @@ +#![allow(dead_code)] + +use std::marker::PhantomData; + +// This is a placeholder for openwebui CRD +struct OpenWebUI { + _p: PhantomData<()>, +} diff --git a/src/web/route/mod.rs b/src/web/route/mod.rs index 31d3b931..e437d5bc 100644 --- a/src/web/route/mod.rs +++ b/src/web/route/mod.rs @@ -21,6 +21,8 @@ pub mod notebook; pub mod portal; pub mod proxy; pub mod resource; +#[cfg(feature = "openwebui")] +pub mod openwebui; #[get("")] async fn index(data: Data, ctx: Data, req: HttpRequest) -> Result { diff --git a/src/web/route/openwebui/mod.rs b/src/web/route/openwebui/mod.rs new file mode 100644 index 00000000..12e06075 --- /dev/null +++ b/src/web/route/openwebui/mod.rs @@ -0,0 +1,52 @@ +use actix_web::{ + HttpRequest, HttpResponse, + dev::PeerAddr, + http::Method, + web::{self, ReqData}, +}; +use tracing::instrument; + +use crate::{errors::Result, web::helper}; + +async fn openwebui_ws() {} + +#[instrument(skip(payload))] +async fn openwebui_forward( + req: HttpRequest, + payload: web::Payload, + method: Method, + peer_addr: Option, + // notebook_cookie: Option>, + client: web::Data, +) -> Result { + let path = req.uri().path(); + /* Example + Path: /notebook/notebook/675fe4d56881c0dbd5cc2960-notebook/static/lab/main.79b385776e13e3f97005.js + New URL: http://localhost:8888/notebook/notebook/675fe4d56881c0dbd5cc2960-notebook + New URL with path: http://localhost:8888/notebook/notebook/675fe4d56881c0dbd5cc2960-notebook/static/lab/main.79b385776e13e3f97005.js + New URL with query: http://localhost:8888/notebook/notebook/675fe4d56881c0dbd5cc2960-notebook/static/lab/main.79b385776e13e3f97005.js?v=79b385776e13e3f97005 + */ + + let notebook_cookie = match notebook_cookie { + Some(cookie) => cookie.into_inner(), + None => { + return helper::log_with_level!( + Err(BridgeError::NotebookAccessError( + "Notebook cookie not found".to_string(), + )), + error + ); + } + }; + + let mut new_url = Url::from_str(¬ebook_helper::make_forward_url( + ¬ebook_cookie.ip, + ¬ebook_helper::make_notebook_name(¬ebook_cookie.subject), + "http", + None, + ))?; + new_url.set_path(path); + new_url.set_query(req.uri().query()); + + helper::forwarding::forward(req, payload, method, peer_addr, client, new_url, None).await +} From 9752e2495f233ccc98f0b2e599c36a1a83b0c8f9 Mon Sep 17 00:00:00 2001 From: Daniel Choi Date: Fri, 20 Jun 2025 16:10:08 -0400 Subject: [PATCH 03/30] feature flag --- src/config/mod.rs | 5 ++++- src/web/route/mod.rs | 4 ++-- src/web/route/openwebui/mod.rs | 17 +++++------------ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index b4118fa6..a7af3327 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -122,7 +122,10 @@ pub fn init_once() -> Configuration { validation.leeway = 0; let (config_location_str, database_location_str) = if cfg!(debug_assertions) { - ("config/configurations_sample.toml", "config/database_sample.toml") + ( + "config/configurations_sample.toml", + "config/database_sample.toml", + ) } else { ("config/configurations.toml", "config/database.toml") }; diff --git a/src/web/route/mod.rs b/src/web/route/mod.rs index e437d5bc..0aa12bdc 100644 --- a/src/web/route/mod.rs +++ b/src/web/route/mod.rs @@ -18,11 +18,11 @@ pub mod health; pub mod mcp; #[cfg(feature = "notebook")] pub mod notebook; +#[cfg(feature = "openwebui")] +pub mod openwebui; pub mod portal; pub mod proxy; pub mod resource; -#[cfg(feature = "openwebui")] -pub mod openwebui; #[get("")] async fn index(data: Data, ctx: Data, req: HttpRequest) -> Result { diff --git a/src/web/route/openwebui/mod.rs b/src/web/route/openwebui/mod.rs index 12e06075..785d60fa 100644 --- a/src/web/route/openwebui/mod.rs +++ b/src/web/route/openwebui/mod.rs @@ -6,7 +6,10 @@ use actix_web::{ }; use tracing::instrument; -use crate::{errors::Result, web::helper}; +use crate::{ + errors::Result, + web::{helper, notebook_helper}, +}; async fn openwebui_ws() {} @@ -27,17 +30,7 @@ async fn openwebui_forward( New URL with query: http://localhost:8888/notebook/notebook/675fe4d56881c0dbd5cc2960-notebook/static/lab/main.79b385776e13e3f97005.js?v=79b385776e13e3f97005 */ - let notebook_cookie = match notebook_cookie { - Some(cookie) => cookie.into_inner(), - None => { - return helper::log_with_level!( - Err(BridgeError::NotebookAccessError( - "Notebook cookie not found".to_string(), - )), - error - ); - } - }; + // check for some auth here let mut new_url = Url::from_str(¬ebook_helper::make_forward_url( ¬ebook_cookie.ip, From 2c29bfa2a408cf32a6bd84ebcb73772c6352b503 Mon Sep 17 00:00:00 2001 From: Daniel Choi Date: Mon, 23 Jun 2025 15:52:11 -0400 Subject: [PATCH 04/30] openwebui impl --- Cargo.lock | 137 ++++++++++++++++-------------- config/configurations_sample.toml | 1 + src/config/mod.rs | 28 ++++-- src/web/route/openwebui/mod.rs | 18 ++-- 4 files changed, 102 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3e67903..20fccc2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,7 +89,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -226,7 +226,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -374,7 +374,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -385,7 +385,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -498,7 +498,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.103", + "syn 2.0.104", "which", ] @@ -909,7 +909,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -933,7 +933,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -944,7 +944,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -982,7 +982,7 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -993,7 +993,7 @@ checksum = "510c292c8cf384b1a340b816a9a6cf2599eb8f566a44949024af88418000c50b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1006,7 +1006,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1026,7 +1026,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "unicode-xid", ] @@ -1056,7 +1056,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1118,7 +1118,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1169,7 +1169,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1189,7 +1189,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1200,12 +1200,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1365,7 +1365,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1797,7 +1797,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.2", "tower-service", - "webpki-roots 1.0.0", + "webpki-roots 1.0.1", ] [[package]] @@ -2282,7 +2282,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2446,7 +2446,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2460,7 +2460,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2471,7 +2471,7 @@ checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2482,7 +2482,7 @@ checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2611,7 +2611,7 @@ dependencies = [ "macro_magic", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2856,7 +2856,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -3028,7 +3028,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -3096,7 +3096,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -3164,12 +3164,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" dependencies = [ "proc-macro2", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -3233,9 +3233,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ "cfg_aliases", "libc", @@ -3327,9 +3327,9 @@ dependencies = [ [[package]] name = "redis" -version = "0.32.0" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd673deb3d184140109c1e27b043c2354cb79399c3ef304c365fc628092ed773" +checksum = "f2f6fd3fd5bb3a9a48819ae5cbad501968f208c1abd139649e7e2cf5d7f4b40b" dependencies = [ "bytes", "cfg-if", @@ -3373,7 +3373,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -3455,7 +3455,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.0", + "webpki-roots 1.0.1", ] [[package]] @@ -3763,7 +3763,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -3884,7 +3884,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -3895,7 +3895,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -3979,7 +3979,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -4192,9 +4192,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.103" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -4218,7 +4218,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -4315,7 +4315,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -4326,7 +4326,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -4429,7 +4429,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -4616,7 +4616,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -4845,7 +4845,7 @@ name = "utils" version = "0.1.0" dependencies = [ "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -4940,7 +4940,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -4975,7 +4975,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5030,9 +5030,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" dependencies = [ "rustls-pki-types", ] @@ -5107,7 +5107,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -5118,7 +5118,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -5129,9 +5129,9 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-registry" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link", "windows-result", @@ -5183,6 +5183,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -5440,7 +5449,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "synstructure", ] @@ -5461,7 +5470,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -5481,7 +5490,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "synstructure", ] @@ -5502,7 +5511,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -5535,7 +5544,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] diff --git a/config/configurations_sample.toml b/config/configurations_sample.toml index 354f7e90..5456db3c 100644 --- a/config/configurations_sample.toml +++ b/config/configurations_sample.toml @@ -39,6 +39,7 @@ name = "Open Accelerated Discovery" description = "This is your gateway to the future of scientific discovery" company = "IBM Research" notebook_namespace = "notebook" +owui_namespace = "openwebui" honeycomb_api_key = "" observability_access_token = "make-believe-token" observability_endpoint = "https://postman-echo.com/post" diff --git a/src/config/mod.rs b/src/config/mod.rs index a7af3327..c08aa877 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -30,13 +30,17 @@ pub struct Configuration { pub argon_config: argon2::Config<'static>, pub db: Database, pub cache: CacheDB, + #[cfg(feature = "notebook")] pub notebooks: HashMap, + #[cfg(feature = "notebook")] pub notebook_namespace: String, pub app_name: String, pub app_discription: String, pub company: String, pub oidc: HashMap, pub observability_cred: Option<(String, String)>, + #[cfg(feature = "openwebui")] + pub owui_namespace: String, } pub struct Database { @@ -117,6 +121,13 @@ pub fn init_once() -> Configuration { let public_key = fs::read("certs/public-key.pem").unwrap(); let decoder = DecodingKey::from_ec_pem(&public_key).unwrap(); + // JWK + let mut hasher = sha2::Sha256::new(); + hasher.update(&public_key); + let kid = BASE64_URL_SAFE_NO_PAD.encode(hasher.finalize()); + let key = p256::PublicKey::from_public_key_pem(&String::from_utf8_lossy(&public_key)).unwrap(); + let jwk = JWK::new(key.to_jwk(), kid.clone()); + let mut validation = Validation::new(Algorithm::ES256); validation.set_audience(&AUD); validation.leeway = 0; @@ -133,12 +144,6 @@ pub fn init_once() -> Configuration { let conf_table: toml::Table = toml::from_str(&read_to_string(PathBuf::from_str(config_location_str).unwrap()).unwrap()) .unwrap(); - let mut hasher = sha2::Sha256::new(); - hasher.update(&public_key); - let kid = BASE64_URL_SAFE_NO_PAD.encode(hasher.finalize()); - - let key = p256::PublicKey::from_public_key_pem(&String::from_utf8_lossy(&public_key)).unwrap(); - let jwk = JWK::new(key.to_jwk(), kid.clone()); let db_table: toml::Table = toml::from_str(&read_to_string(PathBuf::from_str(database_location_str).unwrap()).unwrap()) @@ -190,7 +195,6 @@ pub fn init_once() -> Configuration { let app_name = app_conf["name"].as_str().unwrap().to_string(); let app_discription = app_conf["description"].as_str().unwrap().to_string(); let company = app_conf["company"].as_str().unwrap().to_string(); - let notebook_namespace = app_conf["notebook_namespace"].as_str().unwrap().to_string(); let observability_cred = match ( app_conf["observability_access_token"] @@ -204,11 +208,17 @@ pub fn init_once() -> Configuration { _ => None, }; + #[cfg(feature = "notebook")] + let notebook_namespace = app_conf["notebook_namespace"].as_str().unwrap().to_string(); + #[cfg(feature = "notebook")] let notebooks: HashMap = toml::from_str( &read_to_string(PathBuf::from_str("config/notebook.toml").unwrap()).unwrap(), ) .unwrap(); + #[cfg(feature = "openwebui")] + let owui_namespace = app_conf["owui_namespace"].as_str().unwrap().to_string(); + Configuration { encoder, decoder, @@ -218,13 +228,17 @@ pub fn init_once() -> Configuration { argon_config: argon2::Config::default(), db, cache, + #[cfg(feature = "notebook")] notebooks, + #[cfg(feature = "notebook")] notebook_namespace, app_name, app_discription, company, oidc: oidc_map, observability_cred, + #[cfg(feature = "openwebui")] + owui_namespace, } } diff --git a/src/web/route/openwebui/mod.rs b/src/web/route/openwebui/mod.rs index 785d60fa..f302cbc4 100644 --- a/src/web/route/openwebui/mod.rs +++ b/src/web/route/openwebui/mod.rs @@ -7,10 +7,13 @@ use actix_web::{ use tracing::instrument; use crate::{ + db::models::BridgeCookie, errors::Result, web::{helper, notebook_helper}, }; +const OWUI: &str = "owui"; + async fn openwebui_ws() {} #[instrument(skip(payload))] @@ -19,22 +22,15 @@ async fn openwebui_forward( payload: web::Payload, method: Method, peer_addr: Option, - // notebook_cookie: Option>, + bridge_cookie: Option>, client: web::Data, ) -> Result { let path = req.uri().path(); - /* Example - Path: /notebook/notebook/675fe4d56881c0dbd5cc2960-notebook/static/lab/main.79b385776e13e3f97005.js - New URL: http://localhost:8888/notebook/notebook/675fe4d56881c0dbd5cc2960-notebook - New URL with path: http://localhost:8888/notebook/notebook/675fe4d56881c0dbd5cc2960-notebook/static/lab/main.79b385776e13e3f97005.js - New URL with query: http://localhost:8888/notebook/notebook/675fe4d56881c0dbd5cc2960-notebook/static/lab/main.79b385776e13e3f97005.js?v=79b385776e13e3f97005 - */ - - // check for some auth here + // TODO: check for some auth here let mut new_url = Url::from_str(¬ebook_helper::make_forward_url( - ¬ebook_cookie.ip, - ¬ebook_helper::make_notebook_name(¬ebook_cookie.subject), + &bridge_cookie.ip, + ¬ebook_helper::make_notebook_name(&bridge_cookie.subject), "http", None, ))?; From 7bec01c394409644a370d36f926a775982b84867 Mon Sep 17 00:00:00 2001 From: Daniel Choi Date: Tue, 24 Jun 2025 13:46:22 -0400 Subject: [PATCH 05/30] update dep --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20fccc2f..2a882c59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -602,9 +602,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" From 1bdef032171b562ccb24971ad19ed52d928d2797 Mon Sep 17 00:00:00 2001 From: Daniel Choi Date: Wed, 25 Jun 2025 21:08:41 -0400 Subject: [PATCH 06/30] openwebui impl --- src/web/route/notebook/mod.rs | 1 + src/web/route/openwebui/mod.rs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/web/route/notebook/mod.rs b/src/web/route/notebook/mod.rs index d1048aaa..a7080e65 100644 --- a/src/web/route/notebook/mod.rs +++ b/src/web/route/notebook/mod.rs @@ -46,6 +46,7 @@ const NOTEBOOK_PORT: &str = "8888"; const NOTEBOOK_TOKEN_LIFETIME: usize = const { 60 * 60 * 24 * 30 }; const PVC_DELETE_ATTEMPT: u8 = 9; +// TODO: name is not needed... look into removing them if possible #[get("{name}/api/events/subscribe")] async fn notebook_ws_subscribe( req: HttpRequest, diff --git a/src/web/route/openwebui/mod.rs b/src/web/route/openwebui/mod.rs index f302cbc4..825eb369 100644 --- a/src/web/route/openwebui/mod.rs +++ b/src/web/route/openwebui/mod.rs @@ -1,6 +1,7 @@ use actix_web::{ HttpRequest, HttpResponse, dev::PeerAddr, + get, http::Method, web::{self, ReqData}, }; @@ -14,7 +15,17 @@ use crate::{ const OWUI: &str = "owui"; -async fn openwebui_ws() {} +#[get("ws/socket.io/")] +async fn openwebui_ws( + req: HttpRequest, + pl: web::Payload, + bridge_cookie: Option>, +) -> Result { + // ws://localhost:8000/ws/socket.io/?EIO=4&transport=websocket + Ok(HttpResponse::SwitchingProtocols() + .header("Sec-WebSocket-Protocol", "websocket") + .finish()) +} #[instrument(skip(payload))] async fn openwebui_forward( From eb3c980c518a6f15276e810476617de051f63566 Mon Sep 17 00:00:00 2001 From: Daniel Choi Date: Thu, 26 Jun 2025 23:53:19 -0400 Subject: [PATCH 07/30] impl wip --- Cargo.lock | 22 +++++++++++----------- Dockerfile | 2 +- src/web/route/mod.rs | 12 ++++++++++++ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a882c59..c0d65083 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -579,7 +579,7 @@ dependencies = [ "getrandom 0.2.16", "getrandom 0.3.3", "hex", - "indexmap 2.9.0", + "indexmap 2.10.0", "js-sys", "once_cell", "rand 0.9.1", @@ -859,9 +859,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -1507,7 +1507,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.9.0", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -1526,7 +1526,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.9.0", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -2027,9 +2027,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.4", @@ -3904,7 +3904,7 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "memchr", "ryu", @@ -3961,7 +3961,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.10.0", "schemars 0.9.0", "serde", "serde_derive", @@ -3988,7 +3988,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "ryu", "serde", @@ -4532,7 +4532,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_spanned", "toml_datetime", diff --git a/Dockerfile b/Dockerfile index 239b44a6..643d23db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1 build -FROM rust:1.87.0 AS builder +FROM rust:1.88.0 AS builder WORKDIR /app diff --git a/src/web/route/mod.rs b/src/web/route/mod.rs index 0aa12bdc..d46dd899 100644 --- a/src/web/route/mod.rs +++ b/src/web/route/mod.rs @@ -26,6 +26,18 @@ pub mod resource; #[get("")] async fn index(data: Data, ctx: Data, req: HttpRequest) -> Result { + // subdomain + let subdomain = req + .headers() + .get(header::HOST) + .and_then(|h| h.to_str().ok()) + .unwrap_or("") + .split('.') + .next() + .unwrap_or("") + .to_string() + .to_lowercase(); + // if cookie exists, redirect to portal if req.cookie(COOKIE_NAME).is_some() { return Ok(HttpResponse::SeeOther() From 95fd26459d2c57fc0c8781546c0f8ce20d14116d Mon Sep 17 00:00:00 2001 From: Daniel Choi Date: Fri, 27 Jun 2025 12:34:04 -0400 Subject: [PATCH 08/30] impl wip --- src/web/helper.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/web/helper.rs b/src/web/helper.rs index 23c4fd16..f62a529a 100644 --- a/src/web/helper.rs +++ b/src/web/helper.rs @@ -286,13 +286,10 @@ pub mod forwarding { let forwarded_req = forwarded_req.headers(headers); - let res = log_with_level!( - forwarded_req - .send() - .await - .map_err(|e| { BridgeError::GeneralError(e.to_string()) }), - error - )?; + let res = forwarded_req.send().await.map_err(|e| { + error!("{:?}", e); + BridgeError::GeneralError(e.to_string()) + })?; let status = res.status().as_u16(); let status = From 037bc17f4998d8627901923f6a20f911efbc9162 Mon Sep 17 00:00:00 2001 From: Daniel Choi Date: Sat, 28 Jun 2025 12:18:01 -0400 Subject: [PATCH 09/30] impl wip --- config/configurations_sample.toml | 1 + src/config/mod.rs | 11 ++++++++- src/web/mod.rs | 13 +++++++++++ src/web/route/notebook/mod.rs | 39 ++++++++++++++++++++----------- src/web/route/openwebui/mod.rs | 20 ++++++---------- 5 files changed, 56 insertions(+), 28 deletions(-) diff --git a/config/configurations_sample.toml b/config/configurations_sample.toml index 5456db3c..2861849e 100644 --- a/config/configurations_sample.toml +++ b/config/configurations_sample.toml @@ -36,6 +36,7 @@ redirect_url = "https://open.accelerate.science/auth/callback/ibm" [app-config] name = "Open Accelerated Discovery" +openweb_url = "oweb.open.accelerate.science" description = "This is your gateway to the future of scientific discovery" company = "IBM Research" notebook_namespace = "notebook" diff --git a/src/config/mod.rs b/src/config/mod.rs index c08aa877..7eede5d3 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -41,6 +41,8 @@ pub struct Configuration { pub observability_cred: Option<(String, String)>, #[cfg(feature = "openwebui")] pub owui_namespace: String, + #[cfg(feature = "openwebui")] + pub openweb_url: String, } pub struct Database { @@ -217,7 +219,12 @@ pub fn init_once() -> Configuration { .unwrap(); #[cfg(feature = "openwebui")] - let owui_namespace = app_conf["owui_namespace"].as_str().unwrap().to_string(); + let (owui_namespace, openweb_url) = { + ( + app_conf["owui_namespace"].as_str().unwrap().to_string(), + app_conf["openweb_url"].as_str().unwrap().to_string(), + ) + }; Configuration { encoder, @@ -239,6 +246,8 @@ pub fn init_once() -> Configuration { observability_cred, #[cfg(feature = "openwebui")] owui_namespace, + #[cfg(feature = "openwebui")] + openweb_url, } } diff --git a/src/web/mod.rs b/src/web/mod.rs index 6d259bb7..3ce65b4a 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -157,8 +157,21 @@ pub async fn start_server(with_tls: bool) -> Result<()> { .wrap(middleware::Compress::default()) .wrap(bridge_middleware::Maintainence) .service(actix_files::Files::new("/static", "static")); + #[cfg(feature = "notebook")] let app = app.configure(route::notebook::config_notebook); + + #[cfg(feature = "openwebui")] + let app = { + use actix_web::guard; + let openwebui_host = &CONFIG.openweb_url; + app.service( + web::scope("/") + .guard(guard::Host(openwebui_host)) + .configure(route::openwebui::config_openwebui), + ) + }; + app.service({ let scope = web::scope("") .wrap(bridge_middleware::SecurityCacheHeader) diff --git a/src/web/route/notebook/mod.rs b/src/web/route/notebook/mod.rs index a7080e65..d4b0e427 100644 --- a/src/web/route/notebook/mod.rs +++ b/src/web/route/notebook/mod.rs @@ -102,10 +102,7 @@ async fn notebook_ws_session( let kernel_id = kernel.into_inner().1; let session_id = session_id.session_id.clone(); - let path = format!( - "api/kernels/{}/channels?session_id={}", - kernel_id, session_id - ); + let path = format!("api/kernels/{kernel_id}/channels?session_id={session_id}"); let url = notebook_helper::make_forward_url( ¬ebook_cookie.ip, ¬ebook_helper::make_notebook_name(¬ebook_cookie.subject), @@ -305,7 +302,7 @@ async fn notebook_create( } let bridge_cookie_json = serde_json::to_string(&bridge_cookie).map_err(|e| { - BridgeError::GeneralError(format!("Could not serialize bridge cookie: {}", e)) + BridgeError::GeneralError(format!("Could not serialize bridge cookie: {e}")) })?; let notebook_cookie = NotebookCookie { subject: bridge_cookie.subject, @@ -317,10 +314,10 @@ async fn notebook_create( start_url: start_up_url, }; let notebook_json = serde_json::to_string(¬ebook_cookie).map_err(|e| { - BridgeError::GeneralError(format!("Could not serialize notebook cookie: {}", e)) + BridgeError::GeneralError(format!("Could not serialize notebook cookie: {e}")) })?; let notebook_status_json = serde_json::to_string(¬ebook_status_cookie).map_err(|e| { - BridgeError::GeneralError(format!("Could not serialize notebook status cookie: {}", e)) + BridgeError::GeneralError(format!("Could not serialize notebook status cookie: {e}")) })?; // Create notebook cookies @@ -439,8 +436,6 @@ async fn notebook_delete( } } - println!("Context: {:?}", ctx); - let content = helper::log_with_level!(data.render("components/notebook/start.html", &ctx), error)?; @@ -514,12 +509,11 @@ async fn notebook_status( let notebook_status_json = serde_json::to_string(¬ebook_status_cookie).map_err(|er| { BridgeError::GeneralError(format!( - "Could not serialize notebook status cookie: {}", - er + "Could not serialize notebook status cookie: {er}" )) })?; let notebook_cookie_json = serde_json::to_string(¬ebook_cookie).map_err(|er| { - BridgeError::GeneralError(format!("Could not serialize notebook cookie: {}", er)) + BridgeError::GeneralError(format!("Could not serialize notebook cookie: {er}")) })?; // TODO: We are leveraing cookies to avoid DB calls... but look into not doing this anymore @@ -688,11 +682,11 @@ pub mod notebook_helper { }; pub(crate) fn make_notebook_name(subject: &str) -> String { - format!("{}-notebook", subject) + format!("{subject}-notebook") } pub(crate) fn make_notebook_volume_name(subject: &str) -> String { - format!("{}-notebook-volume-pvc", subject) + format!("{subject}-notebook-volume-pvc") } pub(crate) fn make_forward_url( @@ -804,8 +798,25 @@ pub fn config_notebook(cfg: &mut web::ServiceConfig) { #[cfg(test)] mod test { + use crate::config::init_once; + #[tokio::test] async fn test_notebook_forward() { // assert!(true); } + + #[test] + fn test_make_forward_url() { + init_once(); + let url = + super::notebook_helper::make_forward_url("0.0.0.0", "test-notebook", "http", None); + assert_eq!( + url, + format!( + "http://localhost:{}/notebook/{}/test-notebook", + super::NOTEBOOK_PORT, + *super::NOTEBOOK_NAMESPACE + ) + ); + } } diff --git a/src/web/route/openwebui/mod.rs b/src/web/route/openwebui/mod.rs index 825eb369..c279a1f6 100644 --- a/src/web/route/openwebui/mod.rs +++ b/src/web/route/openwebui/mod.rs @@ -1,13 +1,14 @@ use actix_web::{ HttpRequest, HttpResponse, dev::PeerAddr, - get, + get, guard, http::Method, web::{self, ReqData}, }; use tracing::instrument; use crate::{ + config::CONFIG, db::models::BridgeCookie, errors::Result, web::{helper, notebook_helper}, @@ -36,17 +37,10 @@ async fn openwebui_forward( bridge_cookie: Option>, client: web::Data, ) -> Result { - let path = req.uri().path(); - // TODO: check for some auth here - - let mut new_url = Url::from_str(¬ebook_helper::make_forward_url( - &bridge_cookie.ip, - ¬ebook_helper::make_notebook_name(&bridge_cookie.subject), - "http", - None, - ))?; - new_url.set_path(path); - new_url.set_query(req.uri().query()); + todo!() +} - helper::forwarding::forward(req, payload, method, peer_addr, client, new_url, None).await +pub fn config_openwebui(cfg: &mut web::ServiceConfig) { + cfg.service(openwebui_ws) + .default_service(web::to(openwebui_forward)); } From 6519dcad4a2408a1037c051f18bd058c68d517f2 Mon Sep 17 00:00:00 2001 From: Daniel Choi Date: Sat, 28 Jun 2025 12:20:07 -0400 Subject: [PATCH 10/30] fmt fix --- src/config/mod.rs | 1 + src/db/keydb.rs | 6 +++--- src/web/route/auth/mod.rs | 2 +- src/web/route/health/inference_services.rs | 7 +++---- src/web/route/portal/mod.rs | 4 ++-- src/web/route/portal/system_admin/mod.rs | 2 +- src/web/route/resource/mod.rs | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 7eede5d3..4bf0459c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -261,6 +261,7 @@ mod tests { #[test] fn test_config_init_once() { let config = init_once(); + #[cfg(feature = "notebook")] let workbench = config.notebooks.get("open_ad_workbench").unwrap(); assert_eq!(workbench.pull_policy, "Always"); assert_eq!(workbench.working_dir, Some("/opt/app-root/src".to_string())); diff --git a/src/db/keydb.rs b/src/db/keydb.rs index 2470f862..9ec1ac57 100644 --- a/src/db/keydb.rs +++ b/src/db/keydb.rs @@ -44,7 +44,7 @@ impl CacheDB { let sub = sub.as_ref(); let _: () = self .get_connection() - .set_ex(format!("session:{}", sub), session_id, expiration) + .set_ex(format!("session:{sub}"), session_id, expiration) .await?; Ok(()) } @@ -53,7 +53,7 @@ impl CacheDB { let sub = sub.as_ref(); let session_id: String = self .get_connection() - .get(format!("session:{}", sub)) + .get(format!("session:{sub}")) .await?; Ok(session_id) } @@ -62,7 +62,7 @@ impl CacheDB { let sub = sub.as_ref(); let _: () = self .get_connection() - .del(format!("session:{}", sub)) + .del(format!("session:{sub}")) .await?; Ok(()) } diff --git a/src/web/route/auth/mod.rs b/src/web/route/auth/mod.rs index b0fad8f1..fa475d56 100644 --- a/src/web/route/auth/mod.rs +++ b/src/web/route/auth/mod.rs @@ -221,7 +221,7 @@ async fn code_to_response( }; let content = serde_json::to_string(&bridge_cookie_json).map_err(|e| { - BridgeError::GeneralError(format!("Could not serialize bridge cookie: {}", e)) + BridgeError::GeneralError(format!("Could not serialize bridge cookie: {e}")) })?; // create cookie for all routes for this user diff --git a/src/web/route/health/inference_services.rs b/src/web/route/health/inference_services.rs index c92b469a..1e291908 100644 --- a/src/web/route/health/inference_services.rs +++ b/src/web/route/health/inference_services.rs @@ -96,10 +96,9 @@ impl ListBuilder<'_> { }; self.inner_body.push_str(&format!( - r##"
{}
-
Service is currently {}.
-
{} ms
"##, - name, status, state, elapsed + r##"
{name}
+
Service is currently {status}.
+
{elapsed} ms
"## )); } diff --git a/src/web/route/portal/mod.rs b/src/web/route/portal/mod.rs index 5046ea20..211c7193 100644 --- a/src/web/route/portal/mod.rs +++ b/src/web/route/portal/mod.rs @@ -137,7 +137,7 @@ async fn search_by_email( }; let email = urlencoding::decode(email).map_err(|e| { tracing::error!("{}", e); - BridgeError::GeneralError(format!("Error parsing query string: {}", e)) + BridgeError::GeneralError(format!("Error parsing query string: {e}")) })?; let res = match db.search_users(&email, USER, PhantomData::).await { @@ -147,7 +147,7 @@ async fn search_by_email( return Ok(HttpResponse::BadRequest() .append_header(( HTMX_ERROR_RES, - format!("No email found for {}", email), + format!("No email found for {email}"), )) .finish()); } diff --git a/src/web/route/portal/system_admin/mod.rs b/src/web/route/portal/system_admin/mod.rs index f2cd0238..3529d3f9 100644 --- a/src/web/route/portal/system_admin/mod.rs +++ b/src/web/route/portal/system_admin/mod.rs @@ -183,7 +183,7 @@ async fn system_create_group( // TODO: check if group already exists, and not rely one dup key from DB let result = helper::log_with_level!(db.insert(group, GROUP).await, error); let content = match result { - Ok(r) => format!("

Group created with id: {}

", r), + Ok(r) => format!("

Group created with id: {r}

"), Err(e) if e.to_string().contains("dup key") => { return Ok(HttpResponse::BadRequest() .append_header(( diff --git a/src/web/route/resource/mod.rs b/src/web/route/resource/mod.rs index 6aad19bf..68910f0c 100644 --- a/src/web/route/resource/mod.rs +++ b/src/web/route/resource/mod.rs @@ -62,7 +62,7 @@ async fn resource_http( bridge_cookie.token = Some(token); let content = serde_json::to_string(&bridge_cookie).map_err(|e| { - BridgeError::GeneralError(format!("Could not serialize bridge cookie: {}", e)) + BridgeError::GeneralError(format!("Could not serialize bridge cookie: {e}")) })?; Some( Cookie::build(COOKIE_NAME, content) From bb1c7c6c37b4263248d427bf65a90e385478bb5d Mon Sep 17 00:00:00 2001 From: Daniel Choi Date: Mon, 30 Jun 2025 00:49:35 -0400 Subject: [PATCH 11/30] owui impl wip --- src/config/mod.rs | 18 +++++++++++------- src/db/models.rs | 6 ++++++ src/logger/observability.rs | 1 - src/web/mod.rs | 3 +-- src/web/route/mcp/mod.rs | 8 +++++++- src/web/route/mod.rs | 12 ------------ src/web/route/openwebui/mod.rs | 6 ++++-- src/web/route/portal/mod.rs | 17 +++++++++++++---- templates/components/owui/owui.html | 19 +++++++++++++++++++ templates/pages/nav_base.html | 21 +++++++++++++++++++++ templates/pages/portal_user.html | 6 ++++++ 11 files changed, 88 insertions(+), 29 deletions(-) create mode 100644 templates/components/owui/owui.html diff --git a/src/config/mod.rs b/src/config/mod.rs index 4bf0459c..cfd3079e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -262,13 +262,17 @@ mod tests { fn test_config_init_once() { let config = init_once(); #[cfg(feature = "notebook")] - let workbench = config.notebooks.get("open_ad_workbench").unwrap(); - assert_eq!(workbench.pull_policy, "Always"); - assert_eq!(workbench.working_dir, Some("/opt/app-root/src".to_string())); - assert_eq!( - workbench.start_up_url, - Some("lab/tree/start_menu.ipynb".to_string()) - ); + { + let workbench = config.notebooks.get("open_ad_workbench").unwrap(); + assert_eq!(workbench.pull_policy, "Always"); + assert_eq!(workbench.working_dir, Some("/opt/app-root/src".to_string())); + assert_eq!( + workbench.start_up_url, + Some("lab/tree/start_menu.ipynb".to_string()) + ); + } + + assert_eq!(config.app_name, "Open Accelerated Discovery"); } #[test] diff --git a/src/db/models.rs b/src/db/models.rs index 6f03b076..966835bd 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -217,6 +217,12 @@ pub struct BridgeCookie { pub session_id: Option, } +#[cfg(feature = "openwebui")] +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct OWUICookie { + subject: String, +} + #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Config { pub notebook_persist_pvc: Option, diff --git a/src/logger/observability.rs b/src/logger/observability.rs index 05caecb7..2f2c1520 100644 --- a/src/logger/observability.rs +++ b/src/logger/observability.rs @@ -62,7 +62,6 @@ impl Observe { let endpoint = endpoint.as_str(); let api_key = api_key; while let Some(msg) = recv.recv().await { - println!("Sending observability message: {}", msg); let msg = json!( { "text": msg diff --git a/src/web/mod.rs b/src/web/mod.rs index 3ce65b4a..c128d157 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -164,10 +164,9 @@ pub async fn start_server(with_tls: bool) -> Result<()> { #[cfg(feature = "openwebui")] let app = { use actix_web::guard; - let openwebui_host = &CONFIG.openweb_url; app.service( web::scope("/") - .guard(guard::Host(openwebui_host)) + .guard(guard::Host(&CONFIG.openweb_url)) .configure(route::openwebui::config_openwebui), ) }; diff --git a/src/web/route/mcp/mod.rs b/src/web/route/mcp/mod.rs index 72bfaccc..b2877b40 100644 --- a/src/web/route/mcp/mod.rs +++ b/src/web/route/mcp/mod.rs @@ -10,6 +10,7 @@ use crate::{ }; const MCP_PREFIX: &str = "/mcp/"; +const MCP_SUFFIX: &str = "-s"; #[instrument(skip(payload))] async fn forward( @@ -48,7 +49,12 @@ async fn forward( } let mut new_url = helper::log_with_level!(CATALOG.get_service(mcp), error)?; - new_url.set_path(path); + // fastmcp temp(?) fix + if mcp.ends_with(MCP_SUFFIX) { + new_url.set_path(&format!("{path}/")); + } else { + new_url.set_path(path); + } new_url.set_query(req.uri().query()); helper::forwarding::forward(req, payload, method, peer_addr, client, new_url, None).await diff --git a/src/web/route/mod.rs b/src/web/route/mod.rs index d46dd899..0aa12bdc 100644 --- a/src/web/route/mod.rs +++ b/src/web/route/mod.rs @@ -26,18 +26,6 @@ pub mod resource; #[get("")] async fn index(data: Data, ctx: Data, req: HttpRequest) -> Result { - // subdomain - let subdomain = req - .headers() - .get(header::HOST) - .and_then(|h| h.to_str().ok()) - .unwrap_or("") - .split('.') - .next() - .unwrap_or("") - .to_string() - .to_lowercase(); - // if cookie exists, redirect to portal if req.cookie(COOKIE_NAME).is_some() { return Ok(HttpResponse::SeeOther() diff --git a/src/web/route/openwebui/mod.rs b/src/web/route/openwebui/mod.rs index c279a1f6..a4ce5f56 100644 --- a/src/web/route/openwebui/mod.rs +++ b/src/web/route/openwebui/mod.rs @@ -2,7 +2,7 @@ use actix_web::{ HttpRequest, HttpResponse, dev::PeerAddr, get, guard, - http::Method, + http::{Method, header::SEC_WEBSOCKET_PROTOCOL}, web::{self, ReqData}, }; use tracing::instrument; @@ -15,6 +15,7 @@ use crate::{ }; const OWUI: &str = "owui"; +pub const OUWI_COOKIE_NAME: &str = "owui-cookie"; #[get("ws/socket.io/")] async fn openwebui_ws( @@ -22,9 +23,10 @@ async fn openwebui_ws( pl: web::Payload, bridge_cookie: Option>, ) -> Result { + let query = req.query_string(); // ws://localhost:8000/ws/socket.io/?EIO=4&transport=websocket Ok(HttpResponse::SwitchingProtocols() - .header("Sec-WebSocket-Protocol", "websocket") + .append_header((SEC_WEBSOCKET_PROTOCOL, "websocket")) .finish()) } diff --git a/src/web/route/portal/mod.rs b/src/web/route/portal/mod.rs index 211c7193..3bae982a 100644 --- a/src/web/route/portal/mod.rs +++ b/src/web/route/portal/mod.rs @@ -16,6 +16,7 @@ use tracing::instrument; use crate::web::helper::observability_post; use crate::{ auth::{COOKIE_NAME, NOTEBOOK_COOKIE_NAME, NOTEBOOK_STATUS_COOKIE_NAME}, + config::CONFIG, db::{ Database, models::{BridgeCookie, USER, User, UserPortalRep, UserType}, @@ -25,6 +26,7 @@ use crate::{ web::{ bridge_middleware::{CookieCheck, HTMX_ERROR_RES, Htmx}, helper::log_with_level, + route::openwebui::OUWI_COOKIE_NAME, services::CATALOG, }, }; @@ -145,10 +147,7 @@ async fn search_by_email( Err(e) => match e { BridgeError::RecordSearchError(_) => { return Ok(HttpResponse::BadRequest() - .append_header(( - HTMX_ERROR_RES, - format!("No email found for {email}"), - )) + .append_header((HTMX_ERROR_RES, format!("No email found for {email}"))) .finish()); } _ => { @@ -214,6 +213,15 @@ async fn logout(#[cfg(feature = "observe")] req: HttpRequest) -> HttpResponse { .finish(); notebook_cookie.make_removal(); + let mut openwebui_cookie = Cookie::build(OUWI_COOKIE_NAME, "") + .domain(&CONFIG.openweb_url) + .same_site(SameSite::Strict) + .path("/") + .http_only(true) + .secure(true) + .finish(); + openwebui_cookie.make_removal(); + let mut notebook_status_cookie = Cookie::build(NOTEBOOK_STATUS_COOKIE_NAME, "") .same_site(SameSite::Strict) .path("/") @@ -236,6 +244,7 @@ async fn logout(#[cfg(feature = "observe")] req: HttpRequest) -> HttpResponse { .cookie(cookie_remove) .cookie(notebook_cookie) .cookie(notebook_status_cookie) + .cookie(openwebui_cookie) .finish() } diff --git a/templates/components/owui/owui.html b/templates/components/owui/owui.html new file mode 100644 index 00000000..ce9ff6a6 --- /dev/null +++ b/templates/components/owui/owui.html @@ -0,0 +1,19 @@ +
+
+

Your Open Web UI

+
+

You currently have a owui running.

+ +
+
+
diff --git a/templates/pages/nav_base.html b/templates/pages/nav_base.html index cb4e2e3d..6ff73d10 100644 --- a/templates/pages/nav_base.html +++ b/templates/pages/nav_base.html @@ -123,6 +123,23 @@ {% endif %} + {% if owui %} + +
+ +

Open Web UI

+
+
+ {% endif %} {% if user_type == "system" %}
@@ -204,6 +221,10 @@ {% if notebook %} Notebook {% endif %} + {% if owui %} + Open Web UI + {% endif %} + {% if user_type == "user" %} {% if user_type == "system" %} System Management diff --git a/templates/pages/portal_user.html b/templates/pages/portal_user.html index b6c96367..46138c4b 100644 --- a/templates/pages/portal_user.html +++ b/templates/pages/portal_user.html @@ -25,6 +25,12 @@
{% endif %} +{% if owui %} + +{% endif %} + {% if user_type == "group" %}