mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Allow user to select Kubeconfig from filesystem (#740)
* Allow user to select Kubeconfig from filesystem, fix #738 Signed-off-by: Roman <ixrock@gmail.com> Co-authored-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
24 files changed
+1113
-263
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import type { WorkspaceId } from "./workspace-store";
|
||||
import { ipcRenderer } from "electron";
|
||||
import path from "path";
|
||||
import { app, ipcRenderer, remote } from "electron";
|
||||
import { unlink } from "fs-extra";
|
||||
import { action, computed, observable, toJS } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
@@ -7,6 +8,9 @@ import { Cluster, ClusterState } from "../main/cluster";
|
||||
import migrations from "../migrations/cluster-store"
|
||||
import logger from "../main/logger";
|
||||
import { tracker } from "./tracker";
|
||||
import { dumpConfigYaml } from "./kube-helpers";
|
||||
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
|
||||
export interface ClusterIconUpload {
|
||||
clusterId: string;
|
||||
@@ -49,6 +53,17 @@ export interface ClusterPreferences {
|
||||
}
|
||||
|
||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
static getCustomKubeConfigPath(clusterId: ClusterId): string {
|
||||
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs", clusterId);
|
||||
}
|
||||
|
||||
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
|
||||
const filePath = ClusterStore.getCustomKubeConfigPath(clusterId);
|
||||
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
|
||||
saveToAppFiles(filePath, fileContents);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
super({
|
||||
configName: "lens-cluster-store",
|
||||
@@ -81,6 +96,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
return this.activeClusterId === id;
|
||||
}
|
||||
|
||||
@action
|
||||
setActive(id: ClusterId) {
|
||||
this.activeClusterId = id;
|
||||
}
|
||||
@@ -102,12 +118,12 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
|
||||
@action
|
||||
async addCluster(model: ClusterModel, activate = true): Promise<Cluster> {
|
||||
tracker.event("cluster", "add");
|
||||
const cluster = new Cluster(model);
|
||||
this.clusters.set(model.id, cluster);
|
||||
if (activate) this.activeClusterId = model.id;
|
||||
return cluster;
|
||||
addCluster(...models: ClusterModel[]) {
|
||||
models.forEach(model => {
|
||||
tracker.event("cluster", "add");
|
||||
const cluster = new Cluster(model);
|
||||
this.clusters.set(model.id, cluster);
|
||||
})
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -119,7 +135,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
if (this.activeClusterId === clusterId) {
|
||||
this.activeClusterId = null;
|
||||
}
|
||||
unlink(cluster.kubeConfigPath).catch(() => null);
|
||||
// remove only custom kubeconfigs (pasted as text)
|
||||
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
|
||||
unlink(cluster.kubeConfigPath).catch(() => null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import yaml from "js-yaml";
|
||||
import { Cluster } from "../main/cluster";
|
||||
import { ClusterStore } from "./cluster-store";
|
||||
import { workspaceStore } from "./workspace-store";
|
||||
import { saveConfigToAppFiles } from "./kube-helpers";
|
||||
|
||||
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png")
|
||||
|
||||
@@ -37,7 +36,7 @@ describe("empty config", () => {
|
||||
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
|
||||
clusterName: "minikube"
|
||||
},
|
||||
kubeConfigPath: saveConfigToAppFiles("foo", "fancy foo config"),
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", "fancy foo config"),
|
||||
workspace: workspaceStore.currentWorkspaceId
|
||||
});
|
||||
clusterStore.addCluster(cluster);
|
||||
@@ -58,7 +57,7 @@ describe("empty config", () => {
|
||||
preferences: {
|
||||
clusterName: "prod"
|
||||
},
|
||||
kubeConfigPath: saveConfigToAppFiles("prod", "fancy config"),
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", "fancy config"),
|
||||
workspace: "workstation"
|
||||
});
|
||||
const devCluster = new Cluster({
|
||||
@@ -66,7 +65,7 @@ describe("empty config", () => {
|
||||
preferences: {
|
||||
clusterName: "dev"
|
||||
},
|
||||
kubeConfigPath: saveConfigToAppFiles("dev", "fancy config"),
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", "fancy config"),
|
||||
workspace: "workstation"
|
||||
});
|
||||
clusterStore.addCluster(prodCluster);
|
||||
@@ -84,17 +83,13 @@ describe("empty config", () => {
|
||||
expect(wsClusters[1].id).toBe("dev");
|
||||
})
|
||||
|
||||
it("checks if last added cluster becomes active", () => {
|
||||
expect(clusterStore.activeCluster.id).toBe("dev");
|
||||
})
|
||||
|
||||
it("sets active cluster", () => {
|
||||
clusterStore.setActive("foo");
|
||||
expect(clusterStore.activeCluster.id).toBe("foo");
|
||||
})
|
||||
|
||||
it("check if cluster's kubeconfig file saved", () => {
|
||||
const file = saveConfigToAppFiles("boo", "kubeconfig");
|
||||
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
|
||||
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { app, remote } from "electron";
|
||||
import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node"
|
||||
import fse, { ensureDirSync, readFile, writeFileSync } from "fs-extra";
|
||||
import fse from "fs-extra";
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import yaml from "js-yaml"
|
||||
import logger from "../main/logger";
|
||||
|
||||
export const kubeConfigDefaultPath = path.join(os.homedir(), '.kube', 'config');
|
||||
|
||||
function resolveTilde(filePath: string) {
|
||||
if (filePath[0] === "~" && (filePath[1] === "/" || filePath.length === 1)) {
|
||||
return filePath.replace("~", os.homedir());
|
||||
@@ -139,29 +140,3 @@ export function getNodeWarningConditions(node: V1Node) {
|
||||
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
|
||||
)
|
||||
}
|
||||
|
||||
// Write kubeconfigs to "embedded" store, i.e. "/Users/ixrock/Library/Application Support/Lens/kubeconfigs"
|
||||
export function saveConfigToAppFiles(clusterId: string, kubeConfig: KubeConfig | string): string {
|
||||
const userData = (app || remote.app).getPath("userData");
|
||||
const kubeConfigFile = path.join(userData, `kubeconfigs/${clusterId}`)
|
||||
const kubeConfigContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
|
||||
|
||||
ensureDirSync(path.dirname(kubeConfigFile));
|
||||
writeFileSync(kubeConfigFile, kubeConfigContents);
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
+29
-12
@@ -1,13 +1,16 @@
|
||||
import type { ThemeId } from "../renderer/theme.store";
|
||||
import semver from "semver"
|
||||
import { readFile } from "fs-extra"
|
||||
import { action, observable, reaction, toJS } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import migrations from "../migrations/user-store"
|
||||
import { getAppVersion } from "./utils/app-version";
|
||||
import { getKubeConfigLocal, loadConfig } from "./kube-helpers";
|
||||
import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
|
||||
import { tracker } from "./tracker";
|
||||
import logger from "../main/logger";
|
||||
|
||||
export interface UserStoreModel {
|
||||
kubeConfigPath: string;
|
||||
lastSeenAppVersion: string;
|
||||
seenContexts: string[];
|
||||
preferences: UserPreferences;
|
||||
@@ -37,10 +40,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
|
||||
// refresh new contexts
|
||||
this.whenLoaded.then(this.refreshNewContexts);
|
||||
reaction(() => this.seenContexts.size, this.refreshNewContexts);
|
||||
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
|
||||
}
|
||||
|
||||
@observable lastSeenAppVersion = "0.0.0"
|
||||
@observable kubeConfigPath = kubeConfigDefaultPath; // used in add-cluster page for providing context
|
||||
@observable seenContexts = observable.set<string>();
|
||||
@observable newContexts = observable.set<string>();
|
||||
|
||||
@@ -55,6 +59,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
||||
}
|
||||
|
||||
@action
|
||||
resetKubeConfigPath() {
|
||||
this.kubeConfigPath = kubeConfigDefaultPath;
|
||||
}
|
||||
|
||||
@action
|
||||
resetTheme() {
|
||||
this.preferences.colorTheme = UserStore.defaultTheme;
|
||||
@@ -67,14 +76,18 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
}
|
||||
|
||||
protected refreshNewContexts = async () => {
|
||||
const kubeConfig = await getKubeConfigLocal();
|
||||
if (kubeConfig) {
|
||||
this.newContexts.clear();
|
||||
const localContexts = loadConfig(kubeConfig).getContexts();
|
||||
localContexts
|
||||
.filter(ctx => ctx.cluster)
|
||||
.filter(ctx => !this.seenContexts.has(ctx.name))
|
||||
.forEach(ctx => this.newContexts.add(ctx.name));
|
||||
try {
|
||||
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
|
||||
if (kubeConfig) {
|
||||
this.newContexts.clear();
|
||||
loadConfig(kubeConfig).getContexts()
|
||||
.filter(ctx => ctx.cluster)
|
||||
.filter(ctx => !this.seenContexts.has(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
|
||||
protected fromStore(data: Partial<UserStoreModel> = {}) {
|
||||
const { lastSeenAppVersion, seenContexts = [], preferences } = data
|
||||
protected async fromStore(data: Partial<UserStoreModel> = {}) {
|
||||
const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data
|
||||
if (lastSeenAppVersion) {
|
||||
this.lastSeenAppVersion = lastSeenAppVersion;
|
||||
}
|
||||
if (kubeConfigPath) {
|
||||
this.kubeConfigPath = kubeConfigPath;
|
||||
}
|
||||
this.seenContexts.replace(seenContexts);
|
||||
Object.assign(this.preferences, preferences);
|
||||
}
|
||||
|
||||
toJSON(): UserStoreModel {
|
||||
const model: UserStoreModel = {
|
||||
kubeConfigPath: this.kubeConfigPath,
|
||||
lastSeenAppVersion: this.lastSeenAppVersion,
|
||||
seenContexts: Array.from(this.seenContexts),
|
||||
preferences: this.preferences,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Save file to electron app directory (e.g. "/Users/$USER/Library/Application Support/Lens" for MacOS)
|
||||
import path from "path";
|
||||
import { app, remote } from "electron";
|
||||
import { ensureDirSync, writeFileSync } from "fs-extra";
|
||||
|
||||
export function saveToAppFiles(filePath: string, contents: any): string {
|
||||
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);
|
||||
ensureDirSync(path.dirname(absPath));
|
||||
writeFileSync(absPath, contents);
|
||||
return absPath;
|
||||
}
|
||||
Reference in new issue
Block a user