import fs from "fs"; import path from "path" import hb from "handlebars" import { ResourceApplier } from "./resource-applier" import { CoreV1Api, KubeConfig, Watch } from "@kubernetes/client-node" import { Cluster } from "./cluster"; import logger from "./logger"; export type FeatureStatusMap = Record export interface FeatureInstallRequest { clusterId: string; name: string; config?: any; } export interface FeatureStatus { currentVersion: string; installed: boolean; latestVersion: string; canUpgrade: boolean; } export abstract class Feature { name: string; latestVersion: string; abstract async upgrade(cluster: Cluster): Promise; abstract async uninstall(cluster: Cluster): Promise; abstract async featureStatus(kc: KubeConfig): Promise; constructor(public config: any) { } async install(cluster: Cluster): Promise { const resources = this.renderTemplates(); try { await new ResourceApplier(cluster).kubectlApplyAll(resources); return true; } catch (err) { logger.error("Installing feature error", { err, cluster }); return false } } protected async deleteNamespace(kc: KubeConfig, name: string) { return new Promise(async (resolve, reject) => { const client = kc.makeApiClient(CoreV1Api) const result = await client.deleteNamespace("lens-metrics", 'false', undefined, undefined, undefined, "Foreground"); const nsVersion = result.body.metadata.resourceVersion; const nsWatch = new Watch(kc); const query: Record = { resourceVersion: nsVersion, fieldSelector: "metadata.name=lens-metrics", } const req = await nsWatch.watch('/api/v1/namespaces', query, (phase, obj) => { if (phase === 'DELETED') { logger.debug(`namespace ${name} finally gone`) req.abort(); resolve() } }, (err?: any) => { if (err) reject(err); }); }); } protected renderTemplates(): string[] { const resources: string[] = []; fs.readdirSync(this.manifestPath()).forEach(filename => { const file = path.join(this.manifestPath(), filename); const raw = fs.readFileSync(file); if (filename.endsWith('.hb')) { const template = hb.compile(raw.toString()); resources.push(template(this.config)); } else { resources.push(raw.toString()); } }); return resources; } protected manifestPath() { const devPath = path.join(__dirname, "..", 'src/features', this.name); if (fs.existsSync(devPath)) { return devPath; } return path.join(__dirname, "..", 'features', this.name); } }