From 5a2a9248e8489fc1af43ce7bef5760c09f952019 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 10 Aug 2022 09:37:25 -0400 Subject: [PATCH] Make KubeconfigDialog injectable Signed-off-by: Sebastian Malton --- .../get-service-account-route.injectable.ts | 35 +---- .../service-account-menu.tsx | 23 ++- .../components/delete-cluster-dialog/view.tsx | 2 +- .../kubeconfig-dialog/kubeconfig-dialog.tsx | 148 +++++++----------- ...e-account-kube-config-dialog.injectable.ts | 31 ++++ .../kubeconfig-dialog/open.injectable.ts | 40 +++++ .../kubeconfig-dialog/state.injectable.ts | 14 ++ 7 files changed, 168 insertions(+), 125 deletions(-) create mode 100644 src/renderer/components/kubeconfig-dialog/open-service-account-kube-config-dialog.injectable.ts create mode 100644 src/renderer/components/kubeconfig-dialog/open.injectable.ts create mode 100644 src/renderer/components/kubeconfig-dialog/state.injectable.ts diff --git a/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts b/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts index 48bdc5f644..476ee983a3 100644 --- a/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts +++ b/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts @@ -9,6 +9,7 @@ import type { Cluster } from "../../../common/cluster/cluster"; import type { V1Secret } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node"; import { clusterRoute } from "../../router/route"; +import { dump } from "js-yaml"; const getServiceAccountRouteInjectable = getRouteInjectable({ id: "get-service-account-route", @@ -50,43 +51,15 @@ const getServiceAccountRouteInjectable = getRouteInjectable({ export default getServiceAccountRouteInjectable; -interface ServiceAccountKubeConfig { - apiVersion: string; - kind: string; - clusters: { - name: string; - cluster: { - server: string; - "certificate-authority-data": string; - }; - }[]; - users: { - name: string; - user: { - token: string; - }; - }[]; - contexts: { - name: string; - context: { - user: string; - cluster: string; - namespace: string | undefined; - }; - }[]; - "current-context": string; -} - -function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster): ServiceAccountKubeConfig | undefined { +function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster): string | undefined { if (!secret.data || !secret.metadata) { return undefined; } const { token, "ca.crt": caCrt } = secret.data; - const tokenData = Buffer.from(token, "base64"); - return { + return dump({ "apiVersion": "v1", "kind": "Config", "clusters": [ @@ -117,5 +90,5 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster }, ], "current-context": cluster.contextName, - }; + }); } diff --git a/src/renderer/components/+user-management/+service-accounts/service-account-menu.tsx b/src/renderer/components/+user-management/+service-accounts/service-account-menu.tsx index 23540ce3ea..63b1d89a20 100644 --- a/src/renderer/components/+user-management/+service-accounts/service-account-menu.tsx +++ b/src/renderer/components/+user-management/+service-accounts/service-account-menu.tsx @@ -7,20 +7,33 @@ import React from "react"; import type { KubeObjectMenuProps } from "../../kube-object-menu"; import type { ServiceAccount } from "../../../../common/k8s-api/endpoints"; import { MenuItem } from "../../menu"; -import { openServiceAccountKubeConfig } from "../../kubeconfig-dialog"; import { Icon } from "../../icon"; +import type { OpenServiceAccountKubeConfigDialog } from "../../kubeconfig-dialog/open-service-account-kube-config-dialog.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import openServiceAccountKubeConfigDialogInjectable from "../../kubeconfig-dialog/open-service-account-kube-config-dialog.injectable"; -export function ServiceAccountMenu(props: KubeObjectMenuProps) { - const { object, toolbar } = props; +interface Dependencies { + openServiceAccountKubeConfigDialog: OpenServiceAccountKubeConfigDialog; +} + +function NonInjectedServiceAccountMenu(props: KubeObjectMenuProps & Dependencies) { + const { object, toolbar, openServiceAccountKubeConfigDialog } = props; return ( - openServiceAccountKubeConfig(object)}> + openServiceAccountKubeConfigDialog(object)}> Kubeconfig ); } + +export const ServiceAccountMenu = withInjectables>(NonInjectedServiceAccountMenu, { + getProps: (di, props) => ({ + ...props, + openServiceAccountKubeConfigDialog: di.inject(openServiceAccountKubeConfigDialogInjectable), + }), +}); diff --git a/src/renderer/components/delete-cluster-dialog/view.tsx b/src/renderer/components/delete-cluster-dialog/view.tsx index 3d86bae4a9..1e65e0ebf0 100644 --- a/src/renderer/components/delete-cluster-dialog/view.tsx +++ b/src/renderer/components/delete-cluster-dialog/view.tsx @@ -23,7 +23,7 @@ import hotbarStoreInjectable from "../../../common/hotbars/store.injectable"; import type { DeleteClusterDialogState } from "./state.injectable"; import deleteClusterDialogStateInjectable from "./state.injectable"; -export interface Dependencies { +interface Dependencies { state: IObservableValue; hotbarStore: HotbarStore; } diff --git a/src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx b/src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx index 3028faa902..fd497c9c35 100644 --- a/src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx +++ b/src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx @@ -5,121 +5,95 @@ import styles from "./kubeconfig-dialog.module.scss"; import React from "react"; -import { makeObservable, observable } from "mobx"; +import type { IObservableValue } from "mobx"; import { observer } from "mobx-react"; -import yaml from "js-yaml"; -import type { ServiceAccount } from "../../../common/k8s-api/endpoints"; import { cssNames, saveFileDialog } from "../../utils"; import { Button } from "../button"; import type { DialogProps } from "../dialog"; import { Dialog } from "../dialog"; import { Icon } from "../icon"; -import { Notifications } from "../notifications"; +import type { ShowNotification } from "../notifications"; import { Wizard, WizardStep } from "../wizard"; -import { apiBase } from "../../api"; import { MonacoEditor } from "../monaco-editor"; import { clipboard } from "electron"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import showSuccessNotificationInjectable from "../notifications/show-success-notification.injectable"; +import kubeconfigDialogStateInjectable from "./state.injectable"; export interface KubeconfigDialogData { title?: React.ReactNode; - loader: () => Promise; + config: string; } export interface KubeConfigDialogProps extends Partial { } -const dialogState = observable.box(); +interface Dependencies { + state: IObservableValue; + showSuccessNotification: ShowNotification; +} @observer -export class KubeConfigDialog extends React.Component { - @observable config = ""; // parsed kubeconfig in yaml format - - constructor(props: KubeConfigDialogProps) { +class NonInjectedKubeConfigDialog extends React.Component { + constructor(props: KubeConfigDialogProps & Dependencies) { super(props); - makeObservable(this); - } - - static open(data: KubeconfigDialogData) { - dialogState.set(data); - } - - static close() { - dialogState.set(undefined); } close = () => { - KubeConfigDialog.close(); + this.props.state.set(undefined); }; - onOpen = (data: KubeconfigDialogData) => { - this.loadConfig(data); + copyToClipboard = (config: string) => { + clipboard.writeText(config); + this.props.showSuccessNotification("Config copied to clipboard"); }; - async loadConfig(data: KubeconfigDialogData) { - const config = await data.loader().catch(err => { - Notifications.error(err); - this.close(); - }); - - this.config = config ? yaml.dump(config) : ""; - } - - copyToClipboard = () => { - clipboard.writeText(this.config); - Notifications.ok("Config copied to clipboard"); + download = (config: string) => { + saveFileDialog("config", config, "text/yaml"); }; - download = () => { - saveFileDialog("config", this.config, "text/yaml"); - }; - - renderContents(data: KubeconfigDialogData) { - const yamlConfig = this.config; - - return ( - {data.title || "Kubeconfig File"}}> - - - - - - )} - prev={this.close} - > - - - - ); - } + renderContents = (data: KubeconfigDialogData) => ( + { data.title || "Kubeconfig File" }}> + + + + + + ) } + prev={this.close} + > + + + + ); render() { - const { className, ...dialogProps } = this.props; - const data = dialogState.get(); + const { className, state, ...dialogProps } = this.props; + const data = state.get(); return ( this.onOpen(data))} + isOpen={!!data} close={this.close} > {data && this.renderContents(data)} @@ -128,12 +102,10 @@ export class KubeConfigDialog extends React.Component { } } -export function openServiceAccountKubeConfig(account: ServiceAccount) { - const accountName = account.getName(); - const namespace = account.getNs(); - - KubeConfigDialog.open({ - title: `${accountName} kubeconfig`, - loader: () => apiBase.get(`/kubeconfig/service-account/${namespace}/${accountName}`), - }); -} +export const KubeConfigDialog = withInjectables(NonInjectedKubeConfigDialog, { + getProps: (di, props) => ({ + ...props, + showSuccessNotification: di.inject(showSuccessNotificationInjectable), + state: di.inject(kubeconfigDialogStateInjectable), + }), +}); diff --git a/src/renderer/components/kubeconfig-dialog/open-service-account-kube-config-dialog.injectable.ts b/src/renderer/components/kubeconfig-dialog/open-service-account-kube-config-dialog.injectable.ts new file mode 100644 index 0000000000..3a612dec2e --- /dev/null +++ b/src/renderer/components/kubeconfig-dialog/open-service-account-kube-config-dialog.injectable.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { ServiceAccount } from "../../../common/k8s-api/endpoints"; +import { urlBuilderFor } from "../../../common/utils/buildUrl"; +import apiBaseInjectable from "../../k8s/api-base.injectable"; +import openKubeconfigDialogInjectable from "./open.injectable"; + +export type OpenServiceAccountKubeConfigDialog = (account: ServiceAccount) => void; + +const serviceAccountConfigEndpoint = urlBuilderFor("/kubeconfig/service-account/:namespace/:name"); + +const openServiceAccountKubeConfigDialogInjectable = getInjectable({ + id: "open-service-account-kube-config-dialog", + instantiate: (di): OpenServiceAccountKubeConfigDialog => { + const apiBase = di.inject(apiBaseInjectable); + const openKubeconfigDialog = di.inject(openKubeconfigDialogInjectable); + + return (account) => openKubeconfigDialog({ + title: `${account.getName()} kubeconfig`, + loader: () => apiBase.get(serviceAccountConfigEndpoint.compile({ + name: account.getName(), + namespace: account.getNs(), + })), + }); + }, +}); + +export default openServiceAccountKubeConfigDialogInjectable; diff --git a/src/renderer/components/kubeconfig-dialog/open.injectable.ts b/src/renderer/components/kubeconfig-dialog/open.injectable.ts new file mode 100644 index 0000000000..c3a0d7f87f --- /dev/null +++ b/src/renderer/components/kubeconfig-dialog/open.injectable.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type React from "react"; +import loggerInjectable from "../../../common/logger.injectable"; +import showCheckedErrorNotificationInjectable from "../notifications/show-checked-error.injectable"; +import kubeconfigDialogStateInjectable from "./state.injectable"; + +export interface OpenKubeconfigDialogArgs { + title?: React.ReactNode; + loader: () => Promise; +} + +export type OpenKubeconfigDialog = (openArgs: OpenKubeconfigDialogArgs) => void; + +const openKubeconfigDialogInjectable = getInjectable({ + id: "open-kubeconfig-dialog", + instantiate: (di): OpenKubeconfigDialog => { + const state = di.inject(kubeconfigDialogStateInjectable); + const showCheckedErrorNotification = di.inject(showCheckedErrorNotificationInjectable); + const logger = di.inject(loggerInjectable); + + return ({ title, loader }) => { + (async () => { + try { + const config = await loader(); + + state.set({ title, config }); + } catch (error) { + showCheckedErrorNotification(error, "Failed to retrive config for dialog"); + logger.warn("[KUBEOCONFIG-DIALOG]: failed to retrived config for dialog", error); + } + })(); + }; + }, +}); + +export default openKubeconfigDialogInjectable; diff --git a/src/renderer/components/kubeconfig-dialog/state.injectable.ts b/src/renderer/components/kubeconfig-dialog/state.injectable.ts new file mode 100644 index 0000000000..e85271cda6 --- /dev/null +++ b/src/renderer/components/kubeconfig-dialog/state.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { observable } from "mobx"; +import type { KubeconfigDialogData } from "./kubeconfig-dialog"; + +const kubeconfigDialogStateInjectable = getInjectable({ + id: "kubeconfig-dialog-state", + instantiate: () => observable.box(), +}); + +export default kubeconfigDialogStateInjectable;