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

Allow user to select Kubeconfig from filesystem, fix #738

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-08-24 19:24:24 +03:00
parent 0f11b03692
commit f067152d04
9 changed files with 252 additions and 91 deletions

View File

@ -83,7 +83,7 @@ msgstr "Account Name"
msgid "Active" msgid "Active"
msgstr "Active" msgstr "Active"
#: src/renderer/components/+add-cluster/add-cluster.tsx:171 #: src/renderer/components/+add-cluster/add-cluster.tsx:176
#: src/renderer/components/cluster-manager/clusters-menu.tsx:116 #: src/renderer/components/cluster-manager/clusters-menu.tsx:116
msgid "Add Cluster" msgid "Add Cluster"
msgstr "Add Cluster" msgstr "Add Cluster"
@ -108,7 +108,7 @@ msgstr "Add bindings to {name}"
#~ msgid "Add cluster" #~ msgid "Add cluster"
#~ msgstr "Add cluster" #~ msgstr "Add cluster"
#: src/renderer/components/+add-cluster/add-cluster.tsx:192 #: src/renderer/components/+add-cluster/add-cluster.tsx:204
msgid "Add cluster(s)" msgid "Add cluster(s)"
msgstr "Add cluster(s)" msgstr "Add cluster(s)"
@ -451,7 +451,7 @@ msgstr "Claim Name"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243 #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243
#: src/renderer/components/dialog/logs-dialog.tsx:39 #: src/renderer/components/dialog/logs-dialog.tsx:39
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:94 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93
msgid "Close" msgid "Close"
msgstr "Close" msgstr "Close"
@ -522,7 +522,7 @@ msgstr "Conditions"
msgid "Config Maps" msgid "Config Maps"
msgstr "Config Maps" msgstr "Config Maps"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:55 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:54
msgid "Config copied to clipboard" msgid "Config copied to clipboard"
msgstr "Config copied to clipboard" msgstr "Config copied to clipboard"
@ -594,7 +594,7 @@ msgid "Conversion"
msgstr "Conversion" msgstr "Conversion"
#: src/renderer/components/dialog/logs-dialog.tsx:36 #: src/renderer/components/dialog/logs-dialog.tsx:36
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:88 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:87
msgid "Copy to clipboard" msgid "Copy to clipboard"
msgstr "Copy to clipboard" msgstr "Copy to clipboard"
@ -710,7 +710,7 @@ msgstr "Currently applied filters:"
msgid "Custom Resources" msgid "Custom Resources"
msgstr "Custom Resources" msgstr "Custom Resources"
#: src/renderer/components/+add-cluster/add-cluster.tsx:116 #: src/renderer/components/+add-cluster/add-cluster.tsx:121
msgid "Custom.." msgid "Custom.."
msgstr "Custom.." msgstr "Custom.."
@ -811,7 +811,7 @@ msgstr "Domains"
msgid "Download Mirror" msgid "Download Mirror"
msgstr "Download Mirror" msgstr "Download Mirror"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:91 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:90
msgid "Download file" msgid "Download file"
msgstr "Download file" msgstr "Download file"
@ -1197,7 +1197,7 @@ msgstr "Kind"
msgid "Kubeconfig" msgid "Kubeconfig"
msgstr "Kubeconfig" msgstr "Kubeconfig"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:85 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:84
msgid "Kubeconfig File" msgid "Kubeconfig File"
msgstr "Kubeconfig File" msgstr "Kubeconfig File"
@ -1731,8 +1731,12 @@ msgid "Persistent Volumes"
msgstr "Persistent Volumes" msgstr "Persistent Volumes"
#: src/renderer/components/+add-cluster/add-cluster.tsx:63 #: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Please select kubeconfig" #~ msgid "Please select kubeconfig"
msgstr "Please select kubeconfig" #~ msgstr "Please select kubeconfig"
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
msgid "Please select kubeconfig context"
msgstr "Please select kubeconfig context"
#: src/renderer/components/+workloads-pods/pod-menu.tsx:50 #: src/renderer/components/+workloads-pods/pod-menu.tsx:50
msgid "Pod" msgid "Pod"
@ -1828,7 +1832,7 @@ msgstr "Provisioner"
msgid "Proxy is used only for non-cluster communication." msgid "Proxy is used only for non-cluster communication."
msgstr "Proxy is used only for non-cluster communication." msgstr "Proxy is used only for non-cluster communication."
#: src/renderer/components/+add-cluster/add-cluster.tsx:176 #: src/renderer/components/+add-cluster/add-cluster.tsx:188
msgid "Proxy settings" msgid "Proxy settings"
msgstr "Proxy settings" msgstr "Proxy settings"
@ -2212,13 +2216,21 @@ msgstr "Secret type"
msgid "Secrets" msgid "Secrets"
msgstr "Secrets" msgstr "Secrets"
#: src/renderer/components/+add-cluster/add-cluster.tsx:185
msgid "Select a context"
msgstr "Select a context"
#: src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx:134 #: src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx:134
msgid "Select a quota.." msgid "Select a quota.."
msgstr "Select a quota.." msgstr "Select a quota.."
#: src/renderer/components/+add-cluster/add-cluster.tsx:173 #: src/renderer/components/+add-cluster/add-cluster.tsx:173
msgid "Select kubeconfig" #~ msgid "Select context"
msgstr "Select kubeconfig" #~ msgstr "Select context"
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
#~ msgid "Select kubeconfig"
#~ msgstr "Select kubeconfig"
#: src/renderer/components/+preferences/preferences.tsx:88 #: src/renderer/components/+preferences/preferences.tsx:88
#~ msgid "Select repository" #~ msgid "Select repository"
@ -2773,7 +2785,7 @@ msgstr "{0} total, {1} available"
msgid "{0} unavailable" msgid "{0} unavailable"
msgstr "{0} unavailable" msgstr "{0} unavailable"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:129 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:128
msgid "{accountName} kubeconfig" msgid "{accountName} kubeconfig"
msgstr "{accountName} kubeconfig" msgstr "{accountName} kubeconfig"

