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

add continuation after extension install, add notification of install

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2020-12-09 14:45:05 -05:00
parent 0a44260cd5
commit b6a17eb1be
8 changed files with 97 additions and 46 deletions

View File

@ -337,7 +337,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
}); });
this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null; const curActiveCluster = newClusters.get(activeCluster);
if (!curActiveCluster || curActiveCluster.workspace !== workspaceStore.currentWorkspaceId) {
this.activeCluster = null;
} else {
this.activeCluster = activeCluster;
}
this.clusters.replace(newClusters); this.clusters.replace(newClusters);
this.removedClusters.replace(removedClusters); this.removedClusters.replace(removedClusters);
} }

View File

@ -131,6 +131,7 @@ app
event.preventDefault(); event.preventDefault();
// Broadcast "open-url" events to renderer // Broadcast "open-url" events to renderer
console.log(`broadcasting: ${rawUrl}`);
broadcastMessage(lensProtocolChannel, { rawUrl }); broadcastMessage(lensProtocolChannel, { rawUrl });
}); });

View File

@ -10,6 +10,7 @@ import { Router } from "./router";
import { ClusterManager } from "./cluster-manager"; import { ClusterManager } from "./cluster-manager";
import { ContextHandler } from "./context-handler"; import { ContextHandler } from "./context-handler";
import logger from "./logger"; import logger from "./logger";
import { chunk } from "lodash";
export class LensProxy { export class LensProxy {
protected origin: string; protected origin: string;
@ -65,10 +66,11 @@ export class LensProxy {
return spdyProxy; return spdyProxy;
} }
private static readonly ProxyFilterHeaders = new Set(["Host", "Authorization"]);
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) { protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
const cluster = this.clusterManager.getClusterForRequest(req); const cluster = this.clusterManager.getClusterForRequest(req);
if (cluster) { if (cluster?.initialized) {
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, ""); const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
const apiUrl = url.parse(cluster.apiUrl); const apiUrl = url.parse(cluster.apiUrl);
const pUrl = url.parse(proxyUrl); const pUrl = url.parse(proxyUrl);
@ -79,13 +81,14 @@ export class LensProxy {
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`); proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
proxySocket.write(`Host: ${apiUrl.host}\r\n`); proxySocket.write(`Host: ${apiUrl.host}\r\n`);
for (let i = 0; i < req.rawHeaders.length; i += 2) { for (const [key, value] of chunk(req.rawHeaders, 2)) {
const key = req.rawHeaders[i]; if (LensProxy.ProxyFilterHeaders.has(key)) {
continue;
if (key !== "Host" && key !== "Authorization") {
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`);
} }
proxySocket.write(`${key}: ${value}\r\n`);
} }
proxySocket.write("\r\n"); proxySocket.write("\r\n");
proxySocket.write(head); proxySocket.write(head);
}); });
@ -182,13 +185,19 @@ export class LensProxy {
const cluster = this.clusterManager.getClusterForRequest(req); const cluster = this.clusterManager.getClusterForRequest(req);
if (cluster) { if (cluster) {
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler); if (cluster.initialized) {
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
if (proxyTarget) { if (proxyTarget) {
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port" // allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
res.setHeader("Access-Control-Allow-Origin", this.origin); res.setHeader("Access-Control-Allow-Origin", this.origin);
return proxy.web(req, res, proxyTarget); return proxy.web(req, res, proxyTarget);
}
} else {
res.statusCode = 404;
return res.end("Not initialized");
} }
} }
this.router.route(cluster, req, res); this.router.route(cluster, req, res);

View File

