From 39cc6d8acfa35d9a8bf8567fe23509f4d94bbfc9 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 3 Dec 2021 10:38:04 -0500 Subject: [PATCH] Add support for customizing the extension install registry URL Signed-off-by: Sebastian Malton --- src/common/user-store/preferences-helpers.ts | 31 +++++++++ src/common/user-store/user-store.ts | 5 +- src/common/utils/index.ts | 1 + .../attempt-install-by-info.tsx | 41 +++++++----- .../components/+extensions/extensions.tsx | 2 +- .../get-base-registry-url.injectable.ts | 36 ++++++++++ .../get-base-registry-url.tsx | 57 ++++++++++++++++ .../components/+preferences/application.tsx | 65 +++++++++++++++---- 8 files changed, 205 insertions(+), 33 deletions(-) create mode 100644 src/renderer/components/+extensions/get-base-registry-url/get-base-registry-url.injectable.ts create mode 100644 src/renderer/components/+extensions/get-base-registry-url/get-base-registry-url.tsx diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts index 1f6050017e..450c75ae89 100644 --- a/src/common/user-store/preferences-helpers.ts +++ b/src/common/user-store/preferences-helpers.ts @@ -306,6 +306,36 @@ const updateChannel: PreferenceDescription = { }, }; +export enum ExtensionRegistryLocation { + DEFAULT = "default", + NPMRC = "npmrc", + CUSTOM = "custom", +} +export type ExtensionRegistry = { + location: ExtensionRegistryLocation.DEFAULT | ExtensionRegistryLocation.NPMRC; + customUrl?: undefined; +} | { + location: ExtensionRegistryLocation.CUSTOM, + customUrl: string; +}; + +export const defaultExtensionRegistryUrl = "https://registry.npmjs.org"; + +const extensionRegistryUrl: PreferenceDescription = { + fromStore(val) { + return val ?? { + location: ExtensionRegistryLocation.DEFAULT, + }; + }, + toStore(val) { + if (val.location === ExtensionRegistryLocation.DEFAULT) { + return undefined; + } + + return val; + }, +}; + type PreferencesModelType = typeof DESCRIPTORS[field] extends PreferenceDescription ? T : never; type UserStoreModelType = typeof DESCRIPTORS[field] extends PreferenceDescription ? T : never; @@ -335,6 +365,7 @@ export const DESCRIPTORS = { editorConfiguration, terminalCopyOnSelect, updateChannel, + extensionRegistryUrl, }; export const CONSTANTS = { diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index 3de94113d6..31238f0ded 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -29,7 +29,7 @@ import { kubeConfigDefaultPath } from "../kube-helpers"; import { appEventBus } from "../event-bus"; import path from "path"; import { ObservableToggleSet, toJS } from "../../renderer/utils"; -import { DESCRIPTORS, EditorConfiguration, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers"; +import { DESCRIPTORS, EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers"; import logger from "../../main/logger"; import { AppPaths } from "../app-paths"; @@ -75,6 +75,7 @@ export class UserStore extends BaseStore /* implements UserStore @observable kubectlBinariesPath?: string; @observable terminalCopyOnSelect: boolean; @observable updateChannel?: string; + @observable extensionRegistryUrl: ExtensionRegistry; /** * Download kubectl binaries matching cluster version @@ -201,6 +202,7 @@ export class UserStore extends BaseStore /* implements UserStore this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration); this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect); this.updateChannel = DESCRIPTORS.updateChannel.fromStore(preferences?.updateChannel); + this.extensionRegistryUrl = DESCRIPTORS.extensionRegistryUrl.fromStore(preferences?.extensionRegistryUrl); } toJSON(): UserStoreModel { @@ -224,6 +226,7 @@ export class UserStore extends BaseStore /* implements UserStore editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration), terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect), updateChannel: DESCRIPTORS.updateChannel.toStore(this.updateChannel), + extensionRegistryUrl: DESCRIPTORS.extensionRegistryUrl.toStore(this.extensionRegistryUrl), }, }; diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 733867f1cb..ea2f742bc7 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -48,6 +48,7 @@ export * from "./n-fircate"; export * from "./objects"; export * from "./openExternal"; export * from "./paths"; +export * from "./promise-exec"; export * from "./reject-promise"; export * from "./singleton"; export * from "./sort-compare"; diff --git a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx b/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx index bc5f54c147..c898fa0cc0 100644 --- a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx +++ b/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx @@ -36,32 +36,39 @@ export interface ExtensionInfo { } export interface Dependencies { - attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise + attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise; + getBaseRegistryUrl: () => Promise; } -export const attemptInstallByInfo = ({ attemptInstall }: Dependencies) => async ({ +export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl }: Dependencies) => async ({ name, version, requireConfirmation = false, }: ExtensionInfo) => { const disposer = ExtensionInstallationStateStore.startPreInstall(); - const registryUrl = new URLParse("https://registry.npmjs.com") - .set("pathname", name) - .toString(); - const { promise } = downloadJson({ url: registryUrl }); - const json = await promise.catch(console.error); + const baseUrl = await getBaseRegistryUrl(); + const registryUrl = new URLParse(baseUrl).set("pathname", name).toString(); + let json: any; - if ( - !json || - json.error || - typeof json.versions !== "object" || - !json.versions - ) { - const message = json?.error ? `: ${json.error}` : ""; + try { + json = await downloadJson({ url: registryUrl }).promise; - Notifications.error( - `Failed to get registry information for that extension${message}`, - ); + if (!json || json.error || typeof json.versions !== "object" || !json.versions) { + const message = json?.error ? `: ${json.error}` : ""; + + Notifications.error(`Failed to get registry information for that extension${message}`); + + return disposer(); + } + } catch (error) { + if (error instanceof SyntaxError) { + // assume invalid JSON + console.warn("Set registry has invalid json", { url: baseUrl }, error); + Notifications.error("Failed to get valid registry information for that extension. Registry did not return valid JSON"); + } else { + console.error("Failed to download registry information", error); + Notifications.error(`Failed to get valid registry information for that extension. ${error}`); + } return disposer(); } diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 10438e4aca..2ce17304dc 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -69,7 +69,7 @@ class NonInjectedExtensions extends React.Component { super(props); makeObservable(this); } - + get dependencies() { return this.props.dependencies; } diff --git a/src/renderer/components/+extensions/get-base-registry-url/get-base-registry-url.injectable.ts b/src/renderer/components/+extensions/get-base-registry-url/get-base-registry-url.injectable.ts new file mode 100644 index 0000000000..7f46d4a7b8 --- /dev/null +++ b/src/renderer/components/+extensions/get-base-registry-url/get-base-registry-url.injectable.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { Injectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { UserStore } from "../../../../common/user-store"; +import { Dependencies, getBaseRegistryUrl } from "./get-base-registry-url"; + +const getBaseRegistryUrlInjectable: Injectable<() => Promise, Dependencies> = { + getDependencies: () => ({ + // TODO: use injection + getRegistryUrlPreference: () => UserStore.getInstance().extensionRegistryUrl, + }), + + instantiate: getBaseRegistryUrl, + lifecycle: lifecycleEnum.singleton, +}; + +export default getBaseRegistryUrlInjectable; diff --git a/src/renderer/components/+extensions/get-base-registry-url/get-base-registry-url.tsx b/src/renderer/components/+extensions/get-base-registry-url/get-base-registry-url.tsx new file mode 100644 index 0000000000..0d012ccf55 --- /dev/null +++ b/src/renderer/components/+extensions/get-base-registry-url/get-base-registry-url.tsx @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import React from "react"; +import { defaultExtensionRegistryUrl, ExtensionRegistry, ExtensionRegistryLocation } from "../../../../common/user-store/preferences-helpers"; +import { promiseExecFile } from "../../../utils"; +import { Notifications } from "../../notifications"; + +export interface Dependencies { + getRegistryUrlPreference: () => ExtensionRegistry, +} + +export const getBaseRegistryUrl = ({ getRegistryUrlPreference }: Dependencies) => async () => { + const extensionRegistryUrl = getRegistryUrlPreference(); + + switch (extensionRegistryUrl.location) { + case ExtensionRegistryLocation.CUSTOM: + return extensionRegistryUrl.customUrl; + + case ExtensionRegistryLocation.NPMRC: { + try { + const filteredEnv = Object.fromEntries( + Object.entries(process.env) + .filter(([key]) => !key.startsWith("npm")), + ); + const { stdout } = await promiseExecFile("npm", ["config", "get", "registry"], { env: filteredEnv }); + + return stdout.trim(); + } catch (error) { + Notifications.error(

Failed to get configured registry from .npmrc. Falling back to default registry

); + console.warn("[EXTENSIONS]: failed to get configured registry from .npmrc", error); + // fallthrough + } + } + default: + case ExtensionRegistryLocation.DEFAULT: + return defaultExtensionRegistryUrl; + } +}; diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx index 0fd1087953..40859cd5e0 100644 --- a/src/renderer/components/+preferences/application.tsx +++ b/src/renderer/components/+preferences/application.tsx @@ -29,7 +29,9 @@ import { Input } from "../input"; import { isWindows } from "../../../common/vars"; import { FormSwitch, Switcher } from "../switch"; import moment from "moment-timezone"; -import { CONSTANTS } from "../../../common/user-store/preferences-helpers"; +import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers"; +import { action } from "mobx"; +import { isUrl } from "../input/input_validators"; import { AppPreferenceRegistry } from "../../../extensions/registries"; import { ExtensionSettings } from "./extension-settings"; @@ -43,6 +45,7 @@ const updateChannelOptions: SelectOption[] = Array.from( ); export const Application = observer(() => { + const userStore = UserStore.getInstance(); const defaultShell = process.env.SHELL || process.env.PTYSHELL || ( @@ -51,7 +54,8 @@ export const Application = observer(() => { : "System default shell" ); - const [shell, setShell] = React.useState(UserStore.getInstance().shell || ""); + const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || ""); + const [shell, setShell] = React.useState(userStore.shell || ""); const extensionSettings = AppPreferenceRegistry.getInstance().getItems().filter((preference) => preference.showInPreferencesTab === "application"); return ( @@ -61,8 +65,8 @@ export const Application = observer(() => { { + userStore.extensionRegistryUrl.location = value; + + if (userStore.extensionRegistryUrl.location === ExtensionRegistryLocation.CUSTOM) { + userStore.extensionRegistryUrl.customUrl = ""; + } + })} + themeName="lens" + /> +

+ This setting is to change the registry URL for installing extensions by name.{" "} + If you are unable to access the default registry ({defaultExtensionRegistryUrl}){" "} + you can change it in your .npmrc file or in the input below. +

+ + userStore.extensionRegistryUrl.customUrl = customUrl} + placeholder="Custom Extension Registry URL..." + disabled={userStore.extensionRegistryUrl.location !== ExtensionRegistryLocation.CUSTOM} + /> + + +
+
UserStore.getInstance().openAtLogin = v.target.checked} + checked={userStore.openAtLogin} + onChange={v => userStore.openAtLogin = v.target.checked} name="startup" /> } @@ -120,8 +157,8 @@ export const Application = observer(() => { UserStore.getInstance().setLocaleTimezone(value)} + value={userStore.localeTimezone} + onChange={({ value }) => userStore.setLocaleTimezone(value)} themeName="lens" />