View File

@ -83,7 +83,7 @@ msgstr ""
msgid "Active" msgid "Active"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:171 #: src/renderer/components/+add-cluster/add-cluster.tsx:176
#: src/renderer/components/cluster-manager/clusters-menu.tsx:116 #: src/renderer/components/cluster-manager/clusters-menu.tsx:116
msgid "Add Cluster" msgid "Add Cluster"
msgstr "" msgstr ""
@ -108,7 +108,7 @@ msgstr ""
#~ msgid "Add cluster" #~ msgid "Add cluster"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:192 #: src/renderer/components/+add-cluster/add-cluster.tsx:204
msgid "Add cluster(s)" msgid "Add cluster(s)"
msgstr "" msgstr ""
@ -447,7 +447,7 @@ msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243 #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243
#: src/renderer/components/dialog/logs-dialog.tsx:39 #: src/renderer/components/dialog/logs-dialog.tsx:39
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:94 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93
msgid "Close" msgid "Close"
msgstr "" msgstr ""
@ -518,7 +518,7 @@ msgstr ""
msgid "Config Maps" msgid "Config Maps"
msgstr "" msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:55 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:54
msgid "Config copied to clipboard" msgid "Config copied to clipboard"
msgstr "" msgstr ""
@ -590,7 +590,7 @@ msgid "Conversion"
msgstr "" msgstr ""
#: src/renderer/components/dialog/logs-dialog.tsx:36 #: src/renderer/components/dialog/logs-dialog.tsx:36
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:88 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:87
msgid "Copy to clipboard" msgid "Copy to clipboard"
msgstr "" msgstr ""
@ -706,7 +706,7 @@ msgstr ""
msgid "Custom Resources" msgid "Custom Resources"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:116 #: src/renderer/components/+add-cluster/add-cluster.tsx:121
msgid "Custom.." msgid "Custom.."
msgstr "" msgstr ""
@ -807,7 +807,7 @@ msgstr ""
msgid "Download Mirror" msgid "Download Mirror"
msgstr "" msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:91 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:90
msgid "Download file" msgid "Download file"
msgstr "" msgstr ""
@ -1188,7 +1188,7 @@ msgstr ""
msgid "Kubeconfig" msgid "Kubeconfig"
msgstr "" msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:85 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:84
msgid "Kubeconfig File" msgid "Kubeconfig File"
msgstr "" msgstr ""
@ -1714,7 +1714,11 @@ msgid "Persistent Volumes"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:63 #: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Please select kubeconfig" #~ msgid "Please select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
msgid "Please select kubeconfig context"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-menu.tsx:50 #: src/renderer/components/+workloads-pods/pod-menu.tsx:50
@ -1811,7 +1815,7 @@ msgstr ""
msgid "Proxy is used only for non-cluster communication." msgid "Proxy is used only for non-cluster communication."
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:176 #: src/renderer/components/+add-cluster/add-cluster.tsx:188
msgid "Proxy settings" msgid "Proxy settings"
msgstr "" msgstr ""
@ -2195,13 +2199,21 @@ msgstr ""
msgid "Secrets" msgid "Secrets"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:185
msgid "Select a context"
msgstr ""
#: src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx:134 #: src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx:134
msgid "Select a quota.." msgid "Select a quota.."
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:173 #: src/renderer/components/+add-cluster/add-cluster.tsx:173
msgid "Select kubeconfig" #~ msgid "Select context"
msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
#~ msgid "Select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:88 #: src/renderer/components/+preferences/preferences.tsx:88
#~ msgid "Select repository" #~ msgid "Select repository"
@ -2756,7 +2768,7 @@ msgstr ""
msgid "{0} unavailable" msgid "{0} unavailable"
msgstr "" msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:129 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:128
msgid "{accountName} kubeconfig" msgid "{accountName} kubeconfig"
msgstr "" msgstr ""

