mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
project restructuring and initial refactoring
This commit is contained in:
1 parent
5af12bdbbd
commit
aa0eca2dc4
807 files changed
+11848
-22947
No files matched your search
@@ -0,0 +1,110 @@
|
||||
import { app } from "electron"
|
||||
import { CoreV1Api } from "@kubernetes/client-node"
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { userStore } from "../../common/user-store"
|
||||
import { Cluster } from "../cluster"
|
||||
|
||||
export interface IConfigRoutePayload {
|
||||
kubeVersion?: string;
|
||||
clusterName?: string;
|
||||
lensVersion?: string;
|
||||
lensTheme?: string;
|
||||
username?: string;
|
||||
token?: string;
|
||||
allowedNamespaces?: string[];
|
||||
allowedResources?: string[];
|
||||
isClusterAdmin?: boolean;
|
||||
chartsEnabled: boolean;
|
||||
kubectlAccess?: boolean; // User accessed via kubectl-lens plugin
|
||||
}
|
||||
|
||||
// TODO: auto-populate all resources dynamically
|
||||
const apiResources = [
|
||||
{ resource: "configmaps" },
|
||||
{ resource: "cronjobs", group: "batch" },
|
||||
{ resource: "customresourcedefinitions", group: "apiextensions.k8s.io" },
|
||||
{ resource: "daemonsets", group: "apps" },
|
||||
{ resource: "deployments", group: "apps" },
|
||||
{ resource: "endpoints" },
|
||||
{ resource: "events" },
|
||||
{ resource: "horizontalpodautoscalers" },
|
||||
{ resource: "ingresses", group: "networking.k8s.io" },
|
||||
{ resource: "jobs", group: "batch" },
|
||||
{ resource: "namespaces" },
|
||||
{ resource: "networkpolicies", group: "networking.k8s.io" },
|
||||
{ resource: "nodes" },
|
||||
{ resource: "persistentvolumes" },
|
||||
{ resource: "pods" },
|
||||
{ resource: "podsecuritypolicies" },
|
||||
{ resource: "resourcequotas" },
|
||||
{ resource: "secrets" },
|
||||
{ resource: "services" },
|
||||
{ resource: "statefulsets", group: "apps" },
|
||||
{ resource: "storageclasses", group: "storage.k8s.io" },
|
||||
]
|
||||
|
||||
async function getAllowedNamespaces(cluster: Cluster) {
|
||||
const api = cluster.contextHandler.kc.makeApiClient(CoreV1Api)
|
||||
try {
|
||||
const namespaceList = await api.listNamespace()
|
||||
const nsAccessStatuses = await Promise.all(
|
||||
namespaceList.body.items.map(ns => cluster.canI({
|
||||
namespace: ns.metadata.name,
|
||||
resource: "pods",
|
||||
verb: "list",
|
||||
}))
|
||||
)
|
||||
return namespaceList.body.items
|
||||
.filter((ns, i) => nsAccessStatuses[i])
|
||||
.map(ns => ns.metadata.name)
|
||||
} catch (error) {
|
||||
const kc = cluster.contextHandler.kc
|
||||
const ctx = kc.getContextObject(kc.currentContext)
|
||||
if (ctx.namespace) {
|
||||
return [ctx.namespace]
|
||||
}
|
||||
else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getAllowedResources(cluster: Cluster, namespaces: string[]) {
|
||||
try {
|
||||
const resourceAccessStatuses = await Promise.all(
|
||||
apiResources.map(apiResource => cluster.canI({
|
||||
resource: apiResource.resource,
|
||||
group: apiResource.group,
|
||||
verb: "list",
|
||||
namespace: namespaces[0]
|
||||
}))
|
||||
)
|
||||
return apiResources
|
||||
.filter((resource, i) => resourceAccessStatuses[i]).map(apiResource => apiResource.resource)
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigRoute extends LensApi {
|
||||
public async routeConfig(request: LensApiRequest) {
|
||||
const { params, response, cluster } = request
|
||||
|
||||
const namespaces = await getAllowedNamespaces(cluster)
|
||||
const data: IConfigRoutePayload = {
|
||||
clusterName: cluster.contextName,
|
||||
lensVersion: app.getVersion(),
|
||||
lensTheme: `kontena-${userStore.getPreferences().colorTheme}`,
|
||||
kubeVersion: cluster.version,
|
||||
chartsEnabled: true,
|
||||
isClusterAdmin: cluster.isAdmin,
|
||||
allowedResources: await getAllowedResources(cluster, namespaces),
|
||||
allowedNamespaces: namespaces
|
||||
};
|
||||
|
||||
this.respondJson(response, data)
|
||||
}
|
||||
}
|
||||
|
||||
export const configRoute = new ConfigRoute()
|
||||
@@ -0,0 +1,58 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { Cluster } from "../cluster"
|
||||
import { CoreV1Api, V1Secret } from "@kubernetes/client-node"
|
||||
|
||||
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
|
||||
const tokenData = new Buffer(secret.data["token"], "base64")
|
||||
return {
|
||||
'apiVersion': 'v1',
|
||||
'kind': 'Config',
|
||||
'clusters': [
|
||||
{
|
||||
'name': cluster.contextName,
|
||||
'cluster': {
|
||||
'server': cluster.contextHandler.kc.getCurrentCluster().server,
|
||||
'certificate-authority-data': secret.data["ca.crt"]
|
||||
}
|
||||
}
|
||||
],
|
||||
'users': [
|
||||
{
|
||||
'name': username,
|
||||
'user': {
|
||||
'token': tokenData.toString("utf8"),
|
||||
}
|
||||
}
|
||||
],
|
||||
'contexts': [
|
||||
{
|
||||
'name': cluster.contextName,
|
||||
'context': {
|
||||
'user': username,
|
||||
'cluster': cluster.contextName,
|
||||
'namespace': secret.metadata.namespace,
|
||||
}
|
||||
}
|
||||
],
|
||||
'current-context': cluster.contextName
|
||||
}
|
||||
}
|
||||
|
||||
class KubeconfigRoute extends LensApi {
|
||||
|
||||
public async routeServiceAccountRoute(request: LensApiRequest) {
|
||||
const { params, response, cluster} = request
|
||||
|
||||
const client = cluster.contextHandler.kc.makeApiClient(CoreV1Api);
|
||||
const secretList = await client.listNamespacedSecret(params.namespace)
|
||||
const secret = secretList.body.items.find(secret => {
|
||||
const { annotations } = secret.metadata;
|
||||
return annotations && annotations["kubernetes.io/service-account.name"] == params.account;
|
||||
});
|
||||
const data = generateKubeConfig(params.account, secret, cluster);
|
||||
this.respondJson(response, data)
|
||||
}
|
||||
}
|
||||
|
||||
export const kubeconfigRoute = new KubeconfigRoute()
|
||||
@@ -0,0 +1,91 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import requestPromise from "request-promise-native"
|
||||
import { PrometheusProviderRegistry, PrometheusProvider, PrometheusNodeQuery, PrometheusClusterQuery, PrometheusPodQuery, PrometheusPvcQuery, PrometheusIngressQuery, PrometheusQueryOpts} from "../prometheus/provider-registry"
|
||||
|
||||
export type IMetricsQuery = string | string[] | {
|
||||
[metricName: string]: string;
|
||||
}
|
||||
|
||||
class MetricsRoute extends LensApi {
|
||||
|
||||
public async routeMetrics(request: LensApiRequest) {
|
||||
const { response, cluster} = request
|
||||
const query: IMetricsQuery = request.payload;
|
||||
const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube`
|
||||
const headers = {
|
||||
"Host": `${cluster.id}.localhost:${cluster.port}`,
|
||||
"Content-type": "application/json",
|
||||
}
|
||||
const queryParams: IMetricsQuery = {}
|
||||
request.query.forEach((value: string, key: string) => {
|
||||
queryParams[key] = value
|
||||
})
|
||||
|
||||
let metricsUrl: string
|
||||
let prometheusProvider: PrometheusProvider
|
||||
try {
|
||||
const prometheusPath = await cluster.contextHandler.getPrometheusPath()
|
||||
metricsUrl = `${serverUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range`
|
||||
prometheusProvider = await cluster.contextHandler.getPrometheusProvider()
|
||||
} catch {
|
||||
this.respondJson(response, {})
|
||||
return
|
||||
}
|
||||
// prometheus metrics loader
|
||||
const attempts: { [query: string]: number } = {};
|
||||
const maxAttempts = 5;
|
||||
const loadMetrics = (orgQuery: string): Promise<any> => {
|
||||
const query = orgQuery.trim()
|
||||
const attempt = attempts[query] = (attempts[query] || 0) + 1;
|
||||
return requestPromise(metricsUrl, {
|
||||
resolveWithFullResponse: false,
|
||||
headers: headers,
|
||||
json: true,
|
||||
qs: {
|
||||
query: query,
|
||||
...queryParams
|
||||
}
|
||||
}).catch(async (error) => {
|
||||
if (attempt < maxAttempts && (error.statusCode && error.statusCode != 404)) {
|
||||
await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request
|
||||
return loadMetrics(query);
|
||||
}
|
||||
return {
|
||||
status: error.toString(),
|
||||
data: {
|
||||
result: []
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// return data in same structure as query
|
||||
let data: any;
|
||||
if (typeof query === "string") {
|
||||
data = await loadMetrics(query)
|
||||
}
|
||||
else if (Array.isArray(query)) {
|
||||
data = await Promise.all(query.map(loadMetrics));
|
||||
}
|
||||
else {
|
||||
data = {};
|
||||
const result = await Promise.all(
|
||||
Object.entries(query).map((queryEntry: any) => {
|
||||
const queryName: string = queryEntry[0]
|
||||
const queryOpts: PrometheusQueryOpts = queryEntry[1]
|
||||
const queries = prometheusProvider.getQueries(queryOpts)
|
||||
const q = queries[queryName as keyof (PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery)]
|
||||
return loadMetrics(q)
|
||||
})
|
||||
);
|
||||
Object.keys(query).forEach((metricName, index) => {
|
||||
data[metricName] = result[index];
|
||||
});
|
||||
}
|
||||
|
||||
this.respondJson(response, data)
|
||||
}
|
||||
}
|
||||
|
||||
export const metricsRoute = new MetricsRoute()
|
||||
@@ -0,0 +1,107 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { spawn, ChildProcessWithoutNullStreams } from "child_process"
|
||||
import { bundledKubectl } from "../kubectl"
|
||||
import { getFreePort } from "../port"
|
||||
import { shell } from "electron"
|
||||
import * as tcpPortUsed from "tcp-port-used"
|
||||
import logger from "../logger"
|
||||
|
||||
class PortForward {
|
||||
public static portForwards: PortForward[] = []
|
||||
|
||||
static getPortforward(forward: {clusterId: string; kind: string; name: string; namespace: string; port: string}) {
|
||||
return PortForward.portForwards.find((pf) => {
|
||||
return (
|
||||
pf.clusterId == forward.clusterId &&
|
||||
pf.kind == "service" &&
|
||||
pf.name == forward.name &&
|
||||
pf.namespace == forward.namespace &&
|
||||
pf.port == forward.port
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public clusterId: string
|
||||
public process: ChildProcessWithoutNullStreams
|
||||
public kubeConfig: string
|
||||
public kind: string
|
||||
public namespace: string
|
||||
public name: string
|
||||
public port: string
|
||||
public localPort: number
|
||||
|
||||
constructor(obj: any) {
|
||||
Object.assign(this, obj)
|
||||
}
|
||||
|
||||
public async start() {
|
||||
this.localPort = await getFreePort()
|
||||
const kubectlBin = await bundledKubectl.kubectlPath()
|
||||
const args = [
|
||||
"--kubeconfig", this.kubeConfig,
|
||||
"port-forward",
|
||||
"-n", this.namespace,
|
||||
`service/${this.name}`,
|
||||
`${this.localPort}:${this.port}`
|
||||
]
|
||||
|
||||
this.process = spawn(kubectlBin, args, {
|
||||
env: process.env
|
||||
})
|
||||
PortForward.portForwards.push(this)
|
||||
this.process.on("exit", () => {
|
||||
const index = PortForward.portForwards.indexOf(this)
|
||||
if (index > -1) {
|
||||
PortForward.portForwards.splice(index, 1)
|
||||
}
|
||||
})
|
||||
try {
|
||||
await tcpPortUsed.waitUntilUsed(this.localPort, 500, 3000)
|
||||
return true
|
||||
} catch (error) {
|
||||
this.process.kill()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public open() {
|
||||
shell.openExternal(`http://localhost:${this.localPort}`)
|
||||
}
|
||||
}
|
||||
|
||||
class PortForwardRoute extends LensApi {
|
||||
|
||||
public async routeServicePortForward(request: LensApiRequest) {
|
||||
const { params, response, cluster} = request
|
||||
|
||||
let portForward = PortForward.getPortforward({
|
||||
clusterId: cluster.id, kind: "service", name: params.service,
|
||||
namespace: params.namespace, port: params.port
|
||||
})
|
||||
if (!portForward) {
|
||||
logger.info(`Creating a new port-forward ${params.namespace}/${params.service}:${params.port}`)
|
||||
portForward = new PortForward({
|
||||
clusterId: cluster.id,
|
||||
kind: "service",
|
||||
namespace: params.namespace,
|
||||
name: params.service,
|
||||
port: params.port,
|
||||
kubeConfig: cluster.kubeconfigPath()
|
||||
})
|
||||
const started = await portForward.start()
|
||||
if (!started) {
|
||||
this.respondJson(response, {
|
||||
message: "Failed to open port-forward"
|
||||
}, 400)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
portForward.open()
|
||||
|
||||
this.respondJson(response, {})
|
||||
}
|
||||
}
|
||||
|
||||
export const portForwardRoute = new PortForwardRoute()
|
||||
@@ -0,0 +1,105 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { Watch, KubeConfig } from "@kubernetes/client-node"
|
||||
import { ServerResponse } from "http"
|
||||
import { Request } from "request"
|
||||
import logger from "../logger"
|
||||
|
||||
class ApiWatcher {
|
||||
private apiUrl: string
|
||||
private response: ServerResponse
|
||||
private watchRequest: Request
|
||||
private watch: Watch
|
||||
private processor: NodeJS.Timeout
|
||||
private eventBuffer: any[] = []
|
||||
|
||||
constructor(apiUrl: string, kubeConfig: KubeConfig, response: ServerResponse) {
|
||||
this.apiUrl = apiUrl
|
||||
this.watch = new Watch(kubeConfig)
|
||||
this.response = response
|
||||
}
|
||||
|
||||
public async start() {
|
||||
if (this.processor) {
|
||||
clearInterval(this.processor)
|
||||
}
|
||||
this.processor = setInterval(() => {
|
||||
const events = this.eventBuffer.splice(0)
|
||||
events.map(event => this.sendEvent(event))
|
||||
this.response.flushHeaders()
|
||||
}, 50)
|
||||
this.watchRequest = await this.watch.watch(this.apiUrl, {}, this.watchHandler.bind(this), this.doneHandler.bind(this))
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (!this.watchRequest) { return }
|
||||
|
||||
if (this.processor) {
|
||||
clearInterval(this.processor)
|
||||
}
|
||||
logger.debug("Stopping watcher for api: " + this.apiUrl)
|
||||
this.watchRequest.abort()
|
||||
}
|
||||
|
||||
private watchHandler(phase: string, obj: any) {
|
||||
this.eventBuffer.push({
|
||||
type: phase,
|
||||
object: obj
|
||||
})
|
||||
}
|
||||
|
||||
private doneHandler(error: Error) {
|
||||
if (error) logger.warn("watch ended: " + error.toString())
|
||||
|
||||
this.sendEvent({
|
||||
type: "STREAM_END",
|
||||
url: this.apiUrl,
|
||||
status: 410,
|
||||
})
|
||||
}
|
||||
|
||||
private sendEvent(evt: any) {
|
||||
// convert to "text/event-stream" format
|
||||
this.response.write(`data: ${JSON.stringify(evt)}\n\n`);
|
||||
}
|
||||
}
|
||||
|
||||
class WatchRoute extends LensApi {
|
||||
|
||||
public async routeWatch(request: LensApiRequest) {
|
||||
const { params, response, cluster} = request
|
||||
const apis: string[] = request.query.getAll("api")
|
||||
const watchers: ApiWatcher[] = []
|
||||
|
||||
if (!apis.length) {
|
||||
this.respondJson(response, {
|
||||
message: "Empty request. Query params 'api' are not provided.",
|
||||
example: "?api=/api/v1/pods&api=/api/v1/nodes",
|
||||
}, 400)
|
||||
return
|
||||
}
|
||||
|
||||
response.setHeader("Content-Type", "text/event-stream")
|
||||
response.setHeader("Cache-Control", "no-cache")
|
||||
response.setHeader("Connection", "keep-alive")
|
||||
|
||||
apis.forEach(apiUrl => {
|
||||
const watcher = new ApiWatcher(apiUrl, cluster.contextHandler.kc, response)
|
||||
watcher.start()
|
||||
watchers.push(watcher)
|
||||
})
|
||||
|
||||
request.raw.req.on("close", () => {
|
||||
logger.debug("Watch request closed")
|
||||
watchers.map(watcher => watcher.stop())
|
||||
})
|
||||
|
||||
request.raw.req.on("end", () => {
|
||||
logger.debug("Watch request ended")
|
||||
watchers.map(watcher => watcher.stop())
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const watchRoute = new WatchRoute()
|
||||
Reference in new issue
Block a user