mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Kubeconfigs as file references (#466)
Signed-off-by: Jussi Nummelin <jussi.nummelin@gmail.com> Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com> Co-authored-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
parent
c15aa7972c
commit
55687b7d35
@ -6,6 +6,7 @@ import * as version260Beta2 from "../migrations/cluster-store/2.6.0-beta.2"
|
|||||||
import * as version260Beta3 from "../migrations/cluster-store/2.6.0-beta.3"
|
import * as version260Beta3 from "../migrations/cluster-store/2.6.0-beta.3"
|
||||||
import * as version270Beta0 from "../migrations/cluster-store/2.7.0-beta.0"
|
import * as version270Beta0 from "../migrations/cluster-store/2.7.0-beta.0"
|
||||||
import * as version270Beta1 from "../migrations/cluster-store/2.7.0-beta.1"
|
import * as version270Beta1 from "../migrations/cluster-store/2.7.0-beta.1"
|
||||||
|
import * as version360Beta1 from "../migrations/cluster-store/3.6.0-beta.1"
|
||||||
import { getAppVersion } from "./utils/app-version";
|
import { getAppVersion } from "./utils/app-version";
|
||||||
|
|
||||||
export class ClusterStore {
|
export class ClusterStore {
|
||||||
@ -25,7 +26,8 @@ export class ClusterStore {
|
|||||||
"2.6.0-beta.2": version260Beta2.migration,
|
"2.6.0-beta.2": version260Beta2.migration,
|
||||||
"2.6.0-beta.3": version260Beta3.migration,
|
"2.6.0-beta.3": version260Beta3.migration,
|
||||||
"2.7.0-beta.0": version270Beta0.migration,
|
"2.7.0-beta.0": version270Beta0.migration,
|
||||||
"2.7.0-beta.1": version270Beta1.migration
|
"2.7.0-beta.1": version270Beta1.migration,
|
||||||
|
"3.6.0-beta.1": version360Beta1.migration
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -72,7 +74,8 @@ export class ClusterStore {
|
|||||||
const index = clusters.findIndex((cl) => cl.id === cluster.id)
|
const index = clusters.findIndex((cl) => cl.id === cluster.id)
|
||||||
const storable = {
|
const storable = {
|
||||||
id: cluster.id,
|
id: cluster.id,
|
||||||
kubeConfig: cluster.kubeConfig,
|
kubeConfigPath: cluster.kubeConfigPath,
|
||||||
|
contextName: cluster.contextName,
|
||||||
preferences: cluster.preferences,
|
preferences: cluster.preferences,
|
||||||
workspace: cluster.workspace
|
workspace: cluster.workspace
|
||||||
}
|
}
|
||||||
@ -95,7 +98,8 @@ export class ClusterStore {
|
|||||||
public reloadCluster(cluster: ClusterBaseInfo): void {
|
public reloadCluster(cluster: ClusterBaseInfo): void {
|
||||||
const storedCluster = this.getCluster(cluster.id);
|
const storedCluster = this.getCluster(cluster.id);
|
||||||
if (storedCluster) {
|
if (storedCluster) {
|
||||||
cluster.kubeConfig = storedCluster.kubeConfig
|
cluster.kubeConfigPath = storedCluster.kubeConfigPath
|
||||||
|
cluster.contextName = storedCluster.contextName
|
||||||
cluster.preferences = storedCluster.preferences
|
cluster.preferences = storedCluster.preferences
|
||||||
cluster.workspace = storedCluster.workspace
|
cluster.workspace = storedCluster.workspace
|
||||||
}
|
}
|
||||||
@ -113,4 +117,4 @@ export class ClusterStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clusterStore = ClusterStore.getInstance();
|
export const clusterStore: ClusterStore = ClusterStore.getInstance();
|
||||||
|
|||||||
@ -1,8 +1,19 @@
|
|||||||
import mockFs from "mock-fs"
|
import mockFs from "mock-fs"
|
||||||
import yaml from "js-yaml"
|
import yaml from "js-yaml"
|
||||||
|
import * as fs from "fs"
|
||||||
import { ClusterStore } from "./cluster-store";
|
import { ClusterStore } from "./cluster-store";
|
||||||
import { Cluster } from "../main/cluster";
|
import { Cluster } from "../main/cluster";
|
||||||
|
|
||||||
|
jest.mock("electron", () => {
|
||||||
|
return {
|
||||||
|
app: {
|
||||||
|
getVersion: () => '99.99.99',
|
||||||
|
getPath: () => 'tmp',
|
||||||
|
getLocale: () => 'en'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234
|
// Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234
|
||||||
console.log("");
|
console.log("");
|
||||||
|
|
||||||
@ -24,7 +35,8 @@ describe("for an empty config", () => {
|
|||||||
it("allows to store and retrieve a cluster", async () => {
|
it("allows to store and retrieve a cluster", async () => {
|
||||||
const cluster = new Cluster({
|
const cluster = new Cluster({
|
||||||
id: 'foo',
|
id: 'foo',
|
||||||
kubeConfig: 'kubeconfig string',
|
kubeConfigPath: 'kubeconfig',
|
||||||
|
contextName: "foo",
|
||||||
preferences: {
|
preferences: {
|
||||||
terminalCWD: '/tmp',
|
terminalCWD: '/tmp',
|
||||||
icon: 'path to icon'
|
icon: 'path to icon'
|
||||||
@ -33,7 +45,8 @@ describe("for an empty config", () => {
|
|||||||
const clusterStore = ClusterStore.getInstance()
|
const clusterStore = ClusterStore.getInstance()
|
||||||
clusterStore.storeCluster(cluster);
|
clusterStore.storeCluster(cluster);
|
||||||
const storedCluster = clusterStore.getCluster(cluster.id);
|
const storedCluster = clusterStore.getCluster(cluster.id);
|
||||||
expect(storedCluster.kubeConfig).toBe(cluster.kubeConfig)
|
expect(storedCluster.kubeConfigPath).toBe(cluster.kubeConfigPath)
|
||||||
|
expect(storedCluster.contextName).toBe(cluster.contextName)
|
||||||
expect(storedCluster.preferences.icon).toBe(cluster.preferences.icon)
|
expect(storedCluster.preferences.icon).toBe(cluster.preferences.icon)
|
||||||
expect(storedCluster.preferences.terminalCWD).toBe(cluster.preferences.terminalCWD)
|
expect(storedCluster.preferences.terminalCWD).toBe(cluster.preferences.terminalCWD)
|
||||||
expect(storedCluster.id).toBe(cluster.id)
|
expect(storedCluster.id).toBe(cluster.id)
|
||||||
@ -42,7 +55,8 @@ describe("for an empty config", () => {
|
|||||||
it("allows to delete a cluster", async () => {
|
it("allows to delete a cluster", async () => {
|
||||||
const cluster = new Cluster({
|
const cluster = new Cluster({
|
||||||
id: 'foofoo',
|
id: 'foofoo',
|
||||||
kubeConfig: 'kubeconfig string',
|
kubeConfigPath: 'kubeconfig',
|
||||||
|
contextName: "foo",
|
||||||
preferences: {
|
preferences: {
|
||||||
terminalCWD: '/tmp'
|
terminalCWD: '/tmp'
|
||||||
}
|
}
|
||||||
@ -74,12 +88,12 @@ describe("for a config with existing clusters", () => {
|
|||||||
clusters: [
|
clusters: [
|
||||||
{
|
{
|
||||||
id: 'cluster1',
|
id: 'cluster1',
|
||||||
kubeConfig: 'foo',
|
kubeConfigPath: 'foo',
|
||||||
preferences: { terminalCWD: '/foo' }
|
preferences: { terminalCWD: '/foo' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cluster2',
|
id: 'cluster2',
|
||||||
kubeConfig: 'foo2',
|
kubeConfigPath: 'foo2',
|
||||||
preferences: { terminalCWD: '/foo2' }
|
preferences: { terminalCWD: '/foo2' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -96,12 +110,12 @@ describe("for a config with existing clusters", () => {
|
|||||||
it("allows to retrieve a cluster", async () => {
|
it("allows to retrieve a cluster", async () => {
|
||||||
const clusterStore = ClusterStore.getInstance()
|
const clusterStore = ClusterStore.getInstance()
|
||||||
const storedCluster = clusterStore.getCluster('cluster1')
|
const storedCluster = clusterStore.getCluster('cluster1')
|
||||||
expect(storedCluster.kubeConfig).toBe('foo')
|
expect(storedCluster.kubeConfigPath).toBe('foo')
|
||||||
expect(storedCluster.preferences.terminalCWD).toBe('/foo')
|
expect(storedCluster.preferences.terminalCWD).toBe('/foo')
|
||||||
expect(storedCluster.id).toBe('cluster1')
|
expect(storedCluster.id).toBe('cluster1')
|
||||||
|
|
||||||
const storedCluster2 = clusterStore.getCluster('cluster2')
|
const storedCluster2 = clusterStore.getCluster('cluster2')
|
||||||
expect(storedCluster2.kubeConfig).toBe('foo2')
|
expect(storedCluster2.kubeConfigPath).toBe('foo2')
|
||||||
expect(storedCluster2.preferences.terminalCWD).toBe('/foo2')
|
expect(storedCluster2.preferences.terminalCWD).toBe('/foo2')
|
||||||
expect(storedCluster2.id).toBe('cluster2')
|
expect(storedCluster2.id).toBe('cluster2')
|
||||||
})
|
})
|
||||||
@ -122,7 +136,8 @@ describe("for a config with existing clusters", () => {
|
|||||||
it("allows to reload a cluster in-place", async () => {
|
it("allows to reload a cluster in-place", async () => {
|
||||||
const cluster = new Cluster({
|
const cluster = new Cluster({
|
||||||
id: 'cluster1',
|
id: 'cluster1',
|
||||||
kubeConfig: 'kubeconfig string',
|
kubeConfigPath: 'kubeconfig string',
|
||||||
|
contextName: "foo",
|
||||||
preferences: {
|
preferences: {
|
||||||
terminalCWD: '/tmp'
|
terminalCWD: '/tmp'
|
||||||
}
|
}
|
||||||
@ -131,7 +146,7 @@ describe("for a config with existing clusters", () => {
|
|||||||
const clusterStore = ClusterStore.getInstance()
|
const clusterStore = ClusterStore.getInstance()
|
||||||
clusterStore.reloadCluster(cluster)
|
clusterStore.reloadCluster(cluster)
|
||||||
|
|
||||||
expect(cluster.kubeConfig).toBe('foo')
|
expect(cluster.kubeConfigPath).toBe('foo')
|
||||||
expect(cluster.preferences.terminalCWD).toBe('/foo')
|
expect(cluster.preferences.terminalCWD).toBe('/foo')
|
||||||
expect(cluster.id).toBe('cluster1')
|
expect(cluster.id).toBe('cluster1')
|
||||||
})
|
})
|
||||||
@ -142,11 +157,11 @@ describe("for a config with existing clusters", () => {
|
|||||||
|
|
||||||
expect(storedClusters[0].id).toBe('cluster1')
|
expect(storedClusters[0].id).toBe('cluster1')
|
||||||
expect(storedClusters[0].preferences.terminalCWD).toBe('/foo')
|
expect(storedClusters[0].preferences.terminalCWD).toBe('/foo')
|
||||||
expect(storedClusters[0].kubeConfig).toBe('foo')
|
expect(storedClusters[0].kubeConfigPath).toBe('foo')
|
||||||
|
|
||||||
expect(storedClusters[1].id).toBe('cluster2')
|
expect(storedClusters[1].id).toBe('cluster2')
|
||||||
expect(storedClusters[1].preferences.terminalCWD).toBe('/foo2')
|
expect(storedClusters[1].preferences.terminalCWD).toBe('/foo2')
|
||||||
expect(storedClusters[1].kubeConfig).toBe('foo2')
|
expect(storedClusters[1].kubeConfigPath).toBe('foo2')
|
||||||
})
|
})
|
||||||
|
|
||||||
it("allows storing the clusters in a different order", async () => {
|
it("allows storing the clusters in a different order", async () => {
|
||||||
@ -187,7 +202,7 @@ describe("for a pre 2.0 config with an existing cluster", () => {
|
|||||||
it("migrates to modern format with kubeconfig under a key", async () => {
|
it("migrates to modern format with kubeconfig under a key", async () => {
|
||||||
const clusterStore = ClusterStore.getInstance()
|
const clusterStore = ClusterStore.getInstance()
|
||||||
const storedCluster = clusterStore.store.get('clusters')[0]
|
const storedCluster = clusterStore.store.get('clusters')[0]
|
||||||
expect(storedCluster.kubeConfig).toBe('kubeconfig content')
|
expect(storedCluster.kubeConfigPath).toBe(`tmp/kubeconfigs/${storedCluster.id}`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -254,9 +269,10 @@ describe("for a pre 2.6.0 config with a cluster that has arrays in auth config",
|
|||||||
it("replaces array format access token and expiry into string", async () => {
|
it("replaces array format access token and expiry into string", async () => {
|
||||||
const clusterStore = ClusterStore.getInstance()
|
const clusterStore = ClusterStore.getInstance()
|
||||||
const storedClusterData = clusterStore.store.get('clusters')[0]
|
const storedClusterData = clusterStore.store.get('clusters')[0]
|
||||||
const kc = yaml.safeLoad(storedClusterData.kubeConfig)
|
const kc = yaml.safeLoad(fs.readFileSync(storedClusterData.kubeConfigPath).toString())
|
||||||
expect(kc.users[0].user['auth-provider'].config['access-token']).toBe("should be string")
|
expect(kc.users[0].user['auth-provider'].config['access-token']).toBe("should be string")
|
||||||
expect(kc.users[0].user['auth-provider'].config['expiry']).toBe("should be string")
|
expect(kc.users[0].user['auth-provider'].config['expiry']).toBe("should be string")
|
||||||
|
expect(storedClusterData.contextName).toBe("minikube")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
16
src/common/utils/kubeconfig.ts
Normal file
16
src/common/utils/kubeconfig.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { app, remote } from "electron"
|
||||||
|
import { ensureDirSync, writeFileSync } from "fs-extra"
|
||||||
|
import * as path from "path"
|
||||||
|
|
||||||
|
// Writes kubeconfigs to "embedded" store, i.e. .../Lens/kubeconfigs/
|
||||||
|
export function writeEmbeddedKubeConfig(clusterId: string, kubeConfig: string): string {
|
||||||
|
// This can be called from main & renderer
|
||||||
|
const a = (app || remote.app)
|
||||||
|
const kubeConfigBase = path.join(a.getPath("userData"), "kubeconfigs")
|
||||||
|
ensureDirSync(kubeConfigBase)
|
||||||
|
|
||||||
|
const kubeConfigFile = path.join(kubeConfigBase, clusterId)
|
||||||
|
writeFileSync(kubeConfigFile, kubeConfig)
|
||||||
|
|
||||||
|
return kubeConfigFile
|
||||||
|
}
|
||||||
@ -53,7 +53,7 @@ export class MetricsFeature extends Feature {
|
|||||||
|
|
||||||
async install(cluster: Cluster): Promise<boolean> {
|
async install(cluster: Cluster): Promise<boolean> {
|
||||||
// Check if there are storageclasses
|
// Check if there are storageclasses
|
||||||
const storageClient = cluster.contextHandler.kc.makeApiClient(k8s.StorageV1Api)
|
const storageClient = cluster.proxyKubeconfig().makeApiClient(k8s.StorageV1Api)
|
||||||
const scs = await storageClient.listStorageClass();
|
const scs = await storageClient.listStorageClass();
|
||||||
scs.body.items.forEach(sc => {
|
scs.body.items.forEach(sc => {
|
||||||
if(sc.metadata.annotations &&
|
if(sc.metadata.annotations &&
|
||||||
@ -93,9 +93,9 @@ export class MetricsFeature extends Feature {
|
|||||||
|
|
||||||
async uninstall(cluster: Cluster): Promise<boolean> {
|
async uninstall(cluster: Cluster): Promise<boolean> {
|
||||||
return new Promise<boolean>(async (resolve, reject) => {
|
return new Promise<boolean>(async (resolve, reject) => {
|
||||||
const rbacClient = cluster.contextHandler.kc.makeApiClient(RbacAuthorizationV1Api)
|
const rbacClient = cluster.proxyKubeconfig().makeApiClient(RbacAuthorizationV1Api)
|
||||||
try {
|
try {
|
||||||
await this.deleteNamespace(cluster.contextHandler.kc, "lens-metrics")
|
await this.deleteNamespace(cluster.proxyKubeconfig(), "lens-metrics")
|
||||||
await rbacClient.deleteClusterRole("lens-prometheus");
|
await rbacClient.deleteClusterRole("lens-prometheus");
|
||||||
await rbacClient.deleteClusterRoleBinding("lens-prometheus");
|
await rbacClient.deleteClusterRoleBinding("lens-prometheus");
|
||||||
resolve(true);
|
resolve(true);
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export class UserModeFeature extends Feature {
|
|||||||
|
|
||||||
async uninstall(cluster: Cluster): Promise<boolean> {
|
async uninstall(cluster: Cluster): Promise<boolean> {
|
||||||
return new Promise<boolean>(async (resolve, reject) => {
|
return new Promise<boolean>(async (resolve, reject) => {
|
||||||
const rbacClient = cluster.contextHandler.kc.makeApiClient(RbacAuthorizationV1Api)
|
const rbacClient = cluster.proxyKubeconfig().makeApiClient(RbacAuthorizationV1Api)
|
||||||
try {
|
try {
|
||||||
await rbacClient.deleteClusterRole("lens-user");
|
await rbacClient.deleteClusterRole("lens-user");
|
||||||
await rbacClient.deleteClusterRoleBinding("lens-user");
|
await rbacClient.deleteClusterRoleBinding("lens-user");
|
||||||
|
|||||||
@ -44,12 +44,12 @@ export class ClusterManager {
|
|||||||
this.clusters = new Map()
|
this.clusters = new Map()
|
||||||
clusters.forEach((clusterInfo) => {
|
clusters.forEach((clusterInfo) => {
|
||||||
try {
|
try {
|
||||||
const kc = this.loadKubeConfig(clusterInfo.kubeConfig)
|
const kc = this.loadKubeConfig(clusterInfo.kubeConfigPath)
|
||||||
logger.debug(`Starting to load target definitions for ${ kc.currentContext }`)
|
|
||||||
const cluster = new Cluster({
|
const cluster = new Cluster({
|
||||||
id: clusterInfo.id,
|
id: clusterInfo.id,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
kubeConfig: clusterInfo.kubeConfig,
|
kubeConfigPath: clusterInfo.kubeConfigPath,
|
||||||
|
contextName: clusterInfo.contextName,
|
||||||
preferences: clusterInfo.preferences,
|
preferences: clusterInfo.preferences,
|
||||||
workspace: clusterInfo.workspace
|
workspace: clusterInfo.workspace
|
||||||
})
|
})
|
||||||
@ -77,33 +77,31 @@ export class ClusterManager {
|
|||||||
clusters.map(cluster => cluster.stopServer())
|
clusters.map(cluster => cluster.stopServer())
|
||||||
}
|
}
|
||||||
|
|
||||||
protected loadKubeConfig(config: string): KubeConfig {
|
protected loadKubeConfig(configPath: string): KubeConfig {
|
||||||
const kc = new KubeConfig();
|
const kc = new KubeConfig();
|
||||||
kc.loadFromString(config);
|
kc.loadFromFile(configPath)
|
||||||
return kc;
|
return kc;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async addNewCluster(clusterData: ClusterBaseInfo): Promise<Cluster> {
|
protected async addNewCluster(clusterData: ClusterBaseInfo): Promise<Cluster> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const configs: KubeConfig[] = k8s.loadAndSplitConfig(clusterData.kubeConfig)
|
const kc = this.loadKubeConfig(clusterData.kubeConfigPath)
|
||||||
if(configs.length == 0) {
|
k8s.validateConfig(kc)
|
||||||
reject("No cluster contexts defined")
|
kc.setCurrentContext(clusterData.contextName)
|
||||||
}
|
const cluster = new Cluster({
|
||||||
configs.forEach(c => {
|
id: uuid(),
|
||||||
k8s.validateConfig(c)
|
port: this.port,
|
||||||
const cluster = new Cluster({
|
kubeConfigPath: clusterData.kubeConfigPath,
|
||||||
id: uuid(),
|
contextName: clusterData.contextName,
|
||||||
port: this.port,
|
preferences: clusterData.preferences,
|
||||||
kubeConfig: k8s.dumpConfigYaml(c),
|
workspace: clusterData.workspace
|
||||||
preferences: clusterData.preferences,
|
})
|
||||||
workspace: clusterData.workspace
|
cluster.init(kc)
|
||||||
})
|
cluster.save()
|
||||||
cluster.init(c)
|
this.clusters.set(cluster.id, cluster)
|
||||||
cluster.save()
|
resolve(cluster)
|
||||||
this.clusters.set(cluster.id, cluster)
|
|
||||||
resolve(cluster)
|
|
||||||
});
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.error(error)
|
logger.error(error)
|
||||||
reject(error)
|
reject(error)
|
||||||
|
|||||||
@ -6,9 +6,9 @@ import logger from "./logger"
|
|||||||
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
|
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
|
||||||
import * as fm from "./feature-manager";
|
import * as fm from "./feature-manager";
|
||||||
import { Kubectl } from "./kubectl";
|
import { Kubectl } from "./kubectl";
|
||||||
|
import { KubeconfigManager } from "./kubeconfig-manager"
|
||||||
import { PromiseIpc } from "electron-promise-ipc"
|
import { PromiseIpc } from "electron-promise-ipc"
|
||||||
import request from "request-promise-native"
|
import request from "request-promise-native"
|
||||||
import { KubeconfigManager } from "./kubeconfig-manager"
|
|
||||||
import { apiPrefix } from "../common/vars";
|
import { apiPrefix } from "../common/vars";
|
||||||
|
|
||||||
enum ClusterStatus {
|
enum ClusterStatus {
|
||||||
@ -19,7 +19,8 @@ enum ClusterStatus {
|
|||||||
|
|
||||||
export interface ClusterBaseInfo {
|
export interface ClusterBaseInfo {
|
||||||
id: string;
|
id: string;
|
||||||
kubeConfig: string;
|
kubeConfigPath: string;
|
||||||
|
contextName: string;
|
||||||
preferences?: ClusterPreferences;
|
preferences?: ClusterPreferences;
|
||||||
port?: number;
|
port?: number;
|
||||||
workspace?: string;
|
workspace?: string;
|
||||||
@ -73,7 +74,7 @@ export class Cluster implements ClusterInfo {
|
|||||||
public isAdmin: boolean;
|
public isAdmin: boolean;
|
||||||
public features: FeatureStatusMap;
|
public features: FeatureStatusMap;
|
||||||
public kubeCtl: Kubectl
|
public kubeCtl: Kubectl
|
||||||
public kubeConfig: string;
|
public kubeConfigPath: string;
|
||||||
public eventCount: number;
|
public eventCount: number;
|
||||||
public preferences: ClusterPreferences;
|
public preferences: ClusterPreferences;
|
||||||
|
|
||||||
@ -85,18 +86,25 @@ export class Cluster implements ClusterInfo {
|
|||||||
constructor(clusterInfo: ClusterBaseInfo) {
|
constructor(clusterInfo: ClusterBaseInfo) {
|
||||||
if (clusterInfo) Object.assign(this, clusterInfo)
|
if (clusterInfo) Object.assign(this, clusterInfo)
|
||||||
if (!this.preferences) this.preferences = {}
|
if (!this.preferences) this.preferences = {}
|
||||||
this.kubeconfigManager = new KubeconfigManager(this.kubeConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public kubeconfigPath() {
|
public proxyKubeconfigPath() {
|
||||||
return this.kubeconfigManager.getPath()
|
return this.kubeconfigManager.getPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public proxyKubeconfig() {
|
||||||
|
const kc = new KubeConfig()
|
||||||
|
kc.loadFromFile(this.proxyKubeconfigPath())
|
||||||
|
return kc
|
||||||
|
}
|
||||||
|
|
||||||
public async init(kc: KubeConfig) {
|
public async init(kc: KubeConfig) {
|
||||||
this.contextHandler = new ContextHandler(kc, this)
|
|
||||||
this.contextName = kc.currentContext
|
|
||||||
this.url = this.contextHandler.url
|
|
||||||
this.apiUrl = kc.getCurrentCluster().server
|
this.apiUrl = kc.getCurrentCluster().server
|
||||||
|
this.contextHandler = new ContextHandler(kc, this)
|
||||||
|
await this.contextHandler.init() // So we get the proxy port reserved
|
||||||
|
this.kubeconfigManager = new KubeconfigManager(this)
|
||||||
|
|
||||||
|
this.url = this.contextHandler.url
|
||||||
}
|
}
|
||||||
|
|
||||||
public stopServer() {
|
public stopServer() {
|
||||||
@ -129,7 +137,7 @@ export class Cluster implements ClusterInfo {
|
|||||||
|
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
this.distribution = this.detectKubernetesDistribution(this.version)
|
this.distribution = this.detectKubernetesDistribution(this.version)
|
||||||
this.features = await fm.getFeatures(this.contextHandler)
|
this.features = await fm.getFeatures(this)
|
||||||
this.isAdmin = await this.isClusterAdmin()
|
this.isAdmin = await this.isClusterAdmin()
|
||||||
this.nodes = await this.getNodeCount()
|
this.nodes = await this.getNodeCount()
|
||||||
this.kubeCtl = new Kubectl(this.version)
|
this.kubeCtl = new Kubectl(this.version)
|
||||||
@ -138,16 +146,6 @@ export class Cluster implements ClusterInfo {
|
|||||||
this.eventCount = await this.getEventCount();
|
this.eventCount = await this.getEventCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateKubeconfig(kubeconfig: string) {
|
|
||||||
const storedCluster = clusterStore.getCluster(this.id)
|
|
||||||
if (!storedCluster) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.kubeConfig = kubeconfig
|
|
||||||
this.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
public getPrometheusApiPrefix() {
|
public getPrometheusApiPrefix() {
|
||||||
if (!this.preferences.prometheus?.prefix) {
|
if (!this.preferences.prometheus?.prefix) {
|
||||||
return ""
|
return ""
|
||||||
@ -164,7 +162,7 @@ export class Cluster implements ClusterInfo {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
workspace: this.workspace,
|
workspace: this.workspace,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
contextName: this.contextHandler.kc.currentContext,
|
contextName: this.contextName,
|
||||||
apiUrl: this.apiUrl,
|
apiUrl: this.apiUrl,
|
||||||
online: this.online,
|
online: this.online,
|
||||||
accessible: this.accessible,
|
accessible: this.accessible,
|
||||||
@ -175,7 +173,7 @@ export class Cluster implements ClusterInfo {
|
|||||||
isAdmin: this.isAdmin,
|
isAdmin: this.isAdmin,
|
||||||
features: this.features,
|
features: this.features,
|
||||||
kubeCtl: this.kubeCtl,
|
kubeCtl: this.kubeCtl,
|
||||||
kubeConfig: this.kubeConfig,
|
kubeConfigPath: this.kubeConfigPath,
|
||||||
preferences: this.preferences
|
preferences: this.preferences
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,7 +222,7 @@ export class Cluster implements ClusterInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
|
public async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
|
||||||
const authApi = this.contextHandler.kc.makeApiClient(AuthorizationV1Api)
|
const authApi = this.proxyKubeconfig().makeApiClient(AuthorizationV1Api)
|
||||||
try {
|
try {
|
||||||
const accessReview = await authApi.createSelfSubjectAccessReview({
|
const accessReview = await authApi.createSelfSubjectAccessReview({
|
||||||
apiVersion: "authorization.k8s.io/v1",
|
apiVersion: "authorization.k8s.io/v1",
|
||||||
@ -286,7 +284,7 @@ export class Cluster implements ClusterInfo {
|
|||||||
if (!this.isAdmin) {
|
if (!this.isAdmin) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const client = this.contextHandler.kc.makeApiClient(CoreV1Api);
|
const client = this.proxyKubeconfig().makeApiClient(CoreV1Api);
|
||||||
try {
|
try {
|
||||||
const response = await client.listEventForAllNamespaces(false, null, null, null, 1000);
|
const response = await client.listEventForAllNamespaces(false, null, null, null, 1000);
|
||||||
const uniqEventSources = new Set();
|
const uniqEventSources = new Set();
|
||||||
|
|||||||
@ -7,27 +7,25 @@ import { getFreePort } from "./port"
|
|||||||
import { KubeAuthProxy } from "./kube-auth-proxy"
|
import { KubeAuthProxy } from "./kube-auth-proxy"
|
||||||
import { Cluster, ClusterPreferences } from "./cluster"
|
import { Cluster, ClusterPreferences } from "./cluster"
|
||||||
import { prometheusProviders } from "../common/prometheus-providers"
|
import { prometheusProviders } from "../common/prometheus-providers"
|
||||||
import { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"
|
import { PrometheusService, PrometheusProvider } from "./prometheus/provider-registry"
|
||||||
|
|
||||||
export class ContextHandler {
|
export class ContextHandler {
|
||||||
public contextName: string
|
public contextName: string
|
||||||
public id: string
|
public id: string
|
||||||
public url: string
|
public url: string
|
||||||
public kc: KubeConfig
|
public clusterUrl: url.UrlWithStringQuery
|
||||||
|
public proxyServer: KubeAuthProxy
|
||||||
|
public proxyPort: number
|
||||||
public certData: string
|
public certData: string
|
||||||
public authCertData: string
|
public authCertData: string
|
||||||
public cluster: Cluster
|
public cluster: Cluster
|
||||||
|
|
||||||
protected apiTarget: ServerOptions
|
protected apiTarget: ServerOptions
|
||||||
protected proxyTarget: ServerOptions
|
protected proxyTarget: ServerOptions
|
||||||
protected clusterUrl: url.UrlWithStringQuery
|
|
||||||
protected proxyServer: KubeAuthProxy
|
|
||||||
|
|
||||||
protected clientCert: string
|
protected clientCert: string
|
||||||
protected clientKey: string
|
protected clientKey: string
|
||||||
protected secureApiConnection = true
|
protected secureApiConnection = true
|
||||||
protected defaultNamespace: string
|
protected defaultNamespace: string
|
||||||
protected proxyPort: number
|
|
||||||
protected kubernetesApi: string
|
protected kubernetesApi: string
|
||||||
protected prometheusProvider: string
|
protected prometheusProvider: string
|
||||||
protected prometheusPath: string
|
protected prometheusPath: string
|
||||||
@ -35,39 +33,21 @@ export class ContextHandler {
|
|||||||
|
|
||||||
constructor(kc: KubeConfig, cluster: Cluster) {
|
constructor(kc: KubeConfig, cluster: Cluster) {
|
||||||
this.id = cluster.id
|
this.id = cluster.id
|
||||||
this.kc = new KubeConfig()
|
|
||||||
this.kc.users = [
|
|
||||||
{
|
|
||||||
name: kc.getCurrentUser().name,
|
|
||||||
token: this.id
|
|
||||||
}
|
|
||||||
]
|
|
||||||
this.kc.contexts = [
|
|
||||||
{
|
|
||||||
name: kc.currentContext,
|
|
||||||
cluster: kc.getCurrentCluster().name,
|
|
||||||
user: kc.getCurrentUser().name,
|
|
||||||
namespace: kc.getContextObject(kc.currentContext).namespace
|
|
||||||
}
|
|
||||||
]
|
|
||||||
this.kc.setCurrentContext(kc.currentContext)
|
|
||||||
|
|
||||||
this.cluster = cluster
|
this.cluster = cluster
|
||||||
this.clusterUrl = url.parse(kc.getCurrentCluster().server)
|
this.clusterUrl = url.parse(cluster.apiUrl)
|
||||||
this.contextName = kc.currentContext;
|
this.contextName = cluster.contextName;
|
||||||
this.defaultNamespace = kc.getContextObject(kc.currentContext).namespace
|
this.defaultNamespace = kc.getContextObject(cluster.contextName).namespace
|
||||||
this.url = `http://${this.id}.localhost:${cluster.port}/`
|
this.url = `http://${this.id}.localhost:${cluster.port}/`
|
||||||
this.kubernetesApi = `http://127.0.0.1:${cluster.port}/${this.id}`
|
this.kubernetesApi = `http://127.0.0.1:${cluster.port}/${this.id}`
|
||||||
this.kc.clusters = [
|
|
||||||
{
|
|
||||||
name: kc.getCurrentCluster().name,
|
|
||||||
server: this.kubernetesApi,
|
|
||||||
skipTLSVerify: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
this.setClusterPreferences(cluster.preferences)
|
this.setClusterPreferences(cluster.preferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async init() {
|
||||||
|
await this.resolveProxyPort()
|
||||||
|
}
|
||||||
|
|
||||||
public setClusterPreferences(clusterPreferences?: ClusterPreferences) {
|
public setClusterPreferences(clusterPreferences?: ClusterPreferences) {
|
||||||
this.prometheusProvider = clusterPreferences.prometheusProvider?.type
|
this.prometheusProvider = clusterPreferences.prometheusProvider?.type
|
||||||
|
|
||||||
@ -103,7 +83,7 @@ export class ContextHandler {
|
|||||||
public async getPrometheusService(): Promise<PrometheusService> {
|
public async getPrometheusService(): Promise<PrometheusService> {
|
||||||
const providers = this.prometheusProvider ? prometheusProviders.filter((p, _) => p.id == this.prometheusProvider) : prometheusProviders
|
const providers = this.prometheusProvider ? prometheusProviders.filter((p, _) => p.id == this.prometheusProvider) : prometheusProviders
|
||||||
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
|
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
|
||||||
const apiClient = this.kc.makeApiClient(CoreV1Api)
|
const apiClient = this.cluster.proxyKubeconfig().makeApiClient(CoreV1Api)
|
||||||
return await provider.getPrometheusService(apiClient)
|
return await provider.getPrometheusService(apiClient)
|
||||||
})
|
})
|
||||||
const resolvedPrometheusServices = await Promise.all(prometheusPromises)
|
const resolvedPrometheusServices = await Promise.all(prometheusPromises)
|
||||||
@ -174,7 +154,7 @@ export class ContextHandler {
|
|||||||
|
|
||||||
public async withTemporaryKubeconfig(callback: (kubeconfig: string) => Promise<any>) {
|
public async withTemporaryKubeconfig(callback: (kubeconfig: string) => Promise<any>) {
|
||||||
try {
|
try {
|
||||||
await callback(this.cluster.kubeconfigPath())
|
await callback(this.cluster.proxyKubeconfigPath())
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw(error)
|
throw(error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ContextHandler } from "./context-handler"
|
import { KubeConfig } from "@kubernetes/client-node"
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { Cluster } from "./cluster";
|
import { Cluster } from "./cluster";
|
||||||
import { Feature, FeatureStatusMap } from "./feature"
|
import { Feature, FeatureStatusMap } from "./feature"
|
||||||
@ -10,17 +10,19 @@ const ALL_FEATURES: any = {
|
|||||||
'user-mode': new UserModeFeature(null),
|
'user-mode': new UserModeFeature(null),
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFeatures(clusterContext: ContextHandler): Promise<FeatureStatusMap> {
|
export async function getFeatures(cluster: Cluster): Promise<FeatureStatusMap> {
|
||||||
return new Promise<FeatureStatusMap>(async (resolve, reject) => {
|
return new Promise<FeatureStatusMap>(async (resolve, reject) => {
|
||||||
const result: FeatureStatusMap = {};
|
const result: FeatureStatusMap = {};
|
||||||
logger.debug(`features for ${clusterContext.contextName}`);
|
logger.debug(`features for ${cluster.contextName}`);
|
||||||
for (const key in ALL_FEATURES) {
|
for (const key in ALL_FEATURES) {
|
||||||
logger.debug(`feature ${key}`);
|
logger.debug(`feature ${key}`);
|
||||||
if (ALL_FEATURES.hasOwnProperty(key)) {
|
if (ALL_FEATURES.hasOwnProperty(key)) {
|
||||||
logger.debug("getting feature status...");
|
logger.debug("getting feature status...");
|
||||||
const feature = ALL_FEATURES[key] as Feature;
|
const feature = ALL_FEATURES[key] as Feature;
|
||||||
|
const kc = new KubeConfig()
|
||||||
const status = await feature.featureStatus(clusterContext.kc);
|
kc.loadFromFile(cluster.proxyKubeconfigPath())
|
||||||
|
|
||||||
|
const status = await feature.featureStatus(kc);
|
||||||
result[feature.name] = status
|
result[feature.name] = status
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export class HelmReleaseManager {
|
|||||||
await fs.promises.writeFile(fileName, yaml.safeDump(values))
|
await fs.promises.writeFile(fileName, yaml.safeDump(values))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout, stderr } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.kubeconfigPath()}`).catch((error) => { throw(error.stderr)})
|
const { stdout, stderr } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.proxyKubeconfigPath()}`).catch((error) => { throw(error.stderr)})
|
||||||
return {
|
return {
|
||||||
log: stdout,
|
log: stdout,
|
||||||
release: this.getRelease(name, namespace, cluster)
|
release: this.getRelease(name, namespace, cluster)
|
||||||
@ -66,7 +66,7 @@ export class HelmReleaseManager {
|
|||||||
|
|
||||||
public async getRelease(name: string, namespace: string, cluster: Cluster) {
|
public async getRelease(name: string, namespace: string, cluster: Cluster) {
|
||||||
const helm = await helmCli.binaryPath()
|
const helm = await helmCli.binaryPath()
|
||||||
const {stdout, stderr} = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.kubeconfigPath()}`).catch((error) => { throw(error.stderr)})
|
const {stdout, stderr} = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.proxyKubeconfigPath()}`).catch((error) => { throw(error.stderr)})
|
||||||
const release = JSON.parse(stdout)
|
const release = JSON.parse(stdout)
|
||||||
release.resources = await this.getResources(name, namespace, cluster)
|
release.resources = await this.getResources(name, namespace, cluster)
|
||||||
return release
|
return release
|
||||||
@ -100,7 +100,7 @@ export class HelmReleaseManager {
|
|||||||
protected async getResources(name: string, namespace: string, cluster: Cluster) {
|
protected async getResources(name: string, namespace: string, cluster: Cluster) {
|
||||||
const helm = await helmCli.binaryPath()
|
const helm = await helmCli.binaryPath()
|
||||||
const kubectl = await cluster.kubeCtl.kubectlPath()
|
const kubectl = await cluster.kubeCtl.kubectlPath()
|
||||||
const pathToKubeconfig = cluster.kubeconfigPath()
|
const pathToKubeconfig = cluster.proxyKubeconfigPath()
|
||||||
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch((error) => {
|
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch((error) => {
|
||||||
return { stdout: JSON.stringify({items: []})}
|
return { stdout: JSON.stringify({items: []})}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { releaseManager } from "./helm-release-manager";
|
|||||||
|
|
||||||
class HelmService {
|
class HelmService {
|
||||||
public async installChart(cluster: Cluster, data: {chart: string; values: {}; name: string; namespace: string; version: string}) {
|
public async installChart(cluster: Cluster, data: {chart: string; values: {}; name: string; namespace: string; version: string}) {
|
||||||
const installResult = await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.kubeconfigPath())
|
const installResult = await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.proxyKubeconfigPath())
|
||||||
return installResult
|
return installResult
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ class HelmService {
|
|||||||
|
|
||||||
public async listReleases(cluster: Cluster, namespace: string = null) {
|
public async listReleases(cluster: Cluster, namespace: string = null) {
|
||||||
await repoManager.init()
|
await repoManager.init()
|
||||||
const releases = await releaseManager.listReleases(cluster.kubeconfigPath(), namespace)
|
const releases = await releaseManager.listReleases(cluster.proxyKubeconfigPath(), namespace)
|
||||||
return releases
|
return releases
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,19 +60,19 @@ class HelmService {
|
|||||||
|
|
||||||
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
|
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
logger.debug("Fetch release values")
|
logger.debug("Fetch release values")
|
||||||
const values = await releaseManager.getValues(releaseName, namespace, cluster.kubeconfigPath())
|
const values = await releaseManager.getValues(releaseName, namespace, cluster.proxyKubeconfigPath())
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
logger.debug("Fetch release history")
|
logger.debug("Fetch release history")
|
||||||
const history = await releaseManager.getHistory(releaseName, namespace, cluster.kubeconfigPath())
|
const history = await releaseManager.getHistory(releaseName, namespace, cluster.proxyKubeconfigPath())
|
||||||
return(history)
|
return(history)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
logger.debug("Delete release")
|
logger.debug("Delete release")
|
||||||
const release = await releaseManager.deleteRelease(releaseName, namespace, cluster.kubeconfigPath())
|
const release = await releaseManager.deleteRelease(releaseName, namespace, cluster.proxyKubeconfigPath())
|
||||||
return release
|
return release
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ class HelmService {
|
|||||||
|
|
||||||
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
|
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
|
||||||
logger.debug("Rollback release")
|
logger.debug("Rollback release")
|
||||||
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.kubeconfigPath())
|
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.proxyKubeconfigPath())
|
||||||
return({ message: output })
|
return({ message: output })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -70,13 +70,13 @@ export function splitConfig(kubeConfig: k8s.KubeConfig): k8s.KubeConfig[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads KubeConfig from a yaml string and breaks it into several configs. Each context
|
* Loads KubeConfig from a yaml and breaks it into several configs. Each context per KubeConfig object
|
||||||
*
|
*
|
||||||
* @param configString yaml string of kube config
|
* @param configPath path to kube config yaml file
|
||||||
*/
|
*/
|
||||||
export function loadAndSplitConfig(configString: string): k8s.KubeConfig[] {
|
export function loadAndSplitConfig(configPath: string): k8s.KubeConfig[] {
|
||||||
const allConfigs = new k8s.KubeConfig();
|
const allConfigs = new k8s.KubeConfig();
|
||||||
allConfigs.loadFromString(configString);
|
allConfigs.loadFromFile(configPath);
|
||||||
return splitConfig(allConfigs);
|
return splitConfig(allConfigs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,8 @@ import logger from "./logger"
|
|||||||
import * as tcpPortUsed from "tcp-port-used"
|
import * as tcpPortUsed from "tcp-port-used"
|
||||||
import { Kubectl, bundledKubectl } from "./kubectl"
|
import { Kubectl, bundledKubectl } from "./kubectl"
|
||||||
import { Cluster } from "./cluster"
|
import { Cluster } from "./cluster"
|
||||||
import { readFileSync, watch } from "fs"
|
|
||||||
import { PromiseIpc } from "electron-promise-ipc"
|
import { PromiseIpc } from "electron-promise-ipc"
|
||||||
import { findMainWebContents } from "./webcontents"
|
import { findMainWebContents } from "./webcontents"
|
||||||
import * as url from "url"
|
|
||||||
|
|
||||||
export class KubeAuthProxy {
|
export class KubeAuthProxy {
|
||||||
public lastError: string
|
public lastError: string
|
||||||
@ -31,26 +29,18 @@ export class KubeAuthProxy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const proxyBin = await this.kubectl.kubectlPath()
|
const proxyBin = await this.kubectl.kubectlPath()
|
||||||
const configWatcher = watch(this.cluster.kubeconfigPath(), (eventType: string, filename: string) => {
|
|
||||||
if (eventType === "change") {
|
|
||||||
const kc = readFileSync(this.cluster.kubeconfigPath()).toString()
|
|
||||||
if (kc.trim().length > 0) { // Prevent updating empty configs back to store
|
|
||||||
this.cluster.updateKubeconfig(kc)
|
|
||||||
} else {
|
|
||||||
logger.warn(`kubeconfig watch on ${this.cluster.kubeconfigPath()} resulted into empty config, ignoring...`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const clusterUrl = url.parse(this.cluster.apiUrl)
|
|
||||||
let args = [
|
let args = [
|
||||||
"proxy",
|
"proxy",
|
||||||
"--port", this.port.toString(),
|
"-p", this.port.toString(),
|
||||||
"--kubeconfig", this.cluster.kubeconfigPath(),
|
"--kubeconfig", this.cluster.kubeConfigPath,
|
||||||
"--accept-hosts", clusterUrl.hostname,
|
"--context", this.cluster.contextName,
|
||||||
|
"--accept-hosts", ".*",
|
||||||
|
"--reject-paths", "^[^/]"
|
||||||
]
|
]
|
||||||
if (process.env.DEBUG_PROXY === "true") {
|
if (process.env.DEBUG_PROXY === "true") {
|
||||||
args = args.concat(["-v", "9"])
|
args = args.concat(["-v", "9"])
|
||||||
}
|
}
|
||||||
|
logger.debug(`spawning kubectl proxy with args: ${args}`)
|
||||||
this.proxyProcess = spawn(proxyBin, args, {
|
this.proxyProcess = spawn(proxyBin, args, {
|
||||||
env: this.env
|
env: this.env
|
||||||
})
|
})
|
||||||
@ -60,7 +50,6 @@ export class KubeAuthProxy {
|
|||||||
logger.debug("failed to send IPC log message: " + err.message)
|
logger.debug("failed to send IPC log message: " + err.message)
|
||||||
})
|
})
|
||||||
this.proxyProcess = null
|
this.proxyProcess = null
|
||||||
configWatcher.close()
|
|
||||||
})
|
})
|
||||||
this.proxyProcess.stdout.on('data', (data) => {
|
this.proxyProcess.stdout.on('data', (data) => {
|
||||||
let logItem = data.toString()
|
let logItem = data.toString()
|
||||||
|
|||||||
@ -2,14 +2,17 @@ import { app } from "electron"
|
|||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { ensureDir, randomFileName} from "./file-helpers"
|
import { ensureDir, randomFileName} from "./file-helpers"
|
||||||
import logger from "./logger"
|
import logger from "./logger"
|
||||||
|
import { Cluster } from "./cluster"
|
||||||
|
import * as k8s from "./k8s"
|
||||||
|
import { KubeConfig } from "@kubernetes/client-node"
|
||||||
|
|
||||||
export class KubeconfigManager {
|
export class KubeconfigManager {
|
||||||
protected configDir = app.getPath("temp")
|
protected configDir = app.getPath("temp")
|
||||||
protected kubeconfig: string
|
|
||||||
protected tempFile: string
|
protected tempFile: string
|
||||||
|
protected cluster: Cluster
|
||||||
|
|
||||||
constructor(kubeconfig: string) {
|
constructor(cluster: Cluster) {
|
||||||
this.kubeconfig = kubeconfig
|
this.cluster = cluster
|
||||||
this.tempFile = this.createTemporaryKubeconfig()
|
this.tempFile = this.createTemporaryKubeconfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,11 +20,38 @@ export class KubeconfigManager {
|
|||||||
return this.tempFile
|
return this.tempFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates new "temporary" kubeconfig that point to the kubectl-proxy.
|
||||||
|
* This way any user of the config does not need to know anything about the auth etc. details.
|
||||||
|
*/
|
||||||
protected createTemporaryKubeconfig(): string {
|
protected createTemporaryKubeconfig(): string {
|
||||||
ensureDir(this.configDir)
|
ensureDir(this.configDir)
|
||||||
const path = `${this.configDir}/${randomFileName("kubeconfig")}`
|
const path = `${this.configDir}/${randomFileName("kubeconfig")}`
|
||||||
logger.debug('Creating temporary kubeconfig: ' + path)
|
const originalKc = new KubeConfig()
|
||||||
fs.writeFileSync(path, this.kubeconfig)
|
originalKc.loadFromFile(this.cluster.kubeConfigPath)
|
||||||
|
const kc = {
|
||||||
|
clusters: [
|
||||||
|
{
|
||||||
|
name: this.cluster.contextName,
|
||||||
|
server: `http://127.0.0.1:${this.cluster.contextHandler.proxyPort}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
name: "proxy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
contexts: [
|
||||||
|
{
|
||||||
|
name: this.cluster.contextName,
|
||||||
|
cluster: this.cluster.contextName,
|
||||||
|
namespace: originalKc.getContextObject(this.cluster.contextName).namespace,
|
||||||
|
user: "proxy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
currentContext: this.cluster.contextName
|
||||||
|
} as KubeConfig
|
||||||
|
fs.writeFileSync(path, k8s.dumpConfigYaml(kc))
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export class NodeShellSession extends ShellSession {
|
|||||||
super(socket, pathToKubeconfig, cluster)
|
super(socket, pathToKubeconfig, cluster)
|
||||||
this.nodeName = nodeName
|
this.nodeName = nodeName
|
||||||
this.podId = `node-shell-${uuid()}`
|
this.podId = `node-shell-${uuid()}`
|
||||||
this.kc = cluster.contextHandler.kc
|
this.kc = cluster.proxyKubeconfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ class ResourceApplierApi extends LensApi {
|
|||||||
public async applyResource(request: LensApiRequest) {
|
public async applyResource(request: LensApiRequest) {
|
||||||
const { response, cluster, payload } = request
|
const { response, cluster, payload } = request
|
||||||
try {
|
try {
|
||||||
const resource = await resourceApplier.apply(cluster, cluster.kubeconfigPath(), payload)
|
const resource = await resourceApplier.apply(cluster, cluster.proxyKubeconfigPath(), payload)
|
||||||
this.respondJson(response, [resource], 200)
|
this.respondJson(response, [resource], 200)
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
this.respondText(response, error, 422)
|
this.respondText(response, error, 422)
|
||||||
|
|||||||
@ -45,7 +45,7 @@ const apiResources = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
async function getAllowedNamespaces(cluster: Cluster) {
|
async function getAllowedNamespaces(cluster: Cluster) {
|
||||||
const api = cluster.contextHandler.kc.makeApiClient(CoreV1Api)
|
const api = cluster.proxyKubeconfig().makeApiClient(CoreV1Api)
|
||||||
try {
|
try {
|
||||||
const namespaceList = await api.listNamespace()
|
const namespaceList = await api.listNamespace()
|
||||||
const nsAccessStatuses = await Promise.all(
|
const nsAccessStatuses = await Promise.all(
|
||||||
@ -58,9 +58,8 @@ async function getAllowedNamespaces(cluster: Cluster) {
|
|||||||
return namespaceList.body.items
|
return namespaceList.body.items
|
||||||
.filter((ns, i) => nsAccessStatuses[i])
|
.filter((ns, i) => nsAccessStatuses[i])
|
||||||
.map(ns => ns.metadata.name)
|
.map(ns => ns.metadata.name)
|
||||||
} catch (error) {
|
} catch(error) {
|
||||||
const kc = cluster.contextHandler.kc
|
const ctx = cluster.proxyKubeconfig().getContextObject(cluster.contextName)
|
||||||
const ctx = kc.getContextObject(kc.currentContext)
|
|
||||||
if (ctx.namespace) {
|
if (ctx.namespace) {
|
||||||
return [ctx.namespace]
|
return [ctx.namespace]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
|
|||||||
{
|
{
|
||||||
'name': cluster.contextName,
|
'name': cluster.contextName,
|
||||||
'cluster': {
|
'cluster': {
|
||||||
'server': cluster.contextHandler.kc.getCurrentCluster().server,
|
'server': cluster.apiUrl,
|
||||||
'certificate-authority-data': secret.data["ca.crt"]
|
'certificate-authority-data': secret.data["ca.crt"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,7 +44,7 @@ class KubeconfigRoute extends LensApi {
|
|||||||
public async routeServiceAccountRoute(request: LensApiRequest) {
|
public async routeServiceAccountRoute(request: LensApiRequest) {
|
||||||
const { params, response, cluster} = request
|
const { params, response, cluster} = request
|
||||||
|
|
||||||
const client = cluster.contextHandler.kc.makeApiClient(CoreV1Api);
|
const client = cluster.proxyKubeconfig().makeApiClient(CoreV1Api);
|
||||||
const secretList = await client.listNamespacedSecret(params.namespace)
|
const secretList = await client.listNamespacedSecret(params.namespace)
|
||||||
const secret = secretList.body.items.find(secret => {
|
const secret = secretList.body.items.find(secret => {
|
||||||
const { annotations } = secret.metadata;
|
const { annotations } = secret.metadata;
|
||||||
|
|||||||
@ -87,7 +87,7 @@ class PortForwardRoute extends LensApi {
|
|||||||
namespace: params.namespace,
|
namespace: params.namespace,
|
||||||
name: params.service,
|
name: params.service,
|
||||||
port: params.port,
|
port: params.port,
|
||||||
kubeConfig: cluster.kubeconfigPath()
|
kubeConfig: cluster.proxyKubeconfigPath()
|
||||||
})
|
})
|
||||||
const started = await portForward.start()
|
const started = await portForward.start()
|
||||||
if (!started) {
|
if (!started) {
|
||||||
|
|||||||
@ -38,7 +38,17 @@ class ApiWatcher {
|
|||||||
clearInterval(this.processor)
|
clearInterval(this.processor)
|
||||||
}
|
}
|
||||||
logger.debug("Stopping watcher for api: " + this.apiUrl)
|
logger.debug("Stopping watcher for api: " + this.apiUrl)
|
||||||
this.watchRequest.abort()
|
try {
|
||||||
|
this.watchRequest.abort()
|
||||||
|
this.sendEvent({
|
||||||
|
type: "STREAM_END",
|
||||||
|
url: this.apiUrl,
|
||||||
|
status: 410,
|
||||||
|
})
|
||||||
|
logger.debug("watch aborted")
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Watch abort errored:" + error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private watchHandler(phase: string, obj: any) {
|
private watchHandler(phase: string, obj: any) {
|
||||||
@ -50,12 +60,7 @@ class ApiWatcher {
|
|||||||
|
|
||||||
private doneHandler(error: Error) {
|
private doneHandler(error: Error) {
|
||||||
if (error) logger.warn("watch ended: " + error.toString())
|
if (error) logger.warn("watch ended: " + error.toString())
|
||||||
|
this.watchRequest.abort()
|
||||||
this.sendEvent({
|
|
||||||
type: "STREAM_END",
|
|
||||||
url: this.apiUrl,
|
|
||||||
status: 410,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendEvent(evt: any) {
|
private sendEvent(evt: any) {
|
||||||
@ -82,9 +87,10 @@ class WatchRoute extends LensApi {
|
|||||||
response.setHeader("Content-Type", "text/event-stream")
|
response.setHeader("Content-Type", "text/event-stream")
|
||||||
response.setHeader("Cache-Control", "no-cache")
|
response.setHeader("Cache-Control", "no-cache")
|
||||||
response.setHeader("Connection", "keep-alive")
|
response.setHeader("Connection", "keep-alive")
|
||||||
|
logger.debug("watch using kubeconfig:" + JSON.stringify(cluster.proxyKubeconfig(), null, 2))
|
||||||
|
|
||||||
apis.forEach(apiUrl => {
|
apis.forEach(apiUrl => {
|
||||||
const watcher = new ApiWatcher(apiUrl, cluster.contextHandler.kc, response)
|
const watcher = new ApiWatcher(apiUrl, cluster.proxyKubeconfig(), response)
|
||||||
watcher.start()
|
watcher.start()
|
||||||
watchers.push(watcher)
|
watchers.push(watcher)
|
||||||
})
|
})
|
||||||
|
|||||||
39
src/migrations/cluster-store/3.6.0-beta.1.ts
Normal file
39
src/migrations/cluster-store/3.6.0-beta.1.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// move embedded kubeconfig into separate file and add reference to it to cluster settings
|
||||||
|
import { app } from "electron"
|
||||||
|
import { ensureDirSync } from "fs-extra"
|
||||||
|
import * as path from "path"
|
||||||
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
|
import { writeEmbeddedKubeConfig } from "../../common/utils/kubeconfig"
|
||||||
|
|
||||||
|
export function migration(store: any) {
|
||||||
|
console.log("CLUSTER STORE, MIGRATION: 3.6.0-beta.1");
|
||||||
|
const clusters: any[] = []
|
||||||
|
|
||||||
|
const kubeConfigBase = path.join(app.getPath("userData"), "kubeconfigs")
|
||||||
|
ensureDirSync(kubeConfigBase)
|
||||||
|
const storedClusters = store.get("clusters") as any[]
|
||||||
|
if (!storedClusters) return
|
||||||
|
|
||||||
|
console.log("num clusters to migrate: ", storedClusters.length)
|
||||||
|
for (const cluster of storedClusters ) {
|
||||||
|
try {
|
||||||
|
// take the embedded kubeconfig and dump it into a file
|
||||||
|
const kubeConfigFile = writeEmbeddedKubeConfig(cluster.id, cluster.kubeConfig)
|
||||||
|
cluster.kubeConfigPath = kubeConfigFile
|
||||||
|
|
||||||
|
const kc = new KubeConfig()
|
||||||
|
kc.loadFromFile(cluster.kubeConfigPath)
|
||||||
|
cluster.contextName = kc.getCurrentContext()
|
||||||
|
|
||||||
|
delete cluster.kubeConfig
|
||||||
|
clusters.push(cluster)
|
||||||
|
} catch(error) {
|
||||||
|
console.error("failed to migrate kubeconfig for cluster:", cluster.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "overwrite" the cluster configs
|
||||||
|
if (clusters.length > 0) {
|
||||||
|
store.set("clusters", clusters)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,18 @@
|
|||||||
<b-form-group
|
<b-form-group
|
||||||
label="Choose config:"
|
label="Choose config:"
|
||||||
>
|
>
|
||||||
|
<b-form-file
|
||||||
|
v-model="file"
|
||||||
|
:state="Boolean(file)"
|
||||||
|
placeholder="Choose a file or drop it here..."
|
||||||
|
drop-placeholder="Drop file here..."
|
||||||
|
@input="reloadKubeContexts()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
Selected file: {{ file ? file.name : '' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<b-form-select
|
<b-form-select
|
||||||
id="kubecontext-select"
|
id="kubecontext-select"
|
||||||
v-model="kubecontext"
|
v-model="kubecontext"
|
||||||
@ -113,6 +125,10 @@ import * as PrismEditor from 'vue-prism-editor'
|
|||||||
import * as k8s from "@kubernetes/client-node"
|
import * as k8s from "@kubernetes/client-node"
|
||||||
import { dumpConfigYaml } from "../../../main/k8s"
|
import { dumpConfigYaml } from "../../../main/k8s"
|
||||||
import ClustersMixin from "@/_vue/mixins/ClustersMixin";
|
import ClustersMixin from "@/_vue/mixins/ClustersMixin";
|
||||||
|
import * as path from "path"
|
||||||
|
import fs from 'fs'
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { writeEmbeddedKubeConfig} from "../../../common/utils/kubeconfig"
|
||||||
|
|
||||||
class ClusterAccessError extends Error {}
|
class ClusterAccessError extends Error {}
|
||||||
|
|
||||||
@ -125,6 +141,8 @@ export default {
|
|||||||
},
|
},
|
||||||
data(){
|
data(){
|
||||||
return {
|
return {
|
||||||
|
file: null,
|
||||||
|
filepath: null,
|
||||||
clusterconfig: "",
|
clusterconfig: "",
|
||||||
httpsProxy: "",
|
httpsProxy: "",
|
||||||
kubecontext: "",
|
kubecontext: "",
|
||||||
@ -136,7 +154,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function() {
|
mounted: function() {
|
||||||
this.$store.dispatch("reloadAvailableKubeContexts");
|
const kubeConfigPath = path.join(process.env.HOME, '.kube', 'config')
|
||||||
|
this.filepath = kubeConfigPath
|
||||||
|
this.file = new File(fs.readFileSync(this.filepath), this.filepath)
|
||||||
|
this.$store.dispatch("reloadAvailableKubeContexts", this.filepath);
|
||||||
this.seenContexts = JSON.parse(JSON.stringify(this.$store.getters.seenContexts)) // clone seenContexts from store
|
this.seenContexts = JSON.parse(JSON.stringify(this.$store.getters.seenContexts)) // clone seenContexts from store
|
||||||
this.storeSeenContexts()
|
this.storeSeenContexts()
|
||||||
},
|
},
|
||||||
@ -163,6 +184,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
reloadKubeContexts() {
|
||||||
|
this.filepath = this.file.path
|
||||||
|
this.$store.dispatch("reloadAvailableKubeContexts", this.file.path);
|
||||||
|
},
|
||||||
isNewContext(context) {
|
isNewContext(context) {
|
||||||
return this.newContexts.indexOf(context) > -1
|
return this.newContexts.indexOf(context) > -1
|
||||||
},
|
},
|
||||||
@ -196,8 +221,15 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const kc = new k8s.KubeConfig();
|
const kc = new k8s.KubeConfig();
|
||||||
kc.loadFromString(this.clusterconfig); // throws TypeError if we cannot parse kubeconfig
|
kc.loadFromString(this.clusterconfig); // throws TypeError if we cannot parse kubeconfig
|
||||||
|
const clusterId = uuidv4();
|
||||||
|
// We need to store the kubeconfig to "app-home"/
|
||||||
|
if (this.kubecontext === "custom") {
|
||||||
|
this.filepath = writeEmbeddedKubeConfig(clusterId, this.clusterconfig)
|
||||||
|
}
|
||||||
const clusterInfo = {
|
const clusterInfo = {
|
||||||
kubeConfig: dumpConfigYaml(kc),
|
id: clusterId,
|
||||||
|
kubeConfigPath: this.filepath,
|
||||||
|
contextName: kc.currentContext,
|
||||||
preferences: {
|
preferences: {
|
||||||
clusterName: kc.currentContext
|
clusterName: kc.currentContext
|
||||||
},
|
},
|
||||||
@ -206,6 +238,7 @@ export default {
|
|||||||
if (this.httpsProxy) {
|
if (this.httpsProxy) {
|
||||||
clusterInfo.preferences.httpsProxy = this.httpsProxy
|
clusterInfo.preferences.httpsProxy = this.httpsProxy
|
||||||
}
|
}
|
||||||
|
console.log("sending clusterInfo:", clusterInfo)
|
||||||
let res = await this.$store.dispatch('addCluster', clusterInfo)
|
let res = await this.$store.dispatch('addCluster', clusterInfo)
|
||||||
console.log("addCluster result:", res)
|
console.log("addCluster result:", res)
|
||||||
if(!res){
|
if(!res){
|
||||||
|
|||||||
@ -6,10 +6,10 @@ const state = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
reloadAvailableKubeContexts: ({commit}) => {
|
reloadAvailableKubeContexts({commit}, file) {
|
||||||
let kc = new k8s.KubeConfig();
|
let kc = new k8s.KubeConfig();
|
||||||
try {
|
try {
|
||||||
kc.loadFromDefault();
|
kc.loadFromFile(file);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to read default kubeconfig: " + error.message);
|
console.error("Failed to read default kubeconfig: " + error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user