@ -60,19 +60,23 @@ interface ExtensionUrlMatch {
export class LensProtocolRouter extends Singleton { export class LensProtocolRouter extends Singleton {
private extentionRoutes = new Map<ExtensionId, Map<string, RouteHandler>>(); private extentionRoutes = new Map<ExtensionId, Map<string, RouteHandler>>();
private internalRoutes = new Map<string, RouteHandler>(); private internalRoutes = new Map<string, RouteHandler>();
private missingExtensionHandler?: (name: string) => Promise<void>;
private missingExtensionHandler?: (name: string) => Promise<boolean>;
private static readonly LoggingPrefix = "[PROTOCOL ROUTER]";
private static ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}/:${EXTENSION_NAME_MATCH}`; private static ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}/:${EXTENSION_NAME_MATCH}`;
public init() { public init() {
subscribeToBroadcast(lensProtocolChannel, ((_event, { rawUrl }) => { subscribeToBroadcast(lensProtocolChannel, ((_event, { rawUrl }) => {
console.log(`receiving: ${rawUrl}`);
try { try {
this.route(Url(rawUrl, true)); this.route(Url(rawUrl, true));
} catch (error) { } catch (error) {
if (error instanceof RoutingError) { if (error instanceof RoutingError) {
logger.error(`[PROTOCOL ROUTER]: ${error}`, { url: error.url }); logger.error(`${LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url });
} else { } else {
logger.error(`[PROTOCOL ROUTER]: ${error}`, { rawUrl }); logger.error(`${LensProtocolRouter.LoggingPrefix}: ${error}`, { rawUrl });
} }
} }
})); }));
@ -81,7 +85,7 @@ export class LensProtocolRouter extends Singleton {
/** /**
* route * route
*/ */
public route(url: Url): void { public async route(url: Url): Promise<void> {
if (url.protocol.toLowerCase() !== "lens:") { if (url.protocol.toLowerCase() !== "lens:") {
throw new RoutingError(RoutingErrorType.INVALID_PROTOCOL, url); throw new RoutingError(RoutingErrorType.INVALID_PROTOCOL, url);
} }
@ -90,8 +94,7 @@ export class LensProtocolRouter extends Singleton {
case "internal": case "internal":
return this._route(this.internalRoutes, url); return this._route(this.internalRoutes, url);
case "extension": case "extension":
// Possible rejected promise is ignored return this._routeToExtension(url);
this._routeToExtension(url);
default: default:
throw new RoutingError(RoutingErrorType.INVALID_HOST, url); throw new RoutingError(RoutingErrorType.INVALID_HOST, url);
@ -108,17 +111,23 @@ export class LensProtocolRouter extends Singleton {
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params; const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
const name = `${publisher}/${partialName}`; const name = `${publisher}/${partialName}`;
logger.info(`[PROTOCOL ROUTER] Extension ${name} matched`); logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched`);
const routes = this.extentionRoutes.get(name); let routes = this.extentionRoutes.get(name);
if (!routes) { if (!routes) {
if (this.missingExtensionHandler) { if (this.missingExtensionHandler) {
await this.missingExtensionHandler(name); if (!await this.missingExtensionHandler(name)) {
return;
}
// TODO: After installation we can continue to route to the extension.. routes = this.extentionRoutes.get(name);
// but this is difficult, since the promise resolves before extension installation is complete.
return; if (!routes) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but has no routes`);
return;
}
} else { } else {
throw new RoutingError(RoutingErrorType.MISSING_EXTENSION, url); throw new RoutingError(RoutingErrorType.MISSING_EXTENSION, url);
} }
@ -146,6 +155,8 @@ export class LensProtocolRouter extends Singleton {
throw new RoutingError(RoutingErrorType.NO_HANDLER, url); throw new RoutingError(RoutingErrorType.NO_HANDLER, url);
} }
logger.info(`${LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`);
const [match, handler] = route; const [match, handler] = route;
delete match.params[EXTENSION_NAME_MATCH]; delete match.params[EXTENSION_NAME_MATCH];
@ -174,7 +185,11 @@ export class LensProtocolRouter extends Singleton {
this.extentionRoutes.get(id).set(urlSchema, handler); this.extentionRoutes.get(id).set(urlSchema, handler);
} }
public onMissingExtension(handler: (name: string) => Promise<void>) { /**
* onMissingExtension registers the handler for when an extension is missing
* @param handler If the called handler resolves to true then the routes will be tried again
*/
public onMissingExtension(handler: (name: string) => Promise<boolean>) {
this.missingExtensionHandler = handler; this.missingExtensionHandler = handler;
} }
} }

View File

@ -1,6 +1,7 @@
import logger from "../../../main/logger"; import logger from "../../../main/logger";
import { RouteParams } from "../../../main/protocol-handler"; import { RouteParams } from "../../../main/protocol-handler";
import { installFromNpm } from "../../components/+extensions"; import { extensionsURL, installFromNpm } from "../../components/+extensions";
import { navigate } from "../../navigation";
export async function installExtension(params: RouteParams): Promise<void> { export async function installExtension(params: RouteParams): Promise<void> {
const { name } = params.search; const { name } = params.search;
@ -9,7 +10,9 @@ export async function installExtension(params: RouteParams): Promise<void> {
return void logger.info("installExtension handler: missing 'name' from search params"); return void logger.info("installExtension handler: missing 'name' from search params");
} }
try { try {
navigate(extensionsURL());
await installFromNpm(name); await installFromNpm(name);
} catch (error) { } catch (error) {
logger.error("[PH - Install Extension]: failed to install from NPM", error); logger.error("[PH - Install Extension]: failed to install from NPM", error);

View File

@ -45,12 +45,15 @@ export async function bootstrap(App: AppComponent) {
lensProtocolRouter.init(); lensProtocolRouter.init();
lensProtocolRouter.onMissingExtension(async name => { lensProtocolRouter.onMissingExtension(async name => {
if (!extensionLoader.isInstalled(name)) { if (!extensionLoader.isInstalled(name)) {
logger.info(`[PROTOCOL ROUTER] Extension ${name} not installed, installing..`); logger.info(`[PROTOCOL ROUTER]: Extension ${name} not installed, installing..`);
// TODO: This actually resolves before the extension installation is complete await installFromNpm(name);
return installFromNpm(name);
return true;
} else { } else {
logger.info(`[PROTOCOL ROUTER] Extension already installed, but route is missing.`); logger.info(`[PROTOCOL ROUTER]: Extension already installed, but route is missing.`);
return false;
} }
}); });
protocolEndpoints.registerHandlers(); protocolEndpoints.registerHandlers();

View File

@ -165,7 +165,7 @@ async function requestInstall(init: InstallRequest | InstallRequest[]) {
if (!folderExists) { if (!folderExists) {
// auto-install extension if not yet exists // auto-install extension if not yet exists
unpackExtension(install); return unpackExtension(install);
} else { } else {
// If we show the confirmation dialog, we stop the install spinner until user clicks ok // If we show the confirmation dialog, we stop the install spinner until user clicks ok
// and the install continues // and the install continues
@ -210,7 +210,11 @@ async function unpackExtension({ fileName, tempFile, manifest: { name, version }
try { try {
// extract to temp folder first // extract to temp folder first
await fse.remove(unpackingTempFolder).catch(Function); try {
await fse.remove(unpackingTempFolder);
} catch (err) {
// ignore error
}
await fse.ensureDir(unpackingTempFolder); await fse.ensureDir(unpackingTempFolder);
await extractTar(tempFile, { cwd: unpackingTempFolder }); await extractTar(tempFile, { cwd: unpackingTempFolder });
@ -237,8 +241,12 @@ async function unpackExtension({ fileName, tempFile, manifest: { name, version }
} }
} finally { } finally {
// clean up // clean up
fse.remove(unpackingTempFolder).catch(Function); try {
fse.unlink(tempFile).catch(Function); await fse.remove(unpackingTempFolder);
await fse.unlink(tempFile);
} catch (err) {
// ignore error
}
} }
} }
@ -352,6 +360,8 @@ async function installFromSelectFileDialog() {
export async function installFromNpm(packageName: string) { export async function installFromNpm(packageName: string) {
const tarballUrl = await extensionLoader.getNpmPackageTarballUrl(packageName); const tarballUrl = await extensionLoader.getNpmPackageTarballUrl(packageName);
Notifications.info(`Installing ${packageName}`);
return installFromUrlOrPath(tarballUrl); return installFromUrlOrPath(tarballUrl);
} }

View File

@ -108,25 +108,28 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
* @param params request parameters described in IPodLogsQuery interface * @param params request parameters described in IPodLogsQuery interface
* @returns {Promise} A fetch request promise * @returns {Promise} A fetch request promise
*/ */
loadLogs = async (tabId: TabId, params: Partial<IPodLogsQuery>) => { loadLogs = async (tabId: TabId, params: Partial<IPodLogsQuery>): Promise<string[]> => {
const data = this.getData(tabId); const data = this.getData(tabId);
const { selectedContainer, previous } = data; const { selectedContainer, previous } = data;
const pod = new Pod(data.pod); const pod = new Pod(data.pod);
const namespace = pod.getNs(); const namespace = pod.getNs();
const name = pod.getName(); const name = pod.getName();
return podsApi.getLogs({ namespace, name }, { try {
...params, const result = await podsApi.getLogs({ namespace, name }, {
timestamps: true, // Always setting timestampt to separate old logs from new ones ...params,
container: selectedContainer.name, timestamps: true, // Always setting timestampt to separate old logs from new ones
previous container: selectedContainer.name,
}).then(result => { previous
const logs = [...result.split("\n")]; // Transform them into array });
const logs = result.split(/(\r?\n)+/g);
logs.pop(); // Remove last empty element logs.pop();// Remove last empty element
return logs; return logs;
}); } catch (err) {
// ignore error
}
}; };
/** /**
@ -145,7 +148,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
* @returns {number} Length of log lines * @returns {number} Length of log lines
*/ */
@computed @computed
get lines() { get lines(): number {
const id = dockStore.selectedTabId; const id = dockStore.selectedTabId;
const logs = this.logs.get(id); const logs = this.logs.get(id);