1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

apis / clusters-menu refactoring

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-14 15:04:26 +03:00
parent 4e88715b8d
commit fbcb2fd281
12 changed files with 72 additions and 50 deletions

View File

@ -168,6 +168,7 @@
"@types/node": "^12.12.45", "@types/node": "^12.12.45",
"@types/proper-lockfile": "^4.1.1", "@types/proper-lockfile": "^4.1.1",
"@types/tar": "^4.0.3", "@types/tar": "^4.0.3",
"chalk": "^4.1.0",
"conf": "^7.0.1", "conf": "^7.0.1",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"electron-promise-ipc": "^2.1.0", "electron-promise-ipc": "^2.1.0",

View File

@ -52,7 +52,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@observable removedClusters = observable.map<ClusterId, Cluster>(); @observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>(); @observable clusters = observable.map<ClusterId, Cluster>();
@computed get activeCluster(): Cluster { @computed get activeCluster(): Cluster | null {
return this.getById(this.activeClusterId); return this.getById(this.activeClusterId);
} }

View File

@ -31,7 +31,6 @@ export class Cluster implements ClusterModel {
@observable kubeConfigPath: string; @observable kubeConfigPath: string;
@observable apiUrl: string; // cluster server url @observable apiUrl: string; // cluster server url
@observable kubeProxyUrl: string; // lens-proxy to kube-api url @observable kubeProxyUrl: string; // lens-proxy to kube-api url
@observable kubeAuthProxyUrl: string; // auth-proxy to temp kube-config
@observable webContentUrl: string; // page content url for loading in renderer @observable webContentUrl: string; // page content url for loading in renderer
@observable online: boolean; @observable online: boolean;
@observable accessible: boolean; @observable accessible: boolean;
@ -59,20 +58,21 @@ export class Cluster implements ClusterModel {
async init(port: number) { async init(port: number) {
try { try {
this.contextHandler = new ContextHandler(this); this.contextHandler = new ContextHandler(this);
const contextPort = await this.contextHandler.ensurePort(); this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler);
this.kubeAuthProxyUrl = `http://127.0.0.1:${contextPort}`;
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`; this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
this.webContentUrl = `http://${this.id}.localhost:${port}`; this.webContentUrl = `http://${this.id}.localhost:${port}`;
this.kubeconfigManager = new KubeconfigManager(this);
this.initialized = true; this.initialized = true;
logger.info(`Cluster(${this.id}) init success`, { logger.info(`Cluster init success`, {
id: this.id,
serverUrl: this.apiUrl, serverUrl: this.apiUrl,
webContentUrl: this.webContentUrl, webContentUrl: this.webContentUrl,
kubeProxyUrl: this.kubeProxyUrl, kubeProxyUrl: this.kubeProxyUrl,
kubeAuthProxyUrl: this.kubeAuthProxyUrl,
}); });
} catch (err) { } catch (err) {
logger.error(`💣 Cluster(${this.id}) init failed: ${err}`); logger.error(`💣 Cluster init failed: ${err}`, {
id: this.id,
error: err,
});
} }
} }

View File

@ -68,6 +68,11 @@ export class ContextHandler {
return this.prometheusPath; return this.prometheusPath;
} }
public async resolveAuthProxyUrl() {
const proxyPort = await this.ensurePort();
return `http://127.0.0.1:${proxyPort}`;
}
public async getApiTarget(isWatchRequest = false): Promise<httpProxy.ServerOptions> { public async getApiTarget(isWatchRequest = false): Promise<httpProxy.ServerOptions> {
if (this.apiTarget && !isWatchRequest) { if (this.apiTarget && !isWatchRequest) {
return this.apiTarget return this.apiTarget
@ -81,19 +86,14 @@ export class ContextHandler {
} }
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> { protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
await this.ensurePort(); const proxyUrl = await this.resolveAuthProxyUrl();
return { return {
target: proxyUrl + this.clusterUrl.path,
changeOrigin: true, changeOrigin: true,
timeout: timeout, timeout: timeout,
headers: { headers: {
"Host": this.clusterUrl.hostname, "Host": this.clusterUrl.hostname,
}, },
target: {
protocol: "http://",
host: "127.0.0.1",
port: this.proxyPort,
path: this.clusterUrl.path
}
} }
} }

View File

@ -1,5 +1,6 @@
import type { KubeConfig } from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node";
import type { Cluster } from "./cluster" import type { Cluster } from "./cluster"
import type { ContextHandler } from "./context-handler";
import { app } from "electron" import { app } from "electron"
import path from "path" import path from "path"
import fs from "fs-extra" import fs from "fs-extra"
@ -10,18 +11,13 @@ export class KubeconfigManager {
protected configDir = app.getPath("temp") protected configDir = app.getPath("temp")
protected tempFile: string; protected tempFile: string;
constructor(protected cluster: Cluster) { constructor(protected cluster: Cluster, protected contextHandler: ContextHandler) {
if(!cluster.kubeAuthProxyUrl) {
throw new Error(`Cluster's auth proxy url must be initialized`)
}
if (!cluster.contextHandler.proxyPort) {
throw new Error("Context-handler proxy port must be resolved")
}
this.init(); this.init();
} }
protected async init() { protected async init() {
try { try {
await this.contextHandler.ensurePort();
await this.createProxyKubeconfig(); await this.createProxyKubeconfig();
} catch (err) { } catch (err) {
logger.error(`Failed to created temp config for auth-proxy`, { err }) logger.error(`Failed to created temp config for auth-proxy`, { err })
@ -37,37 +33,38 @@ export class KubeconfigManager {
* This way any user of the config does not need to know anything about the auth etc. details. * This way any user of the config does not need to know anything about the auth etc. details.
*/ */
protected async createProxyKubeconfig(): Promise<string> { protected async createProxyKubeconfig(): Promise<string> {
const { configDir, cluster } = this; const { configDir, cluster, contextHandler } = this;
const { contextName, kubeConfigPath, id } = cluster; const { contextName, kubeConfigPath, id } = cluster;
const tempFile = path.join(configDir, `kubeconfig-${id}`); const tempFile = path.join(configDir, `kubeconfig-${id}`);
const kubeConfig = loadConfig(kubeConfigPath); const kubeConfig = loadConfig(kubeConfigPath);
const proxyUser = "proxy";
const proxyConfig: Partial<KubeConfig> = { const proxyConfig: Partial<KubeConfig> = {
currentContext: contextName, currentContext: contextName,
clusters: [ clusters: [
{ {
name: contextName, name: contextName,
server: cluster.kubeAuthProxyUrl, server: await contextHandler.resolveAuthProxyUrl(),
skipTLSVerify: undefined, skipTLSVerify: undefined,
} }
], ],
users: [ users: [
{ name: proxyUser }, { name: "proxy" },
], ],
contexts: [ contexts: [
{ {
user: proxyUser, user: "proxy",
name: contextName, name: contextName,
cluster: contextName, cluster: contextName,
namespace: kubeConfig.getContextObject(contextName).namespace, namespace: kubeConfig.getContextObject(contextName).namespace,
} }
] ]
}; };
// write
const configYaml = dumpConfigYaml(proxyConfig); const configYaml = dumpConfigYaml(proxyConfig);
fs.ensureDir(path.dirname(tempFile)); fs.ensureDir(path.dirname(tempFile));
fs.writeFileSync(tempFile, configYaml); fs.writeFileSync(tempFile, configYaml);
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
this.tempFile = tempFile; this.tempFile = tempFile;
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
return tempFile; return tempFile;
} }

View File

@ -42,10 +42,13 @@ export class WindowManager {
}); });
}), }),
// auto-show active cluster view // auto-show active cluster view
reaction(() => clusterStore.activeClusterId, clusterId => { reaction(() => clusterStore.activeCluster, activeCluster => {
this.activateView(clusterId); if (activeCluster) {
this.activateView(activeCluster.id);
}
}, { }, {
fireImmediately: true, fireImmediately: true,
delay: 250,
}) })
) )
} }
@ -65,10 +68,7 @@ export class WindowManager {
async activateView(clusterId: ClusterId) { async activateView(clusterId: ClusterId) {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (!cluster) { if (!cluster) return;
logger.error(`Can't show a view for non-existing cluster(${clusterId})`);
return;
}
try { try {
const activeView = this.activeView; const activeView = this.activeView;
const isFresh = !this.getView(clusterId); const isFresh = !this.getView(clusterId);

View File

@ -22,6 +22,7 @@ export default migration({
try { try {
// take the embedded kubeconfig and dump it into a file // take the embedded kubeconfig and dump it into a file
cluster.kubeConfigPath = writeEmbeddedKubeConfig(cluster.id, cluster.kubeConfig) cluster.kubeConfigPath = writeEmbeddedKubeConfig(cluster.id, cluster.kubeConfig)
delete cluster.kubeConfig;
migratedClusters.push(cluster) migratedClusters.push(cluster)
} catch (error) { } catch (error) {
log(`Failed to migrate Kubeconfig for cluster "${cluster.id}"`, error) log(`Failed to migrate Kubeconfig for cluster "${cluster.id}"`, error)

View File

@ -1,7 +1,19 @@
.ClusterIcon { .ClusterIcon {
position: relative;
--size: 40px; --size: 40px;
position: relative;
opacity: .75;
border-radius: $radius;
cursor: pointer;
&.interactive {
&:hover {
opacity: 1;
background-color: #fff;
box-shadow: 0 0 0 $radius #fff;
}
}
> img { > img {
width: var(--size); width: var(--size);
height: var(--size); height: var(--size);

View File

@ -2,7 +2,8 @@ import "./cluster-icon.scss"
import React, { DOMAttributes } from "react"; import React, { DOMAttributes } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Hashicon, HashiconProps } from "@emeraldpay/hashicon-react"; import { Params as HashiconParams } from "@emeraldpay/hashicon";
import { Hashicon } from "@emeraldpay/hashicon-react";
import { Cluster } from "../../../main/cluster"; import { Cluster } from "../../../main/cluster";
import { cssNames, IClassName } from "../../utils"; import { cssNames, IClassName } from "../../utils";
import { Badge } from "../badge"; import { Badge } from "../badge";
@ -11,12 +12,13 @@ interface Props extends DOMAttributes<HTMLElement> {
cluster: Cluster; cluster: Cluster;
className?: IClassName; className?: IClassName;
errorClass?: IClassName; errorClass?: IClassName;
showErrorCount?: boolean; showErrors?: boolean;
options?: HashiconProps["options"] interactive?: boolean;
options?: HashiconParams;
} }
const defaultProps: Partial<Props> = { const defaultProps: Partial<Props> = {
showErrorCount: true, showErrors: true,
}; };
@observer @observer
@ -24,14 +26,17 @@ export class ClusterIcon extends React.Component<Props> {
static defaultProps = defaultProps as object; static defaultProps = defaultProps as object;
render() { render() {
const { className, cluster, showErrorCount, errorClass, options, children, ...elemProps } = this.props; const { className: cName, cluster, showErrors, errorClass, options, interactive, children, ...elemProps } = this.props;
const { isAdmin, eventCount, preferences } = cluster; const { isAdmin, eventCount, preferences } = cluster;
const { clusterName, icon } = preferences; const { clusterName, icon } = preferences;
const className = cssNames("ClusterIcon flex inline", cName, {
interactive: interactive || !!this.props.onClick,
});
return ( return (
<div className={cssNames("ClusterIcon flex inline", className)} {...elemProps}> <div {...elemProps} className={className}>
{icon && <img src={icon} alt={clusterName}/>} {icon && <img src={icon} alt={clusterName}/>}
{!icon && <Hashicon value={clusterName} options={options}/>} {!icon && <Hashicon value={clusterName} options={options}/>}
{showErrorCount && isAdmin && eventCount > 0 && ( {showErrors && isAdmin && eventCount > 0 && (
<Badge <Badge
className={cssNames("events-count", errorClass)} className={cssNames("events-count", errorClass)}
label={eventCount >= 1000 ? Math.ceil(eventCount / 1000) * 1000 + "+" : eventCount} label={eventCount >= 1000 ? Math.ceil(eventCount / 1000) * 1000 + "+" : eventCount}

View File

@ -1,13 +1,11 @@
.ClustersMenu { .ClustersMenu {
@include hidden-scrollbar; @include hidden-scrollbar;
$menuBgc: #252729;
--flex-gap: #{$padding * 2};
--menu-bgc: #252729;
padding: $padding * 1.5; padding: $padding * 1.5;
background: $menuBgc; background: var(--menu-bgc);
> * {
cursor: pointer;
}
.add-cluster { .add-cluster {
position: relative; position: relative;
@ -15,7 +13,7 @@
.Icon.add { .Icon.add {
border-radius: $radius; border-radius: $radius;
padding: $padding / 3; padding: $padding / 3;
color: $menuBgc !important; color: var(--menu-bgc) !important;
background: white !important; background: white !important;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;

View File

@ -63,8 +63,8 @@ export class ClustersMenu extends React.Component<Props> {
return ( return (
<ClusterIcon <ClusterIcon
key={cluster.id} key={cluster.id}
showErrors={true}
cluster={cluster} cluster={cluster}
showErrorCount={true}
className={cssNames({ active: isActive })} className={cssNames({ active: isActive })}
onClick={() => this.selectCluster(cluster)} onClick={() => this.selectCluster(cluster)}
onContextMenu={() => this.showContextMenu(cluster)} onContextMenu={() => this.showContextMenu(cluster)}

View File

@ -3442,6 +3442,14 @@ chalk@^4.0.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
chalk@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
char-regex@^1.0.2: char-regex@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"