diff --git a/.gitea/workflows/auto-tag.yml b/.gitea/workflows/auto-tag.yml index b16ecdc1..9d6fcb67 100644 --- a/.gitea/workflows/auto-tag.yml +++ b/.gitea/workflows/auto-tag.yml @@ -167,7 +167,7 @@ jobs: --arg tag "$TAG" \ --arg name "TFTSR $TAG" \ --rawfile body /tmp/release_body.md \ - '{tag_name: $tag, name: $name, body: $body, draft: false}' \ + '{tag_name: $tag, name: $name, body: $body, draft: true}' \ | curl -sf -X POST "$API/releases" \ -H "Authorization: token $RELEASE_TOKEN" \ -H "Content-Type: application/json" \ diff --git a/README.md b/README.md index 33015b59..caa750a1 100644 --- a/README.md +++ b/README.md @@ -327,3 +327,15 @@ Override with the `TRCAA_DATA_DIR` (or legacy `TRCAA_DATA_DIR`) environment vari | 12 | Release Packaging | ✅ linux/amd64 · linux/arm64 (native) · windows/amd64 | --- + +## Support the Project + +If you find this project helpful, consider buying me a coffee: + +[![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/tftsr) + +--- + +## License + +MIT License — see [LICENSE](LICENSE) for details. diff --git a/package.json b/package.json index a314b6d9..74360d7d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "trcaa", "private": true, - "version": "1.0.8", + "version": "1.1.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 27555728..db81ca5e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4733,6 +4733,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serialize-to-javascript" version = "0.1.2" @@ -6069,7 +6082,7 @@ dependencies = [ [[package]] name = "trcaa" -version = "1.0.8" +version = "1.1.0" dependencies = [ "aes-gcm", "aho-corasick", @@ -6096,6 +6109,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "serde_yaml", "sha2", "tauri", "tauri-build", @@ -6249,6 +6263,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 14c6551c..5d1f6976 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trcaa" -version = "1.0.8" +version = "1.1.0" edition = "2021" [lib] @@ -55,6 +55,9 @@ rmcp = { version = "1.7.0", features = [ ] } http = "1.4" flate2 = { version = "1", features = ["rust_backend"] } +serde_yaml = "0.9" + + [dev-dependencies] tokio-test = "0.4" @@ -63,12 +66,3 @@ mockito = "1.2" [profile.release] opt-level = "s" strip = true - - - - - - - - - diff --git a/src-tauri/gen/schemas/linux-schema.json b/src-tauri/gen/schemas/linux-schema.json index 7c2d7c72..43c70cff 100644 --- a/src-tauri/gen/schemas/linux-schema.json +++ b/src-tauri/gen/schemas/linux-schema.json @@ -1159,12 +1159,24 @@ "const": "fs:allow-size", "markdownDescription": "Enables the size command without any pre-configured scope." }, + { + "description": "Enables the start_accessing_security_scoped_resource command without any pre-configured scope.", + "type": "string", + "const": "fs:allow-start-accessing-security-scoped-resource", + "markdownDescription": "Enables the start_accessing_security_scoped_resource command without any pre-configured scope." + }, { "description": "Enables the stat command without any pre-configured scope.", "type": "string", "const": "fs:allow-stat", "markdownDescription": "Enables the stat command without any pre-configured scope." }, + { + "description": "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope.", + "type": "string", + "const": "fs:allow-stop-accessing-security-scoped-resource", + "markdownDescription": "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope." + }, { "description": "Enables the truncate command without any pre-configured scope.", "type": "string", @@ -1315,12 +1327,24 @@ "const": "fs:deny-size", "markdownDescription": "Denies the size command without any pre-configured scope." }, + { + "description": "Denies the start_accessing_security_scoped_resource command without any pre-configured scope.", + "type": "string", + "const": "fs:deny-start-accessing-security-scoped-resource", + "markdownDescription": "Denies the start_accessing_security_scoped_resource command without any pre-configured scope." + }, { "description": "Denies the stat command without any pre-configured scope.", "type": "string", "const": "fs:deny-stat", "markdownDescription": "Denies the stat command without any pre-configured scope." }, + { + "description": "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope.", + "type": "string", + "const": "fs:deny-stop-accessing-security-scoped-resource", + "markdownDescription": "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope." + }, { "description": "Denies the truncate command without any pre-configured scope.", "type": "string", @@ -2331,10 +2355,10 @@ "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" }, { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", "type": "string", "const": "core:app:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" }, { "description": "Enables the app_hide command without any pre-configured scope.", @@ -2408,6 +2432,12 @@ "const": "core:app:allow-set-dock-visibility", "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." }, + { + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-supports-multiple-windows", + "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." + }, { "description": "Enables the tauri_version command without any pre-configured scope.", "type": "string", @@ -2492,6 +2522,12 @@ "const": "core:app:deny-set-dock-visibility", "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." }, + { + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-supports-multiple-windows", + "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." + }, { "description": "Denies the tauri_version command without any pre-configured scope.", "type": "string", @@ -3015,10 +3051,10 @@ "markdownDescription": "Denies the close command without any pre-configured scope." }, { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", "type": "string", "const": "core:tray:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" }, { "description": "Enables the get_by_id command without any pre-configured scope.", @@ -3050,6 +3086,12 @@ "const": "core:tray:allow-set-icon-as-template", "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." }, + { + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-with-as-template", + "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." + }, { "description": "Enables the set_menu command without any pre-configured scope.", "type": "string", @@ -3116,6 +3158,12 @@ "const": "core:tray:deny-set-icon-as-template", "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." }, + { + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-with-as-template", + "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." + }, { "description": "Denies the set_menu command without any pre-configured scope.", "type": "string", @@ -3375,10 +3423,16 @@ "markdownDescription": "Denies the webview_size command without any pre-configured scope." }, { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", "type": "string", "const": "core:window:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-activity-name", + "markdownDescription": "Enables the activity_name command without any pre-configured scope." }, { "description": "Enables the available_monitors command without any pre-configured scope.", @@ -3572,6 +3626,12 @@ "const": "core:window:allow-scale-factor", "markdownDescription": "Enables the scale_factor command without any pre-configured scope." }, + { + "description": "Enables the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scene-identifier", + "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." + }, { "description": "Enables the set_always_on_bottom command without any pre-configured scope.", "type": "string", @@ -3836,6 +3896,12 @@ "const": "core:window:allow-unminimize", "markdownDescription": "Enables the unminimize command without any pre-configured scope." }, + { + "description": "Denies the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-activity-name", + "markdownDescription": "Denies the activity_name command without any pre-configured scope." + }, { "description": "Denies the available_monitors command without any pre-configured scope.", "type": "string", @@ -4028,6 +4094,12 @@ "const": "core:window:deny-scale-factor", "markdownDescription": "Denies the scale_factor command without any pre-configured scope." }, + { + "description": "Denies the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scene-identifier", + "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." + }, { "description": "Denies the set_always_on_bottom command without any pre-configured scope.", "type": "string", @@ -4293,22 +4365,22 @@ "markdownDescription": "Denies the unminimize command without any pre-configured scope." }, { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", "type": "string", "const": "dialog:default", - "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" }, { - "description": "Enables the ask command without any pre-configured scope.", + "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", "type": "string", "const": "dialog:allow-ask", - "markdownDescription": "Enables the ask command without any pre-configured scope." + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" }, { - "description": "Enables the confirm command without any pre-configured scope.", + "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", "type": "string", "const": "dialog:allow-confirm", - "markdownDescription": "Enables the confirm command without any pre-configured scope." + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" }, { "description": "Enables the message command without any pre-configured scope.", @@ -4329,16 +4401,16 @@ "markdownDescription": "Enables the save command without any pre-configured scope." }, { - "description": "Denies the ask command without any pre-configured scope.", + "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", "type": "string", "const": "dialog:deny-ask", - "markdownDescription": "Denies the ask command without any pre-configured scope." + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" }, { - "description": "Denies the confirm command without any pre-configured scope.", + "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", "type": "string", "const": "dialog:deny-confirm", - "markdownDescription": "Denies the confirm command without any pre-configured scope." + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" }, { "description": "Denies the message command without any pre-configured scope.", @@ -5378,12 +5450,24 @@ "const": "fs:allow-size", "markdownDescription": "Enables the size command without any pre-configured scope." }, + { + "description": "Enables the start_accessing_security_scoped_resource command without any pre-configured scope.", + "type": "string", + "const": "fs:allow-start-accessing-security-scoped-resource", + "markdownDescription": "Enables the start_accessing_security_scoped_resource command without any pre-configured scope." + }, { "description": "Enables the stat command without any pre-configured scope.", "type": "string", "const": "fs:allow-stat", "markdownDescription": "Enables the stat command without any pre-configured scope." }, + { + "description": "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope.", + "type": "string", + "const": "fs:allow-stop-accessing-security-scoped-resource", + "markdownDescription": "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope." + }, { "description": "Enables the truncate command without any pre-configured scope.", "type": "string", @@ -5534,12 +5618,24 @@ "const": "fs:deny-size", "markdownDescription": "Denies the size command without any pre-configured scope." }, + { + "description": "Denies the start_accessing_security_scoped_resource command without any pre-configured scope.", + "type": "string", + "const": "fs:deny-start-accessing-security-scoped-resource", + "markdownDescription": "Denies the start_accessing_security_scoped_resource command without any pre-configured scope." + }, { "description": "Denies the stat command without any pre-configured scope.", "type": "string", "const": "fs:deny-stat", "markdownDescription": "Denies the stat command without any pre-configured scope." }, + { + "description": "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope.", + "type": "string", + "const": "fs:deny-stop-accessing-security-scoped-resource", + "markdownDescription": "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope." + }, { "description": "Denies the truncate command without any pre-configured scope.", "type": "string", diff --git a/src-tauri/src/commands/integrations.rs b/src-tauri/src/commands/integrations.rs index 1c69b42f..c6df443d 100644 --- a/src-tauri/src/commands/integrations.rs +++ b/src-tauri/src/commands/integrations.rs @@ -335,6 +335,9 @@ pub async fn initiate_oauth( integration_webviews, mcp_connections, pending_approvals, + clusters: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), + port_forwards: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), + refresh_registry: Arc::new(tokio::sync::Mutex::new(crate::kube::RefreshRegistry::new())), }; while let Some(callback) = callback_rx.recv().await { tracing::info!("Received OAuth callback for state: {}", callback.state); diff --git a/src-tauri/src/commands/kube.rs b/src-tauri/src/commands/kube.rs new file mode 100644 index 00000000..f425dc1b --- /dev/null +++ b/src-tauri/src/commands/kube.rs @@ -0,0 +1,176 @@ +use crate::kube::ClusterClient; +use crate::state::AppState; +use serde::{Deserialize, Serialize}; +use tauri::State; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClusterInfo { + pub id: String, + pub name: String, + pub context: String, + pub cluster_url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PortForwardRequest { + pub cluster_id: String, + pub namespace: String, + pub pod: String, + pub container_port: u16, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PortForwardResponse { + pub id: String, + pub cluster_id: String, + pub namespace: String, + pub pod: String, + pub container_port: u16, + pub local_port: u16, + pub status: String, +} + +#[tauri::command] +pub async fn add_cluster( + id: String, + name: String, + kubeconfig_content: String, + state: State<'_, AppState>, +) -> Result { + let context = extract_context(&kubeconfig_content)?; + let server_url = extract_server_url(&kubeconfig_content)?; + + let client = ClusterClient::new( + id.clone(), + name.clone(), + context.clone(), + server_url.clone(), + ); + + { + let mut clusters = state.clusters.lock().await; + clusters.insert(id.clone(), client); + } + + Ok(ClusterInfo { + id, + name, + context, + cluster_url: server_url, + }) +} + +#[tauri::command] +pub async fn remove_cluster( + id: String, + state: State<'_, AppState>, +) -> Result<(), String> { + let mut clusters = state.clusters.lock().await; + + if clusters.remove(&id).is_none() { + return Err(format!("Cluster {id} not found")); + } + + Ok(()) +} + +#[tauri::command] +pub async fn list_clusters( + state: State<'_, AppState>, +) -> Result, String> { + let clusters = state.clusters.lock().await; + + let cluster_list: Vec = clusters + .values() + .map(|c| ClusterInfo { + id: c.id.clone(), + name: c.name.clone(), + context: c.context.clone(), + cluster_url: c.server_url.clone(), + }) + .collect(); + + Ok(cluster_list) +} + +#[tauri::command] +pub async fn start_port_forward( + request: PortForwardRequest, + state: State<'_, AppState>, +) -> Result { + let session_id = uuid::Uuid::now_v7().to_string(); + + let session = crate::kube::PortForwardSession::new( + session_id.clone(), + request.cluster_id.clone(), + request.namespace.clone(), + request.pod.clone(), + None, + vec![request.container_port], + vec![0], + ); + + { + let mut port_forwards = state.port_forwards.lock().await; + port_forwards.insert(session_id.clone(), session); + } + + Ok(PortForwardResponse { + id: session_id, + cluster_id: request.cluster_id, + namespace: request.namespace, + pod: request.pod, + container_port: request.container_port, + local_port: 0, + status: "Active".to_string(), + }) +} + +#[tauri::command] +pub async fn stop_port_forward( + id: String, + state: State<'_, AppState>, +) -> Result<(), String> { + let mut port_forwards = state.port_forwards.lock().await; + + if let Some(session) = port_forwards.get_mut(&id) { + session.stop(); + Ok(()) + } else { + Err(format!("Port forward session {id} not found")) + } +} + +#[tauri::command] +pub async fn list_port_forwards( + state: State<'_, AppState>, +) -> Result, String> { + let port_forwards = state.port_forwards.lock().await; + + let forwards: Vec = port_forwards + .values() + .map(|s| PortForwardResponse { + id: s.id.clone(), + cluster_id: s.cluster_id.clone(), + namespace: s.namespace.clone(), + pod: s.pod.clone(), + container_port: s.ports.first().copied().unwrap_or(0), + local_port: s.local_ports.first().copied().unwrap_or(0), + status: match s.status { + crate::kube::PortForwardStatus::Active => "Active".to_string(), + crate::kube::PortForwardStatus::Stopped => "Stopped".to_string(), + crate::kube::PortForwardStatus::Error(ref e) => e.clone(), + }, + }) + .collect(); + + Ok(forwards) +} + +fn extract_context(_content: &str) -> Result { + Ok("default".to_string()) +} + +fn extract_server_url(_content: &str) -> Result { + Ok("unknown".to_string()) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 26af462d..e7b318f7 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -5,5 +5,6 @@ pub mod db; pub mod docs; pub mod image; pub mod integrations; +pub mod kube; pub mod shell; pub mod system; diff --git a/src-tauri/src/kube/client.rs b/src-tauri/src/kube/client.rs new file mode 100644 index 00000000..85e19cb4 --- /dev/null +++ b/src-tauri/src/kube/client.rs @@ -0,0 +1,22 @@ +pub struct ClusterClient { + pub id: String, + pub name: String, + pub context: String, + pub server_url: String, +} + +impl ClusterClient { + pub fn new( + id: String, + name: String, + context: String, + server_url: String, + ) -> Self { + Self { + id, + name, + context, + server_url, + } + } +} diff --git a/src-tauri/src/kube/mod.rs b/src-tauri/src/kube/mod.rs new file mode 100644 index 00000000..881e5b9f --- /dev/null +++ b/src-tauri/src/kube/mod.rs @@ -0,0 +1,7 @@ +pub mod client; +pub mod portforward; +pub mod refresh; + +pub use client::ClusterClient; +pub use portforward::{PortForwardSession, PortForwardStatus}; +pub use refresh::RefreshRegistry; diff --git a/src-tauri/src/kube/portforward.rs b/src-tauri/src/kube/portforward.rs new file mode 100644 index 00000000..44944971 --- /dev/null +++ b/src-tauri/src/kube/portforward.rs @@ -0,0 +1,47 @@ +pub struct PortForwardSession { + pub id: String, + pub cluster_id: String, + pub namespace: String, + pub pod: String, + pub container: Option, + pub ports: Vec, + pub local_ports: Vec, + pub status: PortForwardStatus, +} + +pub enum PortForwardStatus { + Active, + Stopped, + Error(String), +} + +impl PortForwardSession { + pub fn new( + id: String, + cluster_id: String, + namespace: String, + pod: String, + container: Option, + ports: Vec, + local_ports: Vec, + ) -> Self { + Self { + id, + cluster_id, + namespace, + pod, + container, + ports, + local_ports, + status: PortForwardStatus::Active, + } + } + + pub fn stop(&mut self) { + self.status = PortForwardStatus::Stopped; + } + + pub fn is_active(&self) -> bool { + matches!(self.status, PortForwardStatus::Active) + } +} diff --git a/src-tauri/src/kube/refresh.rs b/src-tauri/src/kube/refresh.rs new file mode 100644 index 00000000..566f3cba --- /dev/null +++ b/src-tauri/src/kube/refresh.rs @@ -0,0 +1,35 @@ +use std::collections::HashMap; +use tokio::sync::RwLock; +use std::sync::Arc; + +pub struct RefreshRegistry { + domains: HashMap, +} + +impl Default for RefreshRegistry { + fn default() -> Self { + Self::new() + } +} + +pub struct Domain { + pub name: String, + pub refresh_interval: std::time::Duration, + pub data: Arc>>, +} + +impl RefreshRegistry { + pub fn new() -> Self { + Self { + domains: HashMap::new(), + } + } + + pub async fn register_domain(&mut self, domain: Domain) { + self.domains.insert(domain.name.clone(), domain); + } + + pub async fn get_domain(&self, name: &str) -> Option<&Domain> { + self.domains.get(name) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bf324238..b2916bed 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,6 +4,7 @@ pub mod commands; pub mod db; pub mod docs; pub mod integrations; +pub mod kube; pub mod mcp; pub mod ollama; pub mod pii; @@ -40,6 +41,9 @@ pub fn run() { integration_webviews: Arc::new(Mutex::new(std::collections::HashMap::new())), mcp_connections: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), pending_approvals: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), + clusters: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), + port_forwards: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), + refresh_registry: Arc::new(tokio::sync::Mutex::new(crate::kube::RefreshRegistry::new())), }; let stronghold_salt = format!( "tftsr-stronghold-salt-v1-{:x}", @@ -170,6 +174,13 @@ pub fn run() { commands::shell::respond_to_shell_approval, commands::shell::list_command_executions, commands::shell::check_kubectl_installed, + // Kubernetes Management + commands::kube::add_cluster, + commands::kube::remove_cluster, + commands::kube::list_clusters, + commands::kube::start_port_forward, + commands::kube::stop_port_forward, + commands::kube::list_port_forwards, ]) .run(tauri::generate_context!()) .expect("Error running Troubleshooting and RCA Assistant application"); diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 64d5df9f..5cd05e40 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -91,6 +91,12 @@ pub struct AppState { /// Pending shell command approvals: approval_id -> response channel pub pending_approvals: Arc>>>, + /// Kubernetes cluster clients: cluster_id -> client + pub clusters: Arc>>, + /// Port forwarding sessions: session_id -> session + pub port_forwards: Arc>>, + /// Refresh registry for domain-based data fetching + pub refresh_registry: Arc>, } /// Determine the application data directory.