View File

@ -84,7 +84,7 @@ msgstr "Название аккаунта"
msgid "Active" msgid "Active"
msgstr "Активный" msgstr "Активный"
#: src/renderer/components/+add-cluster/add-cluster.tsx:171 #: src/renderer/components/+add-cluster/add-cluster.tsx:176
#: src/renderer/components/cluster-manager/clusters-menu.tsx:116 #: src/renderer/components/cluster-manager/clusters-menu.tsx:116
msgid "Add Cluster" msgid "Add Cluster"
msgstr "" msgstr ""
@ -109,7 +109,7 @@ msgstr "Добавить привязки к {name}"
#~ msgid "Add cluster" #~ msgid "Add cluster"
#~ msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:192 #: src/renderer/components/+add-cluster/add-cluster.tsx:204
msgid "Add cluster(s)" msgid "Add cluster(s)"
msgstr "" msgstr ""
@ -452,7 +452,7 @@ msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243 #: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243
#: src/renderer/components/dialog/logs-dialog.tsx:39 #: src/renderer/components/dialog/logs-dialog.tsx:39
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:94 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93
msgid "Close" msgid "Close"
msgstr "Закрыть" msgstr "Закрыть"
@ -523,7 +523,7 @@ msgstr "Состояния"
msgid "Config Maps" msgid "Config Maps"
msgstr "" msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:55 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:54
msgid "Config copied to clipboard" msgid "Config copied to clipboard"
msgstr "Конфигурация скопирована в буфер" msgstr "Конфигурация скопирована в буфер"
@ -595,7 +595,7 @@ msgid "Conversion"
msgstr "" msgstr ""
#: src/renderer/components/dialog/logs-dialog.tsx:36 #: src/renderer/components/dialog/logs-dialog.tsx:36
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:88 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:87
msgid "Copy to clipboard" msgid "Copy to clipboard"
msgstr "Копировать" msgstr "Копировать"
@ -711,7 +711,7 @@ msgstr "Текущие фильтры:"
msgid "Custom Resources" msgid "Custom Resources"
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:116 #: src/renderer/components/+add-cluster/add-cluster.tsx:121
msgid "Custom.." msgid "Custom.."
msgstr "" msgstr ""
@ -812,7 +812,7 @@ msgstr "Домены"
msgid "Download Mirror" msgid "Download Mirror"
msgstr "" msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:91 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:90
msgid "Download file" msgid "Download file"
msgstr "Скачать файл" msgstr "Скачать файл"
@ -1198,7 +1198,7 @@ msgstr "Тип"
msgid "Kubeconfig" msgid "Kubeconfig"
msgstr "Файл конфигурации" msgstr "Файл конфигурации"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:85 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:84
msgid "Kubeconfig File" msgid "Kubeconfig File"
msgstr "Файл конфигурации" msgstr "Файл конфигурации"
@ -1732,7 +1732,11 @@ msgid "Persistent Volumes"
msgstr "Persistent Volumes" msgstr "Persistent Volumes"
#: src/renderer/components/+add-cluster/add-cluster.tsx:63 #: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Please select kubeconfig" #~ msgid "Please select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
msgid "Please select kubeconfig context"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-pods/pod-menu.tsx:50 #: src/renderer/components/+workloads-pods/pod-menu.tsx:50
@ -1829,7 +1833,7 @@ msgstr "Комиссия"
msgid "Proxy is used only for non-cluster communication." msgid "Proxy is used only for non-cluster communication."
msgstr "" msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:176 #: src/renderer/components/+add-cluster/add-cluster.tsx:188
msgid "Proxy settings" msgid "Proxy settings"
msgstr "" msgstr ""
@ -2213,13 +2217,21 @@ msgstr "Тип секрета"
msgid "Secrets" msgid "Secrets"
msgstr "Secrets" msgstr "Secrets"
#: src/renderer/components/+add-cluster/add-cluster.tsx:185
msgid "Select a context"
msgstr ""
#: src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx:134 #: src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx:134
msgid "Select a quota.." msgid "Select a quota.."
msgstr "Выберите квоту..." msgstr "Выберите квоту..."
#: src/renderer/components/+add-cluster/add-cluster.tsx:173 #: src/renderer/components/+add-cluster/add-cluster.tsx:173
msgid "Select kubeconfig" #~ msgid "Select context"
msgstr "" #~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
#~ msgid "Select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:88 #: src/renderer/components/+preferences/preferences.tsx:88
#~ msgid "Select repository" #~ msgid "Select repository"
@ -2774,7 +2786,7 @@ msgstr "{0} всего, {1} доступно"
msgid "{0} unavailable" msgid "{0} unavailable"
msgstr "{0} недоступно" msgstr "{0} недоступно"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:129 #: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:128
msgid "{accountName} kubeconfig" msgid "{accountName} kubeconfig"
msgstr "{accountName} конфигурация" msgstr "{accountName} конфигурация"

