Skip to content

Commit

Permalink
feat: support for smoothly handling expired aws sso sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
unxsist committed Apr 29, 2024
1 parent a0741df commit 5c96e03
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 20 deletions.
35 changes: 33 additions & 2 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use k8s_openapi::api::core::v1::{
ConfigMap, Namespace, PersistentVolume, PersistentVolumeClaim, Pod, Secret, Service,
};
use kube::api::{DeleteParams, ListParams};
use kube::config::{KubeConfigOptions, Kubeconfig, KubeconfigError};
use kube::config::{KubeConfigOptions, Kubeconfig, KubeconfigError, NamedAuthInfo};
use kube::{api::Api, Client, Config, Error};
use portable_pty::{native_pty_system, CommandBuilder, PtySize};
use serde::Serialize;
Expand Down Expand Up @@ -107,6 +107,36 @@ async fn list_contexts() -> Result<Vec<String>, SerializableKubeError> {
.collect()
}

#[tauri::command]
async fn get_context_auth_info(context: &str) -> Result<NamedAuthInfo, SerializableKubeError> {
let config = Kubeconfig::read().map_err(|err| SerializableKubeError::from(err))?;

let context_auth_info = config
.contexts
.iter()
.find(|c| c.name == context)
.map(|c| c.clone().context.unwrap().user.clone())
.ok_or(SerializableKubeError {
message: "Context not found".to_string(),
code: None,
reason: None,
details: None,
})?;

let auth_info = config
.auth_infos
.iter()
.find(|a| a.name == context_auth_info)
.ok_or(SerializableKubeError {
message: "Auth info not found".to_string(),
code: None,
reason: None,
details: None,
})?;

return Ok(auth_info.clone());
}

