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/proper-lockfile": "^4.1.1",
"@types/tar": "^4.0.3",
"chalk": "^4.1.0",
"conf": "^7.0.1",
"crypto-js": "^4.0.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 clusters = observable.map<ClusterId, Cluster>();
@computed get activeCluster(): Cluster {
@computed get activeCluster(): Cluster | null {
return this.getById(this.activeClusterId);
}

View File

@ -31,7 +31,6 @@ export class Cluster implements ClusterModel {
@observable kubeConfigPath: string;
@observable apiUrl: string; // cluster server 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 online: boolean;
@observable accessible: boolean;
@ -59,20 +58,21 @@ export class Cluster implements ClusterModel {
async init(port: number) {
try {
this.contextHandler = new ContextHandler(this);
const contextPort = await this.contextHandler.ensurePort();
this.kubeAuthProxyUrl = `http://127.0.0.1:${contextPort}`;
this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler);
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
this.webContentUrl = `http://${this.id}.localhost:${port}`;
this.kubeconfigManager = new KubeconfigManager(this);
this.initialized = true;
logger.info(`Cluster(${this.id}) init success`, {
logger.info(`Cluster init success`, {
id: this.id,
serverUrl: this.apiUrl,
webContentUrl: this.webContentUrl,
kubeProxyUrl: this.kubeProxyUrl,
kubeAuthProxyUrl: this.kubeAuthProxyUrl,
});
} 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;
}
public async resolveAuthProxyUrl() {
const proxyPort = await this.ensurePort();
return `http://127.0.0.1:${proxyPort}`;
}
public async getApiTarget(isWatchRequest = false): Promise<httpProxy.ServerOptions> {
if (this.apiTarget && !isWatchRequest) {
return this.apiTarget
@ -81,19 +86,14 @@ export class ContextHandler {
}
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
await this.ensurePort();
const proxyUrl = await this.resolveAuthProxyUrl();
return {
target: proxyUrl + this.clusterUrl.path,
changeOrigin: true,
timeout: timeout,
headers: {
"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 { Cluster } from "./cluster"
import type { ContextHandler } from "./context-handler";
import { app } from "electron"
import path from "path"
import fs from "fs-extra"
@ -10,18 +11,13 @@ export class KubeconfigManager {
protected configDir = app.getPath("temp")
protected tempFile: string;
constructor(protected cluster: Cluster) {
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")
}
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler) {
this.init();
}
protected async init() {
try {
await this.contextHandler.ensurePort();
await this.createProxyKubeconfig();
} catch (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.
*/
protected async createProxyKubeconfig(): Promise<string> {
const { configDir, cluster } = this;
const { configDir, cluster, contextHandler } = this;
const { contextName, kubeConfigPath, id } = cluster;
const tempFile = path.join(configDir, `kubeconfig-${id}`);
const kubeConfig = loadConfig(kubeConfigPath);
const proxyUser = "proxy";
const proxyConfig: Partial<KubeConfig> = {
currentContext: contextName,
clusters: [
{
name: contextName,
server: cluster.kubeAuthProxyUrl,
server: await contextHandler.resolveAuthProxyUrl(),
skipTLSVerify: undefined,
}
],
users: [
{ name: proxyUser },
{ name: "proxy" },
],
contexts: [
{
user: proxyUser,
user: "proxy",
name: contextName,
cluster: contextName,
namespace: kubeConfig.getContextObject(contextName).namespace,
}
]
};
// write
const configYaml = dumpConfigYaml(proxyConfig);
fs.ensureDir(path.dirname(tempFile));
fs.writeFileSync(tempFile, configYaml);
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
this.tempFile = tempFile;
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
return tempFile;
}

View File

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

View File

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

View File

@ -1,7 +1,19 @@
.ClusterIcon {
position: relative;
--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 {
width: var(--size);
height: var(--size);

View File

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

View File

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

View File

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

View File

@ -3442,6 +3442,14 @@ chalk@^4.0.0:
ansi-styles "^4.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:
version "1.0.2"
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"