View File

@ -1,11 +1,13 @@
import { app, remote } from "electron"; import { app, remote } from "electron";
import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node" import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node"
import fse, { ensureDirSync, readFile, writeFileSync } from "fs-extra"; import fse, { ensureDirSync, writeFileSync } from "fs-extra";
import path from "path" import path from "path"
import os from "os" import os from "os"
import yaml from "js-yaml" import yaml from "js-yaml"
import logger from "../main/logger"; import logger from "../main/logger";
export const kubeConfigDefaultPath = path.join(os.homedir(), '.kube', 'config');
function resolveTilde(filePath: string) { function resolveTilde(filePath: string) {
if (filePath[0] === "~" && (filePath[1] === "/" || filePath.length === 1)) { if (filePath[0] === "~" && (filePath[1] === "/" || filePath.length === 1)) {
return filePath.replace("~", os.homedir()); return filePath.replace("~", os.homedir());
@ -150,18 +152,3 @@ export function saveConfigToAppFiles(clusterId: string, kubeConfig: KubeConfig |
writeFileSync(kubeConfigFile, kubeConfigContents); writeFileSync(kubeConfigFile, kubeConfigContents);
return kubeConfigFile; return kubeConfigFile;
} }
export async function getKubeConfigLocal(): Promise<string> {
try {
const configFile = path.join(os.homedir(), '.kube', 'config');
const file = await readFile(configFile, "utf8");
const obj = yaml.safeLoad(file);
if (obj.contexts) {
obj.contexts = obj.contexts.filter((ctx: any) => ctx?.context?.cluster && ctx?.name)
}
return yaml.safeDump(obj);
} catch (err) {
logger.debug(`Cannot read local kube-config: ${err}`)
return "";
}
}

View File

@ -1,13 +1,16 @@
import type { ThemeId } from "../renderer/theme.store"; import type { ThemeId } from "../renderer/theme.store";
import semver from "semver" import semver from "semver"
import { readFile } from "fs-extra"
import { action, observable, reaction, toJS } from "mobx"; import { action, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import migrations from "../migrations/user-store" import migrations from "../migrations/user-store"
import { getAppVersion } from "./utils/app-version"; import { getAppVersion } from "./utils/app-version";
import { getKubeConfigLocal, loadConfig } from "./kube-helpers"; import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
import { tracker } from "./tracker"; import { tracker } from "./tracker";
import logger from "../main/logger";
export interface UserStoreModel { export interface UserStoreModel {
kubeConfigPath: string;
lastSeenAppVersion: string; lastSeenAppVersion: string;
seenContexts: string[]; seenContexts: string[];
preferences: UserPreferences; preferences: UserPreferences;
@ -37,10 +40,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
// refresh new contexts // refresh new contexts
this.whenLoaded.then(this.refreshNewContexts); this.whenLoaded.then(this.refreshNewContexts);
reaction(() => this.seenContexts.size, this.refreshNewContexts); reaction(() => this.kubeConfigPath, this.refreshNewContexts);
} }
@observable lastSeenAppVersion = "0.0.0" @observable lastSeenAppVersion = "0.0.0"
@observable kubeConfigPath = kubeConfigDefaultPath; // used in add-cluster page for providing context
@observable seenContexts = observable.set<string>(); @observable seenContexts = observable.set<string>();
@observable newContexts = observable.set<string>(); @observable newContexts = observable.set<string>();
@ -55,6 +59,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
return semver.gt(getAppVersion(), this.lastSeenAppVersion); return semver.gt(getAppVersion(), this.lastSeenAppVersion);
} }
@action
resetKubeConfigPath() {
this.kubeConfigPath = kubeConfigDefaultPath;
}
@action @action
resetTheme() { resetTheme() {
this.preferences.colorTheme = UserStore.defaultTheme; this.preferences.colorTheme = UserStore.defaultTheme;
@ -67,14 +76,18 @@ export class UserStore extends BaseStore<UserStoreModel> {
} }
protected refreshNewContexts = async () => { protected refreshNewContexts = async () => {
const kubeConfig = await getKubeConfigLocal(); try {
if (kubeConfig) { const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
this.newContexts.clear(); if (kubeConfig) {
const localContexts = loadConfig(kubeConfig).getContexts(); this.newContexts.clear();
localContexts loadConfig(kubeConfig).getContexts()
.filter(ctx => ctx.cluster) .filter(ctx => ctx.cluster)
.filter(ctx => !this.seenContexts.has(ctx.name)) .filter(ctx => !this.seenContexts.has(ctx.name))
.forEach(ctx => this.newContexts.add(ctx.name)); .forEach(ctx => this.newContexts.add(ctx.name));
}
} catch (err) {
logger.error(err);
this.resetKubeConfigPath();
} }
} }
@ -86,17 +99,21 @@ export class UserStore extends BaseStore<UserStoreModel> {
} }
@action @action
protected fromStore(data: Partial<UserStoreModel> = {}) { protected async fromStore(data: Partial<UserStoreModel> = {}) {
const { lastSeenAppVersion, seenContexts = [], preferences } = data const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data
if (lastSeenAppVersion) { if (lastSeenAppVersion) {
this.lastSeenAppVersion = lastSeenAppVersion; this.lastSeenAppVersion = lastSeenAppVersion;
} }
if (kubeConfigPath) {
this.kubeConfigPath = kubeConfigPath;
}
this.seenContexts.replace(seenContexts); this.seenContexts.replace(seenContexts);
Object.assign(this.preferences, preferences); Object.assign(this.preferences, preferences);
} }
toJSON(): UserStoreModel { toJSON(): UserStoreModel {
const model: UserStoreModel = { const model: UserStoreModel = {
kubeConfigPath: this.kubeConfigPath,
lastSeenAppVersion: this.lastSeenAppVersion, lastSeenAppVersion: this.lastSeenAppVersion,
seenContexts: Array.from(this.seenContexts), seenContexts: Array.from(this.seenContexts),
preferences: this.preferences, preferences: this.preferences,

View File

@ -1,10 +1,34 @@
.AddCluster { .AddCluster {
.Select { .Select {
.kube-context {
--flex-gap: #{$padding};
}
&__control { &__control {
box-shadow: 0 0 0 1px $borderFaintColor; box-shadow: 0 0 0 1px $borderFaintColor;
} }
} }
.kube-config-select {
border: 1px solid $borderColor;
padding: $padding / 2 $padding;
border-radius: $radius;
transition: border 200ms ease-in-out;
> .title {
white-space: nowrap;
}
&:hover {
border-color: $primary;
cursor: pointer;
}
code {
word-break: break-all;
}
}
code { code {
color: $pink-400; color: $pink-400;
} }

View File

@ -3,27 +3,28 @@ import React, { Fragment } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { computed, observable } from "mobx"; import { computed, observable } from "mobx";
import { KubeConfig } from "@kubernetes/client-node"; import { KubeConfig } from "@kubernetes/client-node";
import { t, Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { Input } from "../input"; import { FileInput, Input } from "../input";
import { AceEditor } from "../ace-editor"; import { AceEditor } from "../ace-editor";
import { Button } from "../button"; import { Button } from "../button";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { WizardLayout } from "../layout/wizard-layout"; import { WizardLayout } from "../layout/wizard-layout";
import { getKubeConfigLocal, loadConfig, saveConfigToAppFiles, splitConfig, validateConfig } from "../../../common/kube-helpers"; import { kubeConfigDefaultPath, loadConfig, saveConfigToAppFiles, splitConfig, validateConfig } from "../../../common/kube-helpers";
import { clusterStore } from "../../../common/cluster-store"; import { clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store"; import { workspaceStore } from "../../../common/workspace-store";
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { userStore } from "../../../common/user-store"; import { userStore } from "../../../common/user-store";
import { clusterViewURL } from "../cluster-manager/cluster-view.route"; import { clusterViewURL } from "../cluster-manager/cluster-view.route";
import { cssNames } from "../../utils";
import { Notifications } from "../notifications";
@observer @observer
export class AddCluster extends React.Component { export class AddCluster extends React.Component {
readonly custom: any = "custom" readonly custom: any = "custom"
@observable.ref clusterConfig: KubeConfig; @observable.ref localKubeConfig: KubeConfig;
@observable.ref kubeConfig: KubeConfig; // local ~/.kube/config (if available) @observable.ref newClusterConfig: KubeConfig;
@observable.ref error: React.ReactNode; @observable.ref error: React.ReactNode;
@observable isWaiting = false @observable isWaiting = false
@ -31,25 +32,36 @@ export class AddCluster extends React.Component {
@observable proxyServer = "" @observable proxyServer = ""
@observable customConfig = "" @observable customConfig = ""
async componentDidMount() { componentDidMount() {
const kubeConfig: string = await getKubeConfigLocal() this.setLocalConfigPath(userStore.kubeConfigPath);
if (kubeConfig) {
this.kubeConfig = loadConfig(kubeConfig)
}
} }
componentWillUnmount() { componentWillUnmount() {
userStore.markNewContextsAsSeen(); userStore.markNewContextsAsSeen();
} }
protected setLocalConfigPath(filePath: string) {
try {
const kubeConfig = loadConfig(filePath);
validateConfig(kubeConfig);
this.localKubeConfig = kubeConfig;
this.newClusterConfig = null; // reset previously selected
userStore.kubeConfigPath = filePath; // save to store
} catch (err) {
Notifications.error(
<p>Can't read config file in <em>{filePath}</em>: {String(err)}</p>
);
}
}
@computed get isCustom() { @computed get isCustom() {
return this.clusterConfig === this.custom; return this.newClusterConfig === this.custom;
} }
@computed get clusterOptions() { @computed get clusterOptions() {
const options: SelectOption<KubeConfig>[] = []; const options: SelectOption<KubeConfig>[] = [];
if (this.kubeConfig) { if (this.localKubeConfig) {
splitConfig(this.kubeConfig).forEach(kubeConfig => { splitConfig(this.localKubeConfig).forEach(kubeConfig => {
const context = kubeConfig.currentContext; const context = kubeConfig.currentContext;
const hasContext = clusterStore.hasContext(context); const hasContext = clusterStore.hasContext(context);
if (!hasContext) { if (!hasContext) {
@ -71,9 +83,8 @@ export class AddCluster extends React.Component {
if (value instanceof KubeConfig) { if (value instanceof KubeConfig) {
const context = value.currentContext; const context = value.currentContext;
const isNew = userStore.newContexts.has(context); const isNew = userStore.newContexts.has(context);
const className = `${context} kube-context flex gaps align-center`
return ( return (
<div className={className}> <div className={cssNames("kube-context flex gaps align-center", context)}>
<span>{context}</span> <span>{context}</span>
{isNew && <Icon material="fiber_new"/>} {isNew && <Icon material="fiber_new"/>}
</div> </div>
@ -83,14 +94,14 @@ export class AddCluster extends React.Component {
}; };
addCluster = async () => { addCluster = async () => {
const { clusterConfig, customConfig, proxyServer } = this; const { newClusterConfig, customConfig, proxyServer } = this;
const clusterId = uuid(); const clusterId = uuid();
this.isWaiting = true this.isWaiting = true
this.error = "" this.error = ""
try { try {
const config = this.isCustom ? loadConfig(customConfig) : clusterConfig; const config = this.isCustom ? loadConfig(customConfig) : newClusterConfig;
if (!config) { if (!config) {
this.error = <Trans>Please select kubeconfig</Trans> this.error = <Trans>Please select kube-config's context</Trans>
return; return;
} }
validateConfig(config); validateConfig(config);
@ -167,12 +178,31 @@ export class AddCluster extends React.Component {
return ( return (
<WizardLayout className="AddCluster" infoPanel={this.renderInfo()}> <WizardLayout className="AddCluster" infoPanel={this.renderInfo()}>
<h2><Trans>Add Cluster</Trans></h2> <h2><Trans>Add Cluster</Trans></h2>
<p>Choose config:</p> <div className="flex gaps align-center">
<label
htmlFor="kube-config-select"
className="kube-config-select flex gaps align-center box grow"
>
<span className="title">Config file</span>
<code>{userStore.kubeConfigPath}</code>
</label>
{kubeConfigDefaultPath !== userStore.kubeConfigPath && (
<Icon
material="settings_backup_restore"
onClick={() => this.setLocalConfigPath(kubeConfigDefaultPath)}
tooltip="Reset to defaults"
/>
)}
</div>
<FileInput
id="kube-config-select"
onSelectFiles={({ file }) => this.setLocalConfigPath(file.path)}
/>
<Select <Select
placeholder={<Trans>Select kubeconfig</Trans>} placeholder={<Trans>Select a context</Trans>}
value={this.clusterConfig} value={this.newClusterConfig}
options={this.clusterOptions} options={this.clusterOptions}
onChange={({ value }: SelectOption) => this.clusterConfig = value} onChange={({ value }: SelectOption) => this.newClusterConfig = value}
formatOptionLabel={this.formatClusterContextLabel} formatOptionLabel={this.formatClusterContextLabel}
id="kubecontext-select" id="kubecontext-select"
/> />

View File

@ -0,0 +1,66 @@
import React, { InputHTMLAttributes } from "react";
export interface FileInputSelection<T = string> {
file: File;
data?: T | any; // not available when readAsTexts={false}
error?: string;
}
interface Props extends InputHTMLAttributes<any> {
id?: string; // could be used with <label htmlFor={id}/> to open filesystem dialog
accept?: string; // allowed file types to select, e.g. "application/json"
readAsText?: boolean; // provide files content as text in selection-callback
multiple?: boolean;
onSelectFiles(...selectedFiles: FileInputSelection[]): void;
}
export class FileInput extends React.Component<Props> {
protected input: HTMLInputElement;
protected style: React.CSSProperties = {
position: "absolute",
display: "none",
};
selectFiles = () => {
this.input.click(); // opens system dialog for selecting files
}
protected onChange = async (evt: React.ChangeEvent<HTMLInputElement>) => {
const fileList = Array.from(evt.target.files);
if (!fileList.length) {
return;
}
let selectedFiles: FileInputSelection[] = fileList.map(file => ({ file }));
if (this.props.readAsText) {
const readingFiles: Promise<FileInputSelection>[] = fileList.map(file => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => {
resolve({
file: file,
data: reader.result,
error: reader.error ? String(reader.error) : null,
})
};
reader.readAsText(file);
})
});
selectedFiles = await Promise.all(readingFiles);
}
this.props.onSelectFiles(...selectedFiles);
}
render() {
const { onSelectFiles, readAsText, ...props } = this.props;
return (
<input
type="file"
style={this.style}
onChange={this.onChange}
ref={e => this.input = e}
{...props}
/>
)
}
}

View File

@ -1,2 +1,3 @@
export * from './input' export * from './input'
export * from './search-input' export * from './search-input'
export * from './file-input'