async fn client_with_context(context: &str) -> Result<Client, SerializableKubeError> {
if context.to_string() != CURRENT_CONTEXT.lock().unwrap().as_ref().unwrap().clone() {
let options = KubeConfigOptions {
Expand Down Expand Up @@ -663,7 +693,7 @@ fn write_to_pty(session_id: &str, data: &str) {
}
}

fn main() {
fn main() {
let _ = fix_path_env::fix();

let metadata = AboutMetadata::new()
Expand Down Expand Up @@ -698,6 +728,7 @@ fn main() {
})
.invoke_handler(tauri::generate_handler![
list_contexts,
get_context_auth_info,
get_current_context,
list_namespaces,
get_core_api_versions,
Expand Down
5 changes: 5 additions & 0 deletions src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
"name": "kubectl",
"cmd": "kubectl",
"args": true
},
{
"name": "aws",
"cmd": "aws",
"args": true
}
]
}
Expand Down
16 changes: 8 additions & 8 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ import UpdateHandler from "./components/UpdateHandler.vue";
<Suspense>
<SettingsContextProvider>
<ColorSchemeProvider>
<KubeContextProvider>
<TabProvider>
<CommandPaletteProvider>
<DialogProvider>
<DialogProvider>
<KubeContextProvider>
<TabProvider>
<CommandPaletteProvider>
<Navigation />
<RouterViewport />
<Toaster />
<CommandPalette />
<DialogHandler />
<UpdateHandler />
</DialogProvider>
</CommandPaletteProvider>
</TabProvider>
</KubeContextProvider>
</CommandPaletteProvider>
</TabProvider>
</KubeContextProvider>
</DialogProvider>
</ColorSchemeProvider>
</SettingsContextProvider>
</Suspense>
Expand Down
54 changes: 50 additions & 4 deletions src/components/ContextSwitcher.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { injectStrict } from "@/lib/utils";
import {
ShowSingleCommandKey,
RegisterCommandStateKey,
CloseCommandPaletteKey,
RerunLastCommandKey,
} from "@/providers/CommandPaletteProvider";
import {
KubeContextSetContextKey,
Expand All @@ -12,13 +14,17 @@ import {
import { KubeContextStateKey } from "@/providers/KubeContextProvider";
import { Kubernetes } from "@/services/Kubernetes";
import { SettingsContextStateKey } from "@/providers/SettingsContextProvider";
import { DialogProviderSpawnDialogKey } from "@/providers/DialogProvider";
const showSingleCommand = injectStrict(ShowSingleCommandKey);
const registerCommand = injectStrict(RegisterCommandStateKey);
const closeCommandPalette = injectStrict(CloseCommandPaletteKey);
const rerunLastCommand = injectStrict(RerunLastCommandKey);
const { context, namespace } = injectStrict(KubeContextStateKey);
const setContext = injectStrict(KubeContextSetContextKey);
const setNamespace = injectStrict(KubeContextSetNamespaceKey);
const { settings } = injectStrict(SettingsContextStateKey);
const spawnDialog = injectStrict(DialogProviderSpawnDialogKey);
onMounted(() => {
registerCommand({
Expand All @@ -38,7 +44,7 @@ onMounted(() => {
(c) => c.context === context
);
let namespaces = [];
let namespaces: string[] = [];
if (
clusterSettings &&
Expand All @@ -47,9 +53,49 @@ onMounted(() => {
) {
namespaces = clusterSettings.namespaces;
} else {
namespaces = await (
await Kubernetes.getNamespaces(context)
).map((ns) => ns.metadata?.name || "");
try {
const contextNamespaces = await Kubernetes.getNamespaces(context);
namespaces = contextNamespaces.map(
(ns) => ns.metadata?.name || ""
);
} catch (e: any) {
const authErrorHandler = await Kubernetes.getAuthErrorHandler(
context,
e.message
);
if (authErrorHandler.canHandle) {
spawnDialog({
title: "SSO Session expired",
message:
"Failed to authenticate as the SSO session has expired. Please login again.",
buttons: [
{
label: "Close",
variant: "ghost",
handler: (dialog) => {
dialog.close();
closeCommandPalette();
},
},
{
label: "Login with SSO",
handler: (dialog) => {
dialog.buttons = [];
dialog.title = "Awaiting SSO login";
dialog.message = "Please wait while we redirect you.";
authErrorHandler.callback(() => {
dialog.close();
rerunLastCommand();
});
},
},
],
});
} else {
throw e;
}
}
}
return [
Expand Down
10 changes: 5 additions & 5 deletions src/components/DialogHandler.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
<script setup lang="ts">
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { DialogProviderStateKey } from "@/providers/DialogProvider";
import { injectStrict } from "@/lib/utils";
Expand All @@ -25,12 +24,13 @@ const { dialog } = injectStrict(DialogProviderStateKey);
{{ dialog.message }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction
<AlertDialogFooter v-if="dialog.buttons.length > 0">
<Button
v-for="(button, index) in dialog.buttons"
:key="index"
:variant="button.variant ?? 'default'"
@click="button.handler(dialog)"
>{{ button.label }}</AlertDialogAction
>{{ button.label }}</Button
>
</AlertDialogFooter>
</AlertDialogContent>
Expand Down
16 changes: 16 additions & 0 deletions src/providers/CommandPaletteProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const CloseCommandPaletteKey: InjectionKey<() => void> = Symbol(
);
export const ExecuteCommandKey: InjectionKey<(command: Command) => void> =
Symbol("ExecuteCommand");
export const RerunLastCommandKey: InjectionKey<() => void> =
Symbol("ExecuteCommand");
export const ClearCommandCallStackKey: InjectionKey<() => void> = Symbol(
"ClearCommandCallStack"
);
Expand Down Expand Up @@ -143,9 +145,23 @@ export default {
}
};

const rerunLastCommand = () => {
const lastCommand = Array.from(state.callStack.keys())[
state.callStack.size - 1
];

if (!lastCommand) {
return;
}

pop();
executeCommand(lastCommand);
};

provide(OpenCommandPaletteKey, open);
provide(CloseCommandPaletteKey, close);
provide(ExecuteCommandKey, executeCommand);
provide(RerunLastCommandKey, rerunLastCommand);
provide(ClearCommandCallStackKey, clearStack);
provide(ShowSingleCommandKey, showSingleCommand);
provide(RegisterCommandStateKey, registerCommand);
Expand Down
4 changes: 3 additions & 1 deletion src/providers/DialogProvider.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { ToRefs } from "vue";
import { buttonVariants } from "@/components/ui/button";

export const DialogProviderStateKey: InjectionKey<ToRefs<DialogProviderState>> =
Symbol("DialogProviderState");

export const DialogProviderSpawnDialogKey: InjectionKey<
(dialog: DialogInterface) => void
(dialog: BaseDialogInterface) => void
> = Symbol("DialogProviderSpawnDialog");

export interface DialogButtonInterface {
label: string;
variant?: NonNullable<Parameters<typeof buttonVariants>[0]>["variant"];
handler: (dialog: DialogInterface) => void;
}

Expand Down
49 changes: 49 additions & 0 deletions src/services/Kubernetes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "@kubernetes/client-node";
import { VirtualService } from "@kubernetes-models/istio/networking.istio.io/v1beta1";
import { invoke } from "@tauri-apps/api/tauri";
import { Child, Command } from "@tauri-apps/api/shell";

export interface KubernetesError {
message: string;
Expand All @@ -24,6 +25,54 @@ export interface KubernetesError {
}

export class Kubernetes {
static async getAuthErrorHandler(
context: string,
errorMessage: string
): Promise<{
canHandle: boolean;
callback: (authCompletedCallback?: () => void) => void;
}> {
// AWS SSO
if (
errorMessage.includes("AWS_PROFILE") &&
errorMessage.includes("Error loading SSO Token")
) {
const context_auth_info = (await invoke("get_context_auth_info", {
context: context,
})) as any;

let aws_profile = null;
try {
aws_profile = context_auth_info.user.exec.env.find(
(env: any) => env.name === "AWS_PROFILE"
).value;
} catch {}

return {
canHandle: aws_profile !== null,
callback: async (authCompletedCallback?) => {
const command = new Command("aws", [
"sso",
"login",
"--profile",
aws_profile,
]);
command.addListener("close", async () => {
authCompletedCallback?.();
});
await command.spawn();
},
};
}

return {
canHandle: false,
callback: () => {
console.log("void");
},
};
}

static async getCurrentContext(): Promise<string> {
return invoke("get_current_context", {});
}
Expand Down

0 comments on commit 5c96e03

Please sign in to comment.