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:
parent
4e88715b8d
commit
fbcb2fd281
@ -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",
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user