Merge remote-tracking branch 'origin/master' into feature/ingress_class_api_view

# Conflicts:
#	src/common/rbac.ts
This commit is contained in:
Roman committed 2022-12-27 12:55:11 +02:00
commit 4c44b76f24
363 files changed
+3866 -5580

No files matched your search

@@ -5,7 +5,8 @@
import { getInjectionToken } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import type { KubeApiResourceDescriptor } from "../rbac";
export const allowedResourcesInjectionToken = getInjectionToken<IComputedValue<Set<string>>>({
id: "allowed-resources",
export const shouldShowResourceInjectionToken = getInjectionToken<IComputedValue<boolean>, KubeApiResourceDescriptor>({
id: "should-show-resource",
});
@@ -1,87 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeConfig } from "@kubernetes/client-node";
import { AuthorizationV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable";
import type { KubeApiResource } from "../rbac";
/**
* Requests the permissions for actions on the kube cluster
* @param namespace The namespace of the resources
* @param availableResources List of available resources in the cluster to resolve glob values fir api groups
* @returns list of allowed resources names
*/
export type RequestNamespaceResources = (namespace: string, availableResources: KubeApiResource[]) => Promise<string[]>;
/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
export type AuthorizationNamespaceReview = (proxyConfig: KubeConfig) => RequestNamespaceResources;
interface Dependencies {
logger: Logger;
}
const authorizationNamespaceReview = ({ logger }: Dependencies): AuthorizationNamespaceReview => {
return (proxyConfig) => {
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
return async (namespace, availableResources) => {
try {
const { body } = await api.createSelfSubjectRulesReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectRulesReview",
spec: { namespace },
});
const resources = new Set<string>();
body.status?.resourceRules.forEach(resourceRule => {
if (!resourceRule.verbs.some(verb => ["*", "list"].includes(verb)) || !resourceRule.resources) {
return;
}
const apiGroups = resourceRule.apiGroups;
if (resourceRule.resources.length === 1 && resourceRule.resources[0] === "*" && apiGroups) {
if (apiGroups[0] === "*") {
availableResources.forEach(resource => resources.add(resource.apiName));
} else {
availableResources.forEach((apiResource)=> {
if (apiGroups.includes(apiResource.group || "")) {
resources.add(apiResource.apiName);
}
});
}
} else {
resourceRule.resources.forEach(resource => resources.add(resource));
}
});
return [...resources];
} catch (error) {
logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review: ${error}`, { namespace });
return [];
}
};
};
};
const authorizationNamespaceReviewInjectable = getInjectable({
id: "authorization-namespace-review",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return authorizationNamespaceReview({ logger });
},
});
export default authorizationNamespaceReviewInjectable;
+41 -88
View File
@@ -9,8 +9,8 @@ import type { KubeConfig } from "@kubernetes/client-node";
import { HttpError } from "@kubernetes/client-node";
import type { Kubectl } from "../../main/kubectl/kubectl";
import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager";
import type { KubeApiResource, KubeResource } from "../rbac";
import { apiResourceRecord, apiResources } from "../rbac";
import type { KubeApiResource, KubeApiResourceDescriptor } from "../rbac";
import { formatKubeApiResource } from "../rbac";
import type { VersionDetector } from "../../main/cluster-detectors/version-detector";
import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
import plimit from "p-limit";
@@ -25,8 +25,8 @@ import assert from "assert";
import type { Logger } from "../logger";
import type { BroadcastMessage } from "../ipc/broadcast-message.injectable";
import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.injectable";
import type { RequestNamespaceResources } from "./authorization-namespace-review.injectable";
import type { RequestListApiResources } from "./list-api-resources.injectable";
import type { CanListResource, RequestNamespaceListPermissions, RequestNamespaceListPermissionsFor } from "./request-namespace-list-permissions.injectable";
import type { RequestApiResources } from "../../main/cluster/request-api-resources.injectable";
export interface ClusterDependencies {
readonly directoryForKubeConfigs: string;
@@ -36,8 +36,8 @@ export interface ClusterDependencies {
createContextHandler: (cluster: Cluster) => ClusterContextHandler;
createKubectl: (clusterVersion: string) => Kubectl;
createAuthorizationReview: (config: KubeConfig) => CanI;
createAuthorizationNamespaceReview: (config: KubeConfig) => RequestNamespaceResources;
createListApiResources: (cluster: Cluster) => RequestListApiResources;
requestApiResources: RequestApiResources;
requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor;
createListNamespaces: (config: KubeConfig) => ListNamespaces;
createVersionDetector: (cluster: Cluster) => VersionDetector;
broadcastMessage: BroadcastMessage;
@@ -49,7 +49,7 @@ export interface ClusterDependencies {
*
* @beta
*/
export class Cluster implements ClusterModel, ClusterState {
export class Cluster implements ClusterModel {
/** Unique id for a cluster */
public readonly id: ClusterId;
private kubeCtl: Kubectl | undefined;
@@ -62,7 +62,6 @@ export class Cluster implements ClusterModel, ClusterState {
protected readonly _proxyKubeconfigManager: KubeconfigManager | undefined;
protected readonly eventsDisposer = disposer();
protected activated = false;
private readonly resourceAccessStatuses = new Map<KubeApiResource, boolean>();
public get contextHandler() {
// TODO: remove these once main/renderer are seperate classes
@@ -163,25 +162,21 @@ export class Cluster implements ClusterModel, ClusterState {
* @observable
*/
@observable metadata: ClusterMetadata = {};
/**
* List of allowed namespaces verified via K8S::SelfSubjectAccessReview api
*
* @observable
*/
@observable allowedNamespaces: string[] = [];
/**
* List of allowed resources
*
* @observable
* @internal
*/
@observable allowedResources: string[] = [];
readonly allowedNamespaces = observable.array<string>();
/**
* List of accessible namespaces provided by user in the Cluster Settings
*
* @observable
*/
@observable accessibleNamespaces: string[] = [];
readonly accessibleNamespaces = observable.array<string>();
private readonly knownResources = observable.array<KubeApiResource>();
// The formatting of this is `group.name` or `name` (if in core)
private readonly allowedResources = observable.set<string>();
/**
* Labels for the catalog entity
@@ -299,7 +294,7 @@ export class Cluster implements ClusterModel, ClusterState {
}
if (model.accessibleNamespaces) {
this.accessibleNamespaces = model.accessibleNamespaces;
this.accessibleNamespaces.replace(model.accessibleNamespaces);
}
if (model.labels) {
@@ -433,8 +428,7 @@ export class Cluster implements ClusterModel, ClusterState {
this.accessible = false;
this.ready = false;
this.activated = false;
this.allowedNamespaces = [];
this.resourceAccessStatuses.clear();
this.allowedNamespaces.clear();
this.dependencies.logger.info(`[CLUSTER]: disconnected`, { id: this.id });
}
@@ -474,8 +468,7 @@ export class Cluster implements ClusterModel, ClusterState {
this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.getMeta());
const proxyConfig = await this.getProxyKubeconfig();
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
const requestNamespaceResources = this.dependencies.createAuthorizationNamespaceReview(proxyConfig);
const listApiResources = this.dependencies.createListApiResources(this);
const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig);
this.isAdmin = await canI({
namespace: "kube-system",
@@ -486,8 +479,9 @@ export class Cluster implements ClusterModel, ClusterState {
verb: "watch",
resource: "*",
});
this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig);
this.allowedResources = await this.getAllowedResources(listApiResources, requestNamespaceResources);
this.allowedNamespaces.replace(await this.requestAllowedNamespaces(proxyConfig));
this.knownResources.replace(await this.dependencies.requestApiResources(this));
this.allowedResources.replace(await this.getAllowedResources(requestNamespaceListPermissions));
this.ready = true;
}
@@ -600,7 +594,7 @@ export class Cluster implements ClusterModel, ClusterState {
accessible: this.accessible,
isAdmin: this.isAdmin,
allowedNamespaces: this.allowedNamespaces,
allowedResources: this.allowedResources,
allowedResources: [...this.allowedResources],
isGlobalWatchEnabled: this.isGlobalWatchEnabled,
});
}
@@ -611,8 +605,8 @@ export class Cluster implements ClusterModel, ClusterState {
*/
@action setState(state: ClusterState) {
this.accessible = state.accessible;
this.allowedNamespaces = state.allowedNamespaces;
this.allowedResources = state.allowedResources;
this.allowedNamespaces.replace(state.allowedNamespaces);
this.allowedResources.replace(state.allowedResources);
this.apiUrl = state.apiUrl;
this.disconnected = state.disconnected;
this.isAdmin = state.isAdmin;
@@ -644,7 +638,7 @@ export class Cluster implements ClusterModel, ClusterState {
this.dependencies.broadcastMessage(`cluster:${this.id}:connection-update`, update);
}
protected async getAllowedNamespaces(proxyConfig: KubeConfig) {
protected async requestAllowedNamespaces(proxyConfig: KubeConfig) {
if (this.accessibleNamespaces.length) {
return this.accessibleNamespaces;
}
@@ -668,69 +662,28 @@ export class Cluster implements ClusterModel, ClusterState {
}
}
protected async getAllowedResources(listApiResources:RequestListApiResources, requestNamespaceResources: RequestNamespaceResources) {
protected async getAllowedResources(requestNamespaceListPermissions: RequestNamespaceListPermissions) {
if (!this.allowedNamespaces.length) {
return [];
}
try {
if (!this.allowedNamespaces.length) {
return [];
}
const apiLimit = plimit(5); // 5 concurrent api requests
const canListResourceCheckers = await Promise.all((
this.allowedNamespaces.map(namespace => apiLimit(() => requestNamespaceListPermissions(namespace)))
));
const canListNamespacedResource: CanListResource = (resource) => canListResourceCheckers.some(fn => fn(resource));
const unknownResources = new Map<string, KubeApiResource>(apiResources.map(resource => ([resource.apiName, resource])));
const availableResources = await listApiResources();
const availableResourcesNames = new Set(availableResources.map(apiResource => apiResource.apiName));
[...unknownResources.values()].map(unknownResource => {
if (!availableResourcesNames.has(unknownResource.apiName)) {
this.resourceAccessStatuses.set(unknownResource, false);
unknownResources.delete(unknownResource.apiName);
}
});
if (unknownResources.size > 0) {
const apiLimit = plimit(5); // 5 concurrent api requests
await Promise.all(this.allowedNamespaces.map(namespace => apiLimit(async () => {
if (unknownResources.size === 0) {
return;
}
const namespaceResources = await requestNamespaceResources(namespace, availableResources);
for (const resourceName of namespaceResources) {
const unknownResource = unknownResources.get(resourceName);
if (unknownResource) {
this.resourceAccessStatuses.set(unknownResource, true);
unknownResources.delete(resourceName);
}
}
})));
for (const forbiddenResource of unknownResources.values()) {
this.resourceAccessStatuses.set(forbiddenResource, false);
}
}
return apiResources
.filter((resource) => this.resourceAccessStatuses.get(resource))
.map(apiResource => apiResource.apiName);
return this.knownResources
.filter(canListNamespacedResource)
.map(formatKubeApiResource);
} catch (error) {
return [];
}
}
isAllowedResource(kind: string): boolean {
if ((kind as KubeResource) in apiResourceRecord) {
return this.allowedResources.includes(kind);
}
const apiResource = apiResources.find(resource => resource.kind === kind);
if (apiResource) {
return this.allowedResources.includes(apiResource.apiName);
}
return true; // allowed by default for other resources
shouldShowResource(resource: KubeApiResourceDescriptor): boolean {
return this.allowedResources.has(formatKubeApiResource(resource));
}
isMetricHidden(resource: ClusterMetricsResourceType): boolean {
-20
View File
@@ -1,20 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeResource } from "../rbac";
import { apiResourceRecord, apiResources } from "../rbac";
export const isAllowedResource = (allowedResources: string[]) => (kind: string): boolean => {
if ((kind as KubeResource) in apiResourceRecord) {
return allowedResources.includes(kind);
}
const apiResource = apiResources.find(resource => resource.kind === kind);
if (apiResource) {
return allowedResources.includes(apiResource.apiName);
}
return true; // allowed by default for other resources
};
@@ -1,91 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type {
V1APIGroupList,
V1APIResourceList,
V1APIVersions,
} from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import type { K8sRequest } from "../../main/k8s-request.injectable";
import k8SRequestInjectable from "../../main/k8s-request.injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable";
import type { KubeApiResource, KubeResource } from "../rbac";
import type { Cluster } from "./cluster";
import plimit from "p-limit";
export type RequestListApiResources = () => Promise<KubeApiResource[]>;
/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
export type ListApiResources = (cluster: Cluster) => RequestListApiResources;
interface Dependencies {
logger: Logger;
k8sRequest: K8sRequest;
}
const listApiResources = ({ k8sRequest, logger }: Dependencies): ListApiResources => {
return (cluster) => {
const clusterRequest = (path: string) => k8sRequest(cluster, path);
const apiLimit = plimit(5);
return async () => {
const resources: KubeApiResource[] = [];
try {
const resourceListGroups:{ group:string;path:string }[] = [];
await Promise.all(
[
clusterRequest("/api").then((response:V1APIVersions)=>response.versions.forEach(version => resourceListGroups.push({ group:version, path:`/api/${version}` }))),
clusterRequest("/apis").then((response:V1APIGroupList) => response.groups.forEach(group => {
const preferredVersion = group.preferredVersion?.groupVersion;
if (preferredVersion) {
resourceListGroups.push({ group:group.name, path:`/apis/${preferredVersion}` });
}
})),
],
);
await Promise.all(
resourceListGroups.map(({ group, path }) => apiLimit(async () => {
const apiResources:V1APIResourceList = await clusterRequest(path);
if (apiResources.resources) {
resources.push(
...apiResources.resources.filter(resource => resource.verbs.includes("list")).map((resource) => ({
apiName: resource.name as KubeResource,
kind: resource.kind,
group,
})),
);
}
}),
),
);
} catch (error) {
logger.error(`[LIST-API-RESOURCES]: failed to list api resources: ${error}`);
}
return resources;
};
};
};
const listApiResourcesInjectable = getInjectable({
id: "list-api-resources",
instantiate: (di) => {
const k8sRequest = di.inject(k8SRequestInjectable);
const logger = di.inject(loggerInjectable);
return listApiResources({ k8sRequest, logger });
},
});
export default listApiResourcesInjectable;
@@ -0,0 +1,78 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeConfig } from "@kubernetes/client-node";
import { AuthorizationV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../logger.injectable";
import type { KubeApiResource } from "../rbac";
export type CanListResource = (resource: KubeApiResource) => boolean;
/**
* Requests the permissions for actions on the kube cluster
* @param namespace The namespace of the resources
*/
export type RequestNamespaceListPermissions = (namespace: string) => Promise<CanListResource>;
/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
export type RequestNamespaceListPermissionsFor = (proxyConfig: KubeConfig) => RequestNamespaceListPermissions;
const requestNamespaceListPermissionsForInjectable = getInjectable({
id: "request-namespace-list-permissions-for",
instantiate: (di): RequestNamespaceListPermissionsFor => {
const logger = di.inject(loggerInjectable);
return (proxyConfig) => {
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
return async (namespace) => {
try {
const { body: { status }} = await api.createSelfSubjectRulesReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectRulesReview",
spec: { namespace },
});
if (!status || status.incomplete) {
logger.warn(`[AUTHORIZATION-NAMESPACE-REVIEW]: allowing all resources in namespace="${namespace}" due to incomplete SelfSubjectRulesReview: ${status?.evaluationError}`);
return () => true;
}
const { resourceRules } = status;
return (resource) => {
const resourceRule = resourceRules.find(({
apiGroups = [],
resources = [],
}) => {
const isAboutRelevantApiGroup = apiGroups.includes("*") || apiGroups.includes(resource.group);
const isAboutResource = resources.includes("*") || resources.includes(resource.apiName);
return isAboutRelevantApiGroup && isAboutResource;
});
if (!resourceRule) {
return false;
}
const { verbs } = resourceRule;
return verbs.includes("*") || verbs.includes("list");
};
} catch (error) {
logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review`, { namespace, error });
return () => true;
}
};
};
},
});
export default requestNamespaceListPermissionsForInjectable;
-27
View File
@@ -1,27 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import * as Mobx from "mobx";
import * as Immer from "immer";
/**
* Setup default configuration for external npm-packages
*/
export default function configurePackages() {
// Docs: https://mobx.js.org/configuration.html
Mobx.configure({
enforceActions: "never",
// TODO: enable later (read more: https://mobx.js.org/migrating-from-4-or-5.html)
// computedRequiresReaction: true,
// reactionRequiresObservable: true,
// observableRequiresReaction: true,
});
// Docs: https://immerjs.github.io/immer/
// Required in `utils/storage-helper.ts`
Immer.setAutoFreeze(false); // allow to merge mobx observables
Immer.enableMapSet(); // allow to merge maps and sets
}
@@ -1,55 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { RequestInit, Response } from "node-fetch";
import type { AsyncResult } from "../utils/async-result";
import fetchInjectable from "./fetch.injectable";
export interface DownloadJsonOptions {
signal?: AbortSignal | null | undefined;
}
export type DownloadJson = (url: string, opts?: DownloadJsonOptions) => Promise<AsyncResult<unknown, string>>;
const downloadJsonInjectable = getInjectable({
id: "download-json",
instantiate: (di): DownloadJson => {
const fetch = di.inject(fetchInjectable);
return async (url, opts) => {
let result: Response;
try {
result = await fetch(url, opts as RequestInit);
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
if (result.status < 200 || 300 <= result.status) {
return {
callWasSuccessful: false,
error: result.statusText,
};
}
try {
return {
callWasSuccessful: true,
response: await result.json(),
};
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
};
},
});
export default downloadJsonInjectable;
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AsyncResult } from "../../utils/async-result";
import type { Fetch } from "../fetch.injectable";
import type { RequestInit, Response } from "node-fetch";
export interface DownloadJsonOptions {
signal?: AbortSignal | null | undefined;
}
export type DownloadJson = (url: string, opts?: DownloadJsonOptions) => Promise<AsyncResult<unknown, string>>;
export const downloadJsonWith = (fetch: Fetch): DownloadJson => async (url, opts) => {
let result: Response;
try {
result = await fetch(url, opts as RequestInit);
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
if (result.status < 200 || 300 <= result.status) {
return {
callWasSuccessful: false,
error: result.statusText,
};
}
try {
return {
callWasSuccessful: true,
response: await result.json(),
};
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
};
@@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import fetchInjectable from "../fetch.injectable";
import { downloadJsonWith } from "./impl";
const downloadJsonInjectable = getInjectable({
id: "download-json",
instantiate: (di) => downloadJsonWith(di.inject(fetchInjectable)),
});
export default downloadJsonInjectable;
@@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import proxyFetchInjectable from "../proxy-fetch.injectable";
import { downloadJsonWith } from "./impl";
const proxyDownloadJsonInjectable = getInjectable({
id: "proxy-download-json",
instantiate: (di) => downloadJsonWith(di.inject(proxyFetchInjectable)),
});
export default proxyDownloadJsonInjectable;
@@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type * as FetchModule from "node-fetch";
const { NodeFetch } = require("../../../build/webpack/node-fetch.bundle") as { NodeFetch: typeof FetchModule };
/**
* NOTE: while using this module can cause side effects, this specific injectable is not marked as
* such since sometimes the request can be wholely within the perview of unit test
*/
const nodeFetchModuleInjectable = getInjectable({
id: "node-fetch-module",
instantiate: () => NodeFetch,
});
export default nodeFetchModuleInjectable;
+4 -19
View File
@@ -3,32 +3,17 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { HttpsProxyAgent } from "hpagent";
import type * as FetchModule from "node-fetch";
import userStoreInjectable from "../user-store/user-store.injectable";
const { NodeFetch: { default: fetch }} = require("../../../build/webpack/node-fetch.bundle") as { NodeFetch: typeof FetchModule };
type Response = FetchModule.Response;
type RequestInit = FetchModule.RequestInit;
import type { RequestInit, Response } from "node-fetch";
import nodeFetchModuleInjectable from "./fetch-module.injectable";
export type Fetch = (url: string, init?: RequestInit) => Promise<Response>;
const fetchInjectable = getInjectable({
id: "fetch",
instantiate: (di): Fetch => {
const { httpsProxy, allowUntrustedCAs } = di.inject(userStoreInjectable);
const agent = httpsProxy
? new HttpsProxyAgent({
proxy: httpsProxy,
rejectUnauthorized: !allowUntrustedCAs,
})
: undefined;
const { default: fetch } = di.inject(nodeFetchModuleInjectable);
return (url, init = {}) => fetch(url, {
agent,
...init,
});
return (url, init) => fetch(url, init);
},
causesSideEffects: true,
});
@@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { HttpsProxyAgent } from "hpagent";
import userStoreInjectable from "../user-store/user-store.injectable";
import type { Fetch } from "./fetch.injectable";
import fetchInjectable from "./fetch.injectable";
const proxyFetchInjectable = getInjectable({
id: "proxy-fetch",
instantiate: (di): Fetch => {
const fetch = di.inject(fetchInjectable);
const { httpsProxy, allowUntrustedCAs } = di.inject(userStoreInjectable);
const agent = httpsProxy
? new HttpsProxyAgent({
proxy: httpsProxy,
rejectUnauthorized: !allowUntrustedCAs,
})
: undefined;
return (url, init = {}) => fetch(url, {
agent,
...init,
});
},
});
export default proxyFetchInjectable;
@@ -3,22 +3,19 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const configMapsRouteInjectable = getInjectable({
id: "config-maps-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "configmaps");
return {
path: "/configmaps",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/configmaps",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "configmaps",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const horizontalPodAutoscalersRouteInjectable = getInjectable({
id: "horizontal-pod-autoscalers-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "horizontalpodautoscalers");
return {
path: "/hpa",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/hpa",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "horizontalpodautoscalers",
group: "autoscaling",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const leasesRouteInjectable = getInjectable({
id: "leases",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "leases");
return {
path: "/leases",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/leases",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "leases",
group: "coordination.k8s.io",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,24 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const limitRangesRouteInjectable = getInjectable({
id: "limit-ranges-route",
instantiate: (di) => {
const limitRangesIsAllowed = di.inject(
isAllowedResourceInjectable,
"limitranges",
);
return {
path: "/limitranges",
clusterFrame: true,
isEnabled: limitRangesIsAllowed,
};
},
instantiate: (di) => ({
path: "/limitranges",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "limitranges",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const podDisruptionBudgetsRouteInjectable = getInjectable({
id: "pod-disruption-budgets-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "poddisruptionbudgets");
return {
path: "/poddisruptionbudgets",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/poddisruptionbudgets",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "poddisruptionbudgets",
group: "policy",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const priorityClassesRouteInjectable = getInjectable({
id: "priority-classes-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "priorityclasses");
return {
path: "/priorityclasses",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/priorityclasses",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "priorityclasses",
group: "scheduling.k8s.io",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const resourceQuotasRouteInjectable = getInjectable({
id: "resource-quotas-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "resourcequotas");
return {
path: "/resourcequotas",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/resourcequotas",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "resourcequotas",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const runtimeClassesRouteInjectable = getInjectable({
id: "runtime-classes-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "runtimeclasses");
return {
path: "/runtimeclasses",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/runtimeclasses",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "runtimeclasses",
group: "node.k8s.io",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const secretsRouteInjectable = getInjectable({
id: "secrets-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "secrets");
return {
path: "/secrets",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/secrets",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "secrets",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const eventsRouteInjectable = getInjectable({
id: "events-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "events");
return {
path: "/events",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/events",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "events",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const namespacesRouteInjectable = getInjectable({
id: "namespaces-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "namespaces");
return {
path: "/namespaces",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/namespaces",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "namespaces",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const endpointsRouteInjectable = getInjectable({
id: "endpoints-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "endpoints");
return {
path: "/endpoints",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/endpoints",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "endpoints",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { computedOr } from "../../../../../utils/computed-or";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const ingressesRouteInjectable = getInjectable({
id: "ingresses-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "ingresses");
return {
path: "/ingresses",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/ingresses",
clusterFrame: true,
isEnabled: computedOr(
di.inject(shouldShowResourceInjectionToken, {
apiName: "ingresses",
group: "networking.k8s.io",
}),
di.inject(shouldShowResourceInjectionToken, {
apiName: "ingresses",
group: "extensions",
}),
),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const networkPoliciesRouteInjectable = getInjectable({
id: "network-policies-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "networkpolicies");
return {
path: "/network-policies",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/network-policies",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "networkpolicies",
group: "networking.k8s.io",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const servicesRouteInjectable = getInjectable({
id: "services-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "services");
return {
path: "/services",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/services",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "services",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const nodesRouteInjectable = getInjectable({
id: "nodes-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "nodes");
return {
path: "/nodes",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/nodes",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "nodes",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const clusterOverviewRouteInjectable = getInjectable({
id: "cluster-overview-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "nodes");
return {
path: "/overview",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/overview",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "nodes",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const persistentVolumeClaimsRouteInjectable = getInjectable({
id: "persistent-volume-claims-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "persistentvolumeclaims");
return {
path: "/persistent-volume-claims",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/persistent-volume-claims",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "persistentvolumeclaims",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const persistentVolumesRouteInjectable = getInjectable({
id: "persistent-volumes-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "persistentvolumes");
return {
path: "/persistent-volumes",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/persistent-volumes",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "persistentvolumes",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const storageClassesRouteInjectable = getInjectable({
id: "storage-classes-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "storageclasses");
return {
path: "/storage-classes",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/storage-classes",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "storageclasses",
group: "storage.k8s.io",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const clusterRoleBindingsRouteInjectable = getInjectable({
id: "cluster-role-bindings-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "clusterrolebindings");
return {
path: "/cluster-role-bindings",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/cluster-role-bindings",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "clusterrolebindings",
group: "rbac.authorization.k8s.io",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const clusterRolesRouteInjectable = getInjectable({
id: "cluster-roles-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "clusterroles");
return {
path: "/cluster-roles",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/cluster-roles",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "clusterroles",
group: "rbac.authorization.k8s.io",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,19 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const podSecurityPoliciesRouteInjectable = getInjectable({
id: "pod-security-policies-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "podsecuritypolicies");
return {
path: "/pod-security-policies",
clusterFrame: true,
isEnabled: isAllowedResource,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "podsecuritypolicies",
group: "policy",
}),
};
},
@@ -3,19 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const roleBindingsRouteInjectable = getInjectable({
id: "role-bindings-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "rolebindings");
return {
path: "/role-bindings",
clusterFrame: true,
isEnabled: isAllowedResource,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "rolebindings",
group: "rbac.authorization.k8s.io",
}),
};
},
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const rolesRouteInjectable = getInjectable({
id: "roles-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "roles");
return {
path: "/roles",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/roles",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "roles",
group: "rbac.authorization.k8s.io",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const serviceAccountsRouteInjectable = getInjectable({
id: "service-accounts-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "serviceaccounts");
return {
path: "/service-accounts",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/service-accounts",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "serviceaccounts",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const cronJobsRouteInjectable = getInjectable({
id: "cron-jobs-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "cronjobs");
return {
path: "/cronjobs",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/cronjobs",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "cronjobs",
group: "batch",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const daemonsetsRouteInjectable = getInjectable({
id: "daemonsets-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "daemonsets");
return {
path: "/daemonsets",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/daemonsets",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "daemonsets",
group: "apps",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const deploymentsRouteInjectable = getInjectable({
id: "deployments-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "deployments");
return {
path: "/deployments",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/deployments",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "deployments",
group: "apps",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const jobsRouteInjectable = getInjectable({
id: "jobs-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "jobs");
return {
path: "/jobs",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/jobs",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "jobs",
group: "batch",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const podsRouteInjectable = getInjectable({
id: "pods-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "pods");
return {
path: "/pods",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/pods",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "pods",
group: "v1",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const replicasetsRouteInjectable = getInjectable({
id: "replicasets-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "replicasets");
return {
path: "/replicasets",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/replicasets",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "replicasets",
group: "apps",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,21 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const statefulsetsRouteInjectable = getInjectable({
id: "statefulsets-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "statefulsets");
return {
path: "/statefulsets",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
instantiate: (di) => ({
path: "/statefulsets",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "statefulsets",
group: "apps",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
@@ -3,12 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationInformationInjectable from "../../../vars/application-information.injectable";
import applicationInformationToken from "../../../vars/application-information-token";
const welcomeRouteConfigInjectable = getInjectable({
id: "welcome-route-config",
instantiate: (di) => di.inject(applicationInformationInjectable).config.welcomeRoute,
instantiate: (di) => di.inject(applicationInformationToken).config.welcomeRoute,
});
export default welcomeRouteConfigInjectable;
@@ -3,7 +3,14 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import createClusterInjectable from "../../../main/create-cluster/create-cluster.injectable";
import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable";
import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
import directoryForKubeConfigsInjectable from "../../app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import directoryForUserDataInjectable from "../../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import type { ApiManager } from "../api-manager";
import apiManagerInjectable from "../api-manager/manager.injectable";
import { KubeApi } from "../kube-api";
@@ -22,9 +29,24 @@ class TestStore extends KubeObjectStore<KubeObject, TestApi> {
describe("ApiManager", () => {
let apiManager: ApiManager;
let di: DiContainer;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "/some-user-store-path");
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
apiManager = di.inject(apiManagerInjectable);
});
@@ -40,7 +62,9 @@ describe("ApiManager", () => {
fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true,
});
const kubeStore = new TestStore(kubeApi);
const kubeStore = new TestStore({
context: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
}, kubeApi);
apiManager.registerApi(apiBase, kubeApi);
@@ -3,43 +3,23 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeJsonApi } from "../kube-json-api";
import { PassThrough } from "stream";
import type { ApiManager } from "../api-manager";
import { Ingress, IngressApi } from "../endpoints";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
import apiManagerInjectable from "../api-manager/manager.injectable";
import autoRegistrationInjectable from "../api-manager/auto-registration.injectable";
import type { Fetch } from "../../fetch/fetch.injectable";
import fetchInjectable from "../../fetch/fetch.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { flushPromises } from "../../test-utils/flush-promises";
import createKubeJsonApiInjectable from "../create-kube-json-api.injectable";
import type { Response, Headers as NodeFetchHeaders } from "node-fetch";
const createMockResponseFromString = (url: string, data: string, statusCode = 200) => {
const res: jest.Mocked<Response> = {
buffer: jest.fn(async () => { throw new Error("buffer() is not supported"); }),
clone: jest.fn(() => res),
arrayBuffer: jest.fn(async () => { throw new Error("arrayBuffer() is not supported"); }),
blob: jest.fn(async () => { throw new Error("blob() is not supported"); }),
body: new PassThrough(),
bodyUsed: false,
headers: new Headers() as NodeFetchHeaders,
json: jest.fn(async () => JSON.parse(await res.text())),
ok: 200 <= statusCode && statusCode < 300,
redirected: 300 <= statusCode && statusCode < 400,
size: data.length,
status: statusCode,
statusText: "some-text",
text: jest.fn(async () => data),
type: "basic",
url,
formData: jest.fn(async () => { throw new Error("formData() is not supported"); }),
};
return res;
};
import setupAutoRegistrationInjectable from "../../../renderer/before-frame-starts/runnables/setup-auto-registration.injectable";
import { createMockResponseFromString } from "../../../test-utils/mock-responses";
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
import directoryForUserDataInjectable from "../../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import createClusterInjectable from "../../../main/create-cluster/create-cluster.injectable";
import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
import directoryForKubeConfigsInjectable from "../../app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
describe("KubeApi", () => {
let request: KubeJsonApi;
@@ -52,6 +32,20 @@ describe("KubeApi", () => {
fetchMock = asyncFn();
di.override(fetchInjectable, () => fetchMock);
di.override(directoryForUserDataInjectable, () => "/some-user-store-path");
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
const createKubeJsonApi = di.inject(createKubeJsonApiInjectable);
request = createKubeJsonApi({
@@ -60,7 +54,9 @@ describe("KubeApi", () => {
});
registerApiSpy = jest.spyOn(di.inject(apiManagerInjectable), "registerApi");
di.inject(autoRegistrationInjectable);
const setupAutoRegistration = di.inject(setupAutoRegistrationInjectable);
setupAutoRegistration.run();
});
describe("on first call to IngressApi.get()", () => {
+38 -59
View File
@@ -8,7 +8,6 @@ import type { KubeJsonApi, KubeJsonApiData } from "../kube-json-api";
import { PassThrough } from "stream";
import { Deployment, DeploymentApi, NamespaceApi, Pod, PodApi } from "../endpoints";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
import autoRegistrationInjectable from "../api-manager/auto-registration.injectable";
import type { Fetch } from "../../fetch/fetch.injectable";
import fetchInjectable from "../../fetch/fetch.injectable";
import type { CreateKubeApiForRemoteCluster } from "../create-kube-api-for-remote-cluster.injectable";
@@ -19,64 +18,14 @@ import { flushPromises } from "../../test-utils/flush-promises";
import createKubeJsonApiInjectable from "../create-kube-json-api.injectable";
import type { IKubeWatchEvent } from "../kube-watch-event";
import type { KubeJsonApiDataFor } from "../kube-object";
import type { Response, Headers as NodeFetchHeaders } from "node-fetch";
import AbortController from "abort-controller";
const createMockResponseFromString = (url: string, data: string, statusCode = 200) => {
const res: jest.Mocked<Response> = {
buffer: jest.fn(async () => { throw new Error("buffer() is not supported"); }),
clone: jest.fn(() => res),
arrayBuffer: jest.fn(async () => { throw new Error("arrayBuffer() is not supported"); }),
blob: jest.fn(async () => { throw new Error("blob() is not supported"); }),
body: new PassThrough(),
bodyUsed: false,
headers: new Headers() as NodeFetchHeaders,
json: jest.fn(async () => JSON.parse(await res.text())),
ok: 200 <= statusCode && statusCode < 300,
redirected: 300 <= statusCode && statusCode < 400,
size: data.length,
status: statusCode,
statusText: "some-text",
text: jest.fn(async () => data),
type: "basic",
url,
formData: jest.fn(async () => { throw new Error("formData() is not supported"); }),
};
return res;
};
const createMockResponseFromStream = (url: string, stream: NodeJS.ReadableStream, statusCode = 200) => {
const res: jest.Mocked<Response> = {
buffer: jest.fn(async () => { throw new Error("buffer() is not supported"); }),
clone: jest.fn(() => res),
arrayBuffer: jest.fn(async () => { throw new Error("arrayBuffer() is not supported"); }),
blob: jest.fn(async () => { throw new Error("blob() is not supported"); }),
body: stream,
bodyUsed: false,
headers: new Headers() as NodeFetchHeaders,
json: jest.fn(async () => JSON.parse(await res.text())),
ok: 200 <= statusCode && statusCode < 300,
redirected: 300 <= statusCode && statusCode < 400,
size: 10,
status: statusCode,
statusText: "some-text",
text: jest.fn(() => {
const chunks: Buffer[] = [];
return new Promise((resolve, reject) => {
stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
stream.on("error", (err) => reject(err));
stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
});
}),
type: "basic",
url,
formData: jest.fn(async () => { throw new Error("formData() is not supported"); }),
};
return res;
};
import setupAutoRegistrationInjectable from "../../../renderer/before-frame-starts/runnables/setup-auto-registration.injectable";
import { createMockResponseFromStream, createMockResponseFromString } from "../../../test-utils/mock-responses";
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
import directoryForUserDataInjectable from "../../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import createClusterInjectable from "../../../main/create-cluster/create-cluster.injectable";
import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
import directoryForKubeConfigsInjectable from "../../app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
describe("createKubeApiForRemoteCluster", () => {
let createKubeApiForRemoteCluster: CreateKubeApiForRemoteCluster;
@@ -85,6 +34,20 @@ describe("createKubeApiForRemoteCluster", () => {
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "/some-user-store-path");
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
fetchMock = asyncFn();
di.override(fetchInjectable, () => fetchMock);
@@ -174,6 +137,20 @@ describe("KubeApi", () => {
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "/some-user-store-path");
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
fetchMock = asyncFn();
di.override(fetchInjectable, () => fetchMock);
@@ -184,7 +161,9 @@ describe("KubeApi", () => {
apiBase: "/api-kube",
});
di.inject(autoRegistrationInjectable);
const setupAutoRegistration = di.inject(setupAutoRegistrationInjectable);
setupAutoRegistration.run();
});
describe("patching deployments", () => {
@@ -3,27 +3,22 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Cluster } from "../../cluster/cluster";
import type { ClusterContext } from "../cluster-context";
import type { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object";
import type { KubeObjectStoreLoadingParams } from "../kube-object.store";
import { KubeObjectStore } from "../kube-object.store";
class FakeKubeObjectStore extends KubeObjectStore<KubeObject> {
_context = {
allNamespaces: [],
contextNamespaces: [],
hasSelectedAll: false,
cluster: {} as Cluster,
} as ClusterContext;
get context() {
return this._context;
}
constructor(private readonly _loadItems: (params: KubeObjectStoreLoadingParams) => KubeObject[], api: Partial<KubeApi<KubeObject>>) {
super(api as KubeApi<KubeObject>);
super({
context: {
allNamespaces: [],
contextNamespaces: [],
hasSelectedAll: false,
isGlobalWatchEnabled: () => true,
isLoadingAll: () => true,
},
}, api as KubeApi<KubeObject>);
}
async loadItems(params: KubeObjectStoreLoadingParams) {
@@ -1,74 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { CustomResourceDefinition } from "../endpoints";
import { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object";
import autoRegistrationEmitterInjectable from "./auto-registration-emitter.injectable";
import apiManagerInjectable from "./manager.injectable";
import { CustomResourceStore } from "./resource.store";
const autoRegistrationInjectable = getInjectable({
id: "api-manager-auto-registration",
instantiate: (di) => {
const autoRegistrationEmitter = di.inject(autoRegistrationEmitterInjectable);
const beforeApiManagerInitializationCrds: CustomResourceDefinition[] = [];
const beforeApiManagerInitializationApis: KubeApi[] = [];
let initialized = false;
const autoInitCustomResourceStore = (crd: CustomResourceDefinition) => {
const objectConstructor = class extends KubeObject {
static readonly kind = crd.getResourceKind();
static readonly namespaced = crd.isNamespaced();
static readonly apiBase = crd.getResourceApiBase();
};
const api = (() => {
const rawApi = apiManager.getApi(objectConstructor.apiBase);
if (rawApi) {
return rawApi;
}
const api = new KubeApi({ objectConstructor });
apiManager.registerApi(api);
return api;
})();
if (!apiManager.getStore(api)) {
apiManager.registerStore(new CustomResourceStore(api));
}
};
const autoInitKubeApi = (api: KubeApi) => {
apiManager.registerApi(api);
};
autoRegistrationEmitter
.on("customResourceDefinition", (crd) => {
if (initialized) {
autoInitCustomResourceStore(crd);
} else {
beforeApiManagerInitializationCrds.push(crd);
}
})
.on("kubeApi", (api) => {
if (initialized) {
autoInitKubeApi(api);
} else {
beforeApiManagerInitializationApis.push(api);
}
});
const apiManager = di.inject(apiManagerInjectable);
beforeApiManagerInitializationCrds.forEach(autoInitCustomResourceStore);
beforeApiManagerInitializationApis.forEach(autoInitKubeApi);
initialized = true;
},
});
export default autoRegistrationInjectable;
@@ -4,11 +4,12 @@
*/
import type { KubeApi } from "../kube-api";
import type { KubeObjectStoreDependencies } from "../kube-object.store";
import { KubeObjectStore } from "../kube-object.store";
import type { KubeObject } from "../kube-object";
export class CustomResourceStore<K extends KubeObject> extends KubeObjectStore<K, KubeApi<K>> {
constructor(api: KubeApi<K>) {
super(api);
constructor(deps: KubeObjectStoreDependencies, api: KubeApi<K>) {
super(deps, api);
}
}
-13
View File
@@ -1,13 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Cluster } from "../cluster/cluster";
export interface ClusterContext {
cluster: Cluster;
allNamespaces: string[]; // available / allowed namespaces from cluster.ts
contextNamespaces: string[]; // selected by user (see: namespace-select.tsx)
hasSelectedAll: boolean;
}
@@ -3,6 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import moment from "moment";
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api";
import type { KubeObjectStatus, LabelSelector, NamespaceScopedMetadata } from "../kube-object";
@@ -87,4 +89,23 @@ export class DaemonSetApi extends KubeApi<DaemonSet> {
objectConstructor: DaemonSet,
});
}
restart(params: { namespace: string; name: string }) {
return this.request.patch(this.getUrl(params), {
data: {
spec: {
template: {
metadata: {
annotations: { "kubectl.kubernetes.io/restartedAt" : moment.utc().format() },
},
},
},
},
},
{
headers: {
"content-type": "application/strategic-merge-patch+json",
},
});
}
}
@@ -5,25 +5,44 @@
import { getInjectable } from "@ogre-tools/injectable";
import type { Patch } from "rfc6902";
import apiBaseInjectable from "../../api-base.injectable";
import type { AsyncResult } from "../../../utils/async-result";
import type { KubeJsonApiData } from "../../kube-json-api";
export type RequestKubeObjectPatch = (name: string, kind: string, ns: string | undefined, patch: Patch) => Promise<KubeJsonApiData>;
export type RequestKubeObjectPatch = (name: string, kind: string, ns: string | undefined, patch: Patch) => Promise<AsyncResult<KubeJsonApiData, string>>;
const requestKubeObjectPatchInjectable = getInjectable({
id: "request-kube-object-patch",
instantiate: (di): RequestKubeObjectPatch => {
const apiBase = di.inject(apiBaseInjectable);
return (name, kind, ns, patch) => (
apiBase.patch("/stack", {
return async (name, kind, ns, patch) => {
const result = await apiBase.patch("/stack", {
data: {
name,
kind,
ns,
patch,
},
})
);
}) as AsyncResult<string, string>;
if (!result.callWasSuccessful) {
return result;
}
try {
const response = JSON.parse(result.response);
return {
callWasSuccessful: true,
response,
};
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
};
},
});
@@ -4,16 +4,37 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import apiBaseInjectable from "../../api-base.injectable";
import type { AsyncResult } from "../../../utils/async-result";
import type { KubeJsonApiData } from "../../kube-json-api";
export type RequestKubeObjectCreation = (resourceDescriptor: string) => Promise<KubeJsonApiData>;
export type RequestKubeObjectCreation = (resourceDescriptor: string) => Promise<AsyncResult<KubeJsonApiData, string>>;
const requestKubeObjectCreationInjectable = getInjectable({
id: "request-kube-object-creation",
instantiate: (di): RequestKubeObjectCreation => {
const apiBase = di.inject(apiBaseInjectable);
return (data) => apiBase.post("/stack", { data });
return async (data) => {
const result = await apiBase.post("/stack", { data }) as AsyncResult<string, string>;
if (!result.callWasSuccessful) {
return result;
}
try {
const response = JSON.parse(result.response);
return {
callWasSuccessful: true,
response,
};
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
};
},
});
+2 -2
View File
@@ -104,7 +104,7 @@ export class JsonApi<Data = JsonApiData, Params extends JsonApiParams<Data> = Js
);
const { query } = params ?? {};
if (query) {
if (query && Object.keys(query).length > 0) {
const queryString = stringify(query as unknown as QueryParams);
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
@@ -171,7 +171,7 @@ export class JsonApi<Data = JsonApiData, Params extends JsonApiParams<Data> = Js
reqInit.body = JSON.stringify(data);
}
if (query) {
if (query && Object.keys(query).length > 0) {
const queryString = stringify(query as unknown as QueryParams);
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
+15 -12
View File
@@ -403,10 +403,11 @@ export class KubeApi<
/**
* This method differs from {@link formatUrlForNotListing} because this treats `""` as "all namespaces"
* NOTE: This is also useful for watching
* @param namespace The namespace to list in or `""` for all namespaces
*/
formatUrlForListing(namespace: string) {
return createKubeApiURL({
formatUrlForListing(namespace: string | undefined, query?: Partial<KubeApiQueryParams>) {
const resourcePath = createKubeApiURL({
apiPrefix: this.apiPrefix,
apiVersion: this.apiVersionWithGroup,
resource: this.apiResource,
@@ -414,15 +415,15 @@ export class KubeApi<
? namespace ?? "default"
: undefined,
});
return resourcePath + (query ? `?${stringify(this.normalizeQuery(query))}` : "");
}
/**
* Format a URL pathname and query for acting upon a specific resource.
*/
formatUrlForNotListing(resource?: Partial<ResourceDescriptor>, query?: Partial<KubeApiQueryParams>): string;
formatUrlForNotListing({ name, namespace }: Partial<ResourceDescriptor> = {}, query?: Partial<KubeApiQueryParams>) {
const resourcePath = createKubeApiURL({
formatUrlForNotListing({ name, namespace }: Partial<ResourceDescriptor> = {}) {
return createKubeApiURL({
apiPrefix: this.apiPrefix,
apiVersion: this.apiVersionWithGroup,
resource: this.apiResource,
@@ -431,15 +432,17 @@ export class KubeApi<
: undefined,
name,
});
return resourcePath + (query ? `?${stringify(this.normalizeQuery(query))}` : "");
}
/**
* @deprecated use {@link formatUrlForNotListing} instead
* @deprecated use {@link formatUrlForNotListing} or {@link formatUrlForListing} instead
*/
getUrl(resource?: Partial<ResourceDescriptor>, query?: Partial<KubeApiQueryParams>) {
return this.formatUrlForNotListing(resource, query);
if (query) {
return this.formatUrlForListing(resource?.namespace, query);
}
return this.formatUrlForNotListing(resource);
}
protected normalizeQuery(query: Partial<KubeApiQueryParams> = {}) {
@@ -625,14 +628,14 @@ export class KubeApi<
}
getWatchUrl(namespace?: string, query: KubeApiQueryParams = {}) {
return this.formatUrlForNotListing({ namespace }, {
return this.formatUrlForListing(namespace, {
watch: 1,
resourceVersion: this.getResourceVersion(namespace),
...query,
});
}
watch(opts?: KubeApiWatchOptions<Object, Data>): () => void {
watch(opts?: KubeApiWatchOptions<Object, Data>): Disposer {
let errorReceived = false;
let timedRetry: NodeJS.Timeout;
const {
+40 -66
View File
@@ -3,11 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterContext } from "./cluster-context";
import { action, computed, makeObservable, observable, reaction, when } from "mobx";
import { action, computed, makeObservable, observable, reaction } from "mobx";
import type { Disposer } from "../utils";
import { waitUntilDefined, autoBind, includes, noop, rejectPromiseBy } from "../utils";
import { waitUntilDefined, autoBind, includes, rejectPromiseBy } from "../utils";
import type { KubeJsonApiDataFor, KubeObject } from "./kube-object";
import { KubeStatus } from "./kube-object";
import type { IKubeWatchEvent } from "./kube-watch-event";
@@ -21,6 +19,7 @@ import assert from "assert";
import type { PartialDeep } from "type-fest";
import { entries } from "../utils/objects";
import AbortController from "abort-controller";
import type { ClusterContext } from "../../renderer/cluster-frame-context/cluster-frame-context";
export type OnLoadFailure = (error: unknown) => void;
@@ -85,38 +84,26 @@ export type KubeApiDataFrom<K extends KubeObject, A> = A extends KubeApi<K, infe
export type JsonPatch = Patch;
export interface KubeObjectStoreDependencies {
readonly context: ClusterContext;
}
export abstract class KubeObjectStore<
K extends KubeObject = KubeObject,
A extends KubeApi<K, D> = KubeApi<K, KubeJsonApiDataFor<K>>,
D extends KubeJsonApiDataFor<K> = KubeApiDataFrom<K, A>,
> extends ItemStore<K> {
static readonly defaultContext = observable.box<ClusterContext>(); // TODO: support multiple cluster contexts
public readonly api!: A;
public readonly limit: number | undefined;
public readonly bufferSize: number;
@observable private loadedNamespaces: string[] | undefined = undefined;
get contextReady() {
return when(() => Boolean(this.context));
}
private readonly loadedNamespaces = observable.box<string[]>();
get namespacesReady() {
return when(() => Boolean(this.loadedNamespaces));
}
constructor(api: A, opts?: KubeObjectStoreOptions);
/**
* @deprecated Supply API instance through constructor
*/
constructor();
constructor(api?: A, opts?: KubeObjectStoreOptions) {
constructor(
protected readonly dependencies: KubeObjectStoreDependencies,
public readonly api: A,
opts?: KubeObjectStoreOptions,
) {
super();
if (api) {
this.api = api;
}
this.limit = opts?.limit;
this.bufferSize = opts?.bufferSize ?? 50_000;
@@ -125,13 +112,9 @@ export abstract class KubeObjectStore<
this.bindWatchEventsUpdater();
}
get context(): ClusterContext | undefined {
return KubeObjectStore.defaultContext.get();
}
// TODO: Circular dependency: KubeObjectStore -> ClusterFrameContext -> NamespaceStore -> KubeObjectStore
@computed get contextItems(): K[] {
const namespaces = this.context?.contextNamespaces ?? [];
const namespaces = this.dependencies.context.contextNamespaces;
return this.items.filter(item => {
const itemNamespace = item.getNs();
@@ -202,17 +185,11 @@ export abstract class KubeObjectStore<
}
protected async loadItems({ namespaces, reqInit, onLoadFailure }: KubeObjectStoreLoadingParams): Promise<K[]> {
if (!this.context?.cluster?.isAllowedResource(this.api.kind)) {
return [];
}
const isLoadingAll = this.context.allNamespaces?.length > 1
&& this.context.cluster.accessibleNamespaces.length === 0
&& this.context.allNamespaces.every(ns => namespaces.includes(ns));
const isLoadingAll = this.dependencies.context.isLoadingAll(namespaces);
if (!this.api.isNamespaced || isLoadingAll) {
if (this.api.isNamespaced) {
this.loadedNamespaces = [];
this.loadedNamespaces.set([]);
}
const res = this.api.list({ reqInit }, this.query);
@@ -234,7 +211,7 @@ export abstract class KubeObjectStore<
return await res ?? [];
}
this.loadedNamespaces = namespaces;
this.loadedNamespaces.set(namespaces);
const results = await Promise.allSettled(
namespaces.map(namespace => this.api.list({ namespace, reqInit }, this.query)),
@@ -266,9 +243,7 @@ export abstract class KubeObjectStore<
@action
async loadAll({ namespaces, merge = true, reqInit, onLoadFailure }: KubeObjectStoreLoadAllParams = {}): Promise<undefined | K[]> {
const context = await waitUntilDefined(() => this.context);
namespaces ??= context.contextNamespaces;
namespaces ??= this.dependencies.context.contextNamespaces;
this.isLoading = true;
try {
@@ -425,7 +400,7 @@ export abstract class KubeObjectStore<
}
// collect items from watch-api events to avoid UI blowing up with huge streams of data
protected eventsBuffer = observable.array<IKubeWatchEvent<D>>([], { deep: false });
protected readonly eventsBuffer = observable.array<IKubeWatchEvent<D>>([], { deep: false });
protected bindWatchEventsUpdater(delay = 1000) {
reaction(() => [...this.eventsBuffer], this.updateFromEventsBuffer, {
@@ -435,25 +410,24 @@ export abstract class KubeObjectStore<
subscribe({ onLoadFailure, abortController = new AbortController() }: KubeObjectStoreSubscribeParams = {}): Disposer {
if (this.api.isNamespaced) {
Promise.race([
rejectPromiseBy(abortController.signal),
Promise.all([
waitUntilDefined(() => this.context),
this.namespacesReady,
] as const),
])
.then(([context]) => {
assert(this.loadedNamespaces);
void (async () => {
try {
const loadedNamespaces = await Promise.race([
rejectPromiseBy(abortController.signal),
waitUntilDefined(() => this.loadedNamespaces.get()),
]);
if (context.cluster?.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) {
return this.watchNamespace("", abortController, { onLoadFailure });
if (this.dependencies.context.isGlobalWatchEnabled() && loadedNamespaces.length === 0) {
this.watchNamespace("", abortController, { onLoadFailure });
} else {
for (const namespace of loadedNamespaces) {
this.watchNamespace(namespace, abortController, { onLoadFailure });
}
}
for (const namespace of this.loadedNamespaces) {
this.watchNamespace(namespace, abortController, { onLoadFailure });
}
})
.catch(noop); // ignore DOMExceptions
} catch (error) {
console.error(`[KUBE-OBJECT-STORE]: failed to subscribe to ${this.api.apiBase}`, error);
}
})();
} else {
this.watchNamespace("", abortController, { onLoadFailure });
}
@@ -467,7 +441,7 @@ export abstract class KubeObjectStore<
}
let timedRetry: NodeJS.Timeout;
const watch = () => this.api.watch({
const startNewWatch = () => this.api.watch({
namespace,
abortController,
callback,
@@ -486,7 +460,7 @@ export abstract class KubeObjectStore<
// not sure what to do, best to retry
clearTimeout(timedRetry);
timedRetry = setTimeout(watch, 5000);
timedRetry = setTimeout(startNewWatch, 5000);
} else if (error instanceof KubeStatus && error.code === 410) {
clearTimeout(timedRetry);
// resourceVersion has gone, let's try to reload
@@ -495,11 +469,11 @@ export abstract class KubeObjectStore<
namespace
? this.loadAll({ namespaces: [namespace], reqInit: { signal }, ...opts })
: this.loadAll({ merge: false, reqInit: { signal }, ...opts })
).then(watch);
).then(startNewWatch);
}, 1000);
} else if (error) { // not sure what to do, best to retry
clearTimeout(timedRetry);
timedRetry = setTimeout(watch, 5000);
timedRetry = setTimeout(startNewWatch, 5000);
}
if (data) {
@@ -508,7 +482,7 @@ export abstract class KubeObjectStore<
};
signal.addEventListener("abort", () => clearTimeout(timedRetry));
watch();
startNewWatch();
}
@action
+13 -2
View File
@@ -645,8 +645,13 @@ export class KubeObject<
}
const requestKubeObjectPatch = asLegacyGlobalFunctionForExtensionApi(requestKubeObjectPatchInjectable);
const result = await requestKubeObjectPatch(this.getName(), this.kind, this.getNs(), patch);
return requestKubeObjectPatch(this.getName(), this.kind, this.getNs(), patch);
if (!result.callWasSuccessful) {
throw new Error(result.error);
}
return result.response;
}
/**
@@ -665,7 +670,13 @@ export class KubeObject<
...data,
});
return requestKubeObjectCreation(descriptor);
const result = await requestKubeObjectCreation(descriptor);
if (!result.callWasSuccessful) {
throw new Error(result.error);
}
return result.response;
}
/**
+14
View File
@@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import applicationInformationToken from "./vars/application-information-token";
import type { ApplicationInformation } from "./vars/application-information-token";
import { bundledExtensionInjectionToken } from "../extensions/extension-discovery/bundled-extension-token";
// @experimental
export {
applicationInformationToken,
ApplicationInformation,
bundledExtensionInjectionToken,
};
+17 -7
View File
@@ -9,13 +9,23 @@ import { loggerTransportInjectionToken } from "./logger/transports";
const loggerInjectable = getInjectable({
id: "logger",
instantiate: (di): Logger => createLogger({
format: format.combine(
format.splat(),
format.simple(),
),
transports: di.injectMany(loggerTransportInjectionToken),
}),
instantiate: (di): Logger => {
const baseLogger = createLogger({
format: format.combine(
format.splat(),
format.simple(),
),
transports: di.injectMany(loggerTransportInjectionToken),
});
return {
debug: (message, ...data) => baseLogger.debug(message, ...data),
info: (message, ...data) => baseLogger.info(message, ...data),
warn: (message, ...data) => baseLogger.warn(message, ...data),
error: (message, ...data) => baseLogger.error(message, ...data),
silly: (message, ...data) => baseLogger.silly(message, ...data),
};
},
});
export default loggerInjectable;
+3 -1
View File
@@ -17,4 +17,6 @@ export interface Logger {
/**
* @deprecated use `di.inject(loggerInjectable)` instead
*/
export default asLegacyGlobalForExtensionApi(loggerInjectable);
const logger = asLegacyGlobalForExtensionApi(loggerInjectable);
export default logger;
+185 -42
View File
@@ -5,58 +5,201 @@
export type KubeResource =
"namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" | "leases" |
"secrets" | "configmaps" | "ingresses" | "ingressclasses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" |
"secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" |
"pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" |
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets" |
"priorityclasses" | "runtimeclasses" |
"roles" | "clusterroles" | "rolebindings" | "clusterrolebindings" | "serviceaccounts";
export interface KubeApiResource extends KubeApiResourceData {
apiName: KubeResource; // valid api resource name (e.g. "namespaces")
export interface KubeApiResource {
kind: string;
group: string;
apiName: string;
namespaced: boolean;
}
export interface KubeApiResourceDescriptor {
apiName: string;
group: string;
}
export const formatKubeApiResource = (res: KubeApiResourceDescriptor) => `${res.group}/${res.apiName}`;
export interface KubeApiResourceData {
kind: string; // resource type (e.g. "Namespace")
group?: string; // api-group
group: string; // api-group, if empty then "core"
namespaced: boolean;
}
export const apiResourceRecord: Record<KubeResource, KubeApiResourceData> = {
"clusterroles": { kind: "ClusterRole", group: "rbac.authorization.k8s.io" },
"clusterrolebindings": { kind: "ClusterRoleBinding", group: "rbac.authorization.k8s.io" },
"configmaps": { kind: "ConfigMap" }, //empty group means "core"
"cronjobs": { kind: "CronJob", group: "batch" },
"customresourcedefinitions": { kind: "CustomResourceDefinition", group: "apiextensions.k8s.io" },
"daemonsets": { kind: "DaemonSet", group: "apps" },
"deployments": { kind: "Deployment", group: "apps" },
"endpoints": { kind: "Endpoint" },
"events": { kind: "Event" },
"horizontalpodautoscalers": { kind: "HorizontalPodAutoscaler", group: "autoscaling" },
"ingresses": { kind: "Ingress", group: "networking.k8s.io" },
"ingressclasses": { kind: "IngressClass", group: "networking.k8s.io" },
"jobs": { kind: "Job", group: "batch" },
"namespaces": { kind: "Namespace" },
"limitranges": { kind: "LimitRange" },
"leases": { kind: "Lease" },
"networkpolicies": { kind: "NetworkPolicy", group: "networking.k8s.io" },
"nodes": { kind: "Node" },
"persistentvolumes": { kind: "PersistentVolume" },
"persistentvolumeclaims": { kind: "PersistentVolumeClaim" },
"pods": { kind: "Pod" },
"poddisruptionbudgets": { kind: "PodDisruptionBudget", group: "policy" },
"podsecuritypolicies": { kind: "PodSecurityPolicy", group: "policy" },
"priorityclasses": { kind: "PriorityClass", group: "scheduling.k8s.io" },
"runtimeclasses": { kind: "RuntimeClass", group: "node.k8s.io" },
"resourcequotas": { kind: "ResourceQuota" },
"replicasets": { kind: "ReplicaSet", group: "apps" },
"roles": { kind: "Role", group: "rbac.authorization.k8s.io" },
"rolebindings": { kind: "RoleBinding", group: "rbac.authorization.k8s.io" },
"secrets": { kind: "Secret" },
"serviceaccounts": { kind: "ServiceAccount" },
"services": { kind: "Service" },
"statefulsets": { kind: "StatefulSet", group: "apps" },
"storageclasses": { kind: "StorageClass", group: "storage.k8s.io" },
clusterroles: {
kind: "ClusterRole",
group: "rbac.authorization.k8s.io",
namespaced: false,
},
clusterrolebindings: {
kind: "ClusterRoleBinding",
group: "rbac.authorization.k8s.io",
namespaced: false,
},
configmaps: {
kind: "ConfigMap",
group: "v1",
namespaced: true,
},
cronjobs: {
kind: "CronJob",
group: "batch",
namespaced: true,
},
customresourcedefinitions: {
kind: "CustomResourceDefinition",
group: "apiextensions.k8s.io",
namespaced: false,
},
daemonsets: {
kind: "DaemonSet",
group: "apps",
namespaced: true,
},
deployments: {
kind: "Deployment",
group: "apps",
namespaced: true,
},
endpoints: {
kind: "Endpoint",
group: "v1",
namespaced: true,
},
events: {
kind: "Event",
group: "v1",
namespaced: true,
},
horizontalpodautoscalers: {
kind: "HorizontalPodAutoscaler",
group: "autoscaling",
namespaced: true,
},
ingresses: {
kind: "Ingress",
group: "networking.k8s.io",
namespaced: true,
},
ingressclasses: {
kind: "IngressClass",
group: "networking.k8s.io",
namespaced: true,
}
jobs: {
kind: "Job",
group: "batch",
namespaced: true,
},
namespaces: {
kind: "Namespace",
group: "v1",
namespaced: false,
},
limitranges: {
kind: "LimitRange",
group: "v1",
namespaced: true,
},
leases: {
kind: "Lease",
group: "v1",
namespaced: true,
},
networkpolicies: {
kind: "NetworkPolicy",
group: "networking.k8s.io",
namespaced: true,
},
nodes: {
kind: "Node",
group: "v1",
namespaced: false,
},
persistentvolumes: {
kind: "PersistentVolume",
group: "v1",
namespaced: false,
},
persistentvolumeclaims: {
kind: "PersistentVolumeClaim",
group: "v1",
namespaced: true,
},
pods: {
kind: "Pod",
group: "v1",
namespaced: true,
},
poddisruptionbudgets: {
kind: "PodDisruptionBudget",
group: "policy",
namespaced: true,
},
podsecuritypolicies: {
kind: "PodSecurityPolicy",
group: "policy",
namespaced: false,
},
priorityclasses: {
kind: "PriorityClass",
group: "scheduling.k8s.io",
namespaced: false,
},
runtimeclasses: {
kind: "RuntimeClass",
group: "node.k8s.io",
namespaced: false,
},
resourcequotas: {
kind: "ResourceQuota",
group: "v1",
namespaced: true,
},
replicasets: {
kind: "ReplicaSet",
group: "apps",
namespaced: true,
},
roles: {
kind: "Role",
group: "rbac.authorization.k8s.io",
namespaced: true,
},
rolebindings: {
kind: "RoleBinding",
group: "rbac.authorization.k8s.io",
namespaced: true,
},
secrets: {
kind: "Secret",
group: "v1",
namespaced: true,
},
serviceaccounts: {
kind: "ServiceAccount",
group: "v1",
namespaced: true,
},
services: {
kind: "Service",
group: "v1",
namespaced: true,
},
statefulsets: {
kind: "StatefulSet",
group: "apps",
namespaced: true,
},
storageclasses: {
kind: "StorageClass",
group: "storage.k8s.io",
namespaced: false,
},
};
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
export const apiResources: KubeApiResource[] = Object.entries(apiResourceRecord)
.map(([apiName, data]) => ({ apiName: apiName as KubeResource, ...data }));
+11
View File
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IComputedValue } from "mobx";
import { computed } from "mobx";
export const computedOr = (...values: IComputedValue<boolean>[]) => computed((
() => values.some(value => value.get())
));
@@ -8,15 +8,11 @@ const environmentVariablesInjectable = getInjectable({
id: "environment-variables",
instantiate: () => {
// IMPORTANT: The syntax needs to be exactly this in order to make environment variable values
// hard-coded at compile-time by Webpack.
const NODE_ENV = process.env.NODE_ENV;
const JEST_WORKER_ID = process.env.JEST_WORKER_ID;
const CICD = process.env.CICD;
return {
// Compile-time environment variables
NODE_ENV,
JEST_WORKER_ID,
CICD,
@@ -1,26 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import type { KubeResource } from "../rbac";
import { allowedResourcesInjectionToken } from "../cluster-store/allowed-resources-injection-token";
export type IsAllowedResource = (resource: KubeResource) => boolean;
const isAllowedResourceInjectable = getInjectable({
id: "is-allowed-resource",
instantiate: (di, resourceName: string) => {
const allowedResources = di.inject(allowedResourcesInjectionToken);
return computed(() => allowedResources.get().has(resourceName));
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, resource: string) => resource,
}),
});
export default isAllowedResourceInjectable;
+7 -6
View File
@@ -5,7 +5,7 @@
export type Falsey = false | 0 | "" | null | undefined;
interface Iterator<T> {
interface Iterator<T> extends Iterable<T> {
filter(fn: (val: T) => unknown): Iterator<T>;
filterMap<U>(fn: (val: T) => Falsey | U): Iterator<U>;
find(fn: (val: T) => unknown): T | undefined;
@@ -15,15 +15,16 @@ interface Iterator<T> {
join(sep?: string): string;
}
export function pipeline<T>(src: IterableIterator<T>): Iterator<T> {
export function chain<T>(src: IterableIterator<T>): Iterator<T> {
return {
filter: (fn) => pipeline(filter(src, fn)),
filterMap: (fn) => pipeline(filterMap(src, fn)),
map: (fn) => pipeline(map(src, fn)),
flatMap: (fn) => pipeline(flatMap(src, fn)),
filter: (fn) => chain(filter(src, fn)),
filterMap: (fn) => chain(filterMap(src, fn)),
map: (fn) => chain(map(src, fn)),
flatMap: (fn) => chain(flatMap(src, fn)),
find: (fn) => find(src, fn),
join: (sep) => join(src, sep),
collect: (fn) => fn(src),
[Symbol.iterator]: () => src,
};
}
+5 -8
View File
@@ -8,26 +8,23 @@ import type { Disposer } from "./disposer";
export async function waitUntilDefined<T>(getter: (() => T | null | undefined) | IComputedValue<T | null | undefined>, opts?: { timeout?: number }): Promise<T> {
return new Promise<T>((resolve, reject) => {
let res: T | null | undefined;
when(
() => {
res = typeof getter === "function"
const res = typeof getter === "function"
? getter()
: getter.get();
const isDefined = res != null;
if (res != null) {
if (isDefined) {
resolve(res);
return true;
}
return false;
return isDefined;
},
() => {},
{
onError: reject,
...opts,
...(opts ?? {}),
},
);
});
@@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import plimit from "p-limit";
export type ConcurrencyLimiter = <Args extends any[], Res>(fn: (...args: Args) => Res) => (...args: Args) => Promise<Res>;
export function withConcurrencyLimit(limit: number): ConcurrencyLimiter {
const limiter = plimit(limit);
return fn => (...args) => limiter(() => fn(...args));
}
-20
View File
@@ -6,26 +6,6 @@
// App's common configuration for any process (main, renderer, build pipeline, etc.)
import type { ThemeId } from "../renderer/themes/lens-theme";
/**
* @deprecated Switch to using isMacInjectable
*/
export const isMac = process.platform === "darwin";
/**
* @deprecated Switch to using isWindowsInjectable
*/
export const isWindows = process.platform === "win32";
/**
* @deprecated Switch to using isLinuxInjectable
*/
export const isLinux = process.platform === "linux";
/**
* @deprecated switch to using `isDebuggingInjectable`
*/
export const isDebugging = ["true", "1", "yes", "y", "on"].includes((process.env.DEBUG ?? "").toLowerCase());
/**
* @deprecated Switch to using isTestEnvInjectable
*/
@@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationInformationInjectable from "./application-information.injectable";
import applicationInformationToken from "./application-information-token";
const applicationCopyrightInjectable = getInjectable({
id: "application-copyright",
instantiate: (di) => di.inject(applicationInformationInjectable).copyright,
instantiate: (di) => di.inject(applicationInformationToken).copyright,
});
export default applicationCopyrightInjectable;
@@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationInformationInjectable from "./application-information.injectable";
import applicationInformationToken from "./application-information-token";
const applicationDescriptionInjectable = getInjectable({
id: "application-description",
instantiate: (di) => di.inject(applicationInformationInjectable).description,
instantiate: (di) => di.inject(applicationInformationToken).description,
});
export default applicationDescriptionInjectable;
@@ -4,14 +4,12 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import packageJson from "../../../package.json";
export type ApplicationInformation = Pick<typeof packageJson, "version" | "config" | "productName" | "copyright" | "description" | "name"> & {
build: Partial<typeof packageJson["build"]> & { publish?: unknown[] };
};
import applicationInformationToken from "../../common/vars/application-information-token";
const applicationInformationInjectable = getInjectable({
id: "application-information",
instantiate: (): ApplicationInformation => {
injectionToken: applicationInformationToken,
instantiate: () => {
const { version, config, productName, build, copyright, description, name } = packageJson;
return { version, config, productName, build, copyright, description, name };
@@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type packageJson from "../../../package.json";
export type ApplicationInformation = Pick<typeof packageJson, "version" | "config" | "productName" | "copyright" | "description" | "name"> & {
build: Partial<typeof packageJson["build"]> & { publish?: unknown[] };
};
const applicationInformationToken = getInjectionToken<ApplicationInformation>({
id: "application-information-token",
});
export default applicationInformationToken;
@@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationInformationInjectable from "./application-information.injectable";
import applicationInformationToken from "./application-information-token";
const bundledKubectlVersionInjectable = getInjectable({
id: "bundled-kubectl-version",
instantiate: (di) => di.inject(applicationInformationInjectable).config.bundledKubectlVersion,
instantiate: (di) => di.inject(applicationInformationToken).config.bundledKubectlVersion,
});
export default bundledKubectlVersionInjectable;
@@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationInformationInjectable from "./application-information.injectable";
import applicationInformationToken from "./application-information-token";
const contentSecurityPolicyInjectable = getInjectable({
id: "content-security-policy",
instantiate: (di) => di.inject(applicationInformationInjectable).config.contentSecurityPolicy,
instantiate: (di) => di.inject(applicationInformationToken).config.contentSecurityPolicy,
});
export default contentSecurityPolicyInjectable;
@@ -4,12 +4,12 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { SemVer } from "semver";
import applicationInformationInjectable from "./application-information.injectable";
import applicationInformationToken from "./application-information-token";
const extensionApiVersionInjectable = getInjectable({
id: "extension-api-version",
instantiate: (di) => {
const { major, minor, patch } = new SemVer(di.inject(applicationInformationInjectable).version);
const { major, minor, patch } = new SemVer(di.inject(applicationInformationToken).version);
return `${major}.${minor}.${patch}`;
},
+2 -2
View File
@@ -3,13 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import environmentVariablesInjectable from "../utils/environment-variables.injectable";
import nodeEnvInjectionToken from "./node-env-injection-token";
const isProductionInjectable = getInjectable({
id: "is-production",
instantiate: (di) => {
const { NODE_ENV: nodeEnv } = di.inject(environmentVariablesInjectable);
const nodeEnv = di.inject(nodeEnvInjectionToken);
return nodeEnv === "production";
},
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
const nodeEnvInjectionToken = getInjectionToken<string | undefined>({
id: "node-env-injection-token",
});
export default nodeEnvInjectionToken;
+2 -2
View File
@@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationInformationInjectable from "./application-information.injectable";
import applicationInformationToken from "./application-information-token";
const productNameInjectable = getInjectable({
id: "product-name",
instantiate: (di) => di.inject(applicationInformationInjectable).productName,
instantiate: (di) => di.inject(applicationInformationToken).productName,
});
export default productNameInjectable;
+2 -2
View File
@@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationInformationInjectable from "./application-information.injectable";
import applicationInformationToken from "./application-information-token";
const sentryDataSourceNameInjectable = getInjectable({
id: "sentry-data-source-name",
instantiate: (di) => di.inject(applicationInformationInjectable).config.sentryDsn,
instantiate: (di) => di.inject(applicationInformationToken).config.sentryDsn,
});
export default sentryDataSourceNameInjectable;
@@ -5,5 +5,4 @@
import { getGlobalOverride } from "../test-utils/get-global-override";
import staticFilesDirectoryInjectable from "./static-files-directory.injectable";
export default getGlobalOverride(staticFilesDirectoryInjectable, () => "/some-static-directory");
@@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationInformationInjectable from "./application-information.injectable";
import applicationInformationToken from "./application-information-token";
const storeMigrationVersionInjectable = getInjectable({
id: "store-migration-version",
instantiate: (di) => di.inject(applicationInformationInjectable).version,
instantiate: (di) => di.inject(applicationInformationToken).version,
});
export default storeMigrationVersionInjectable;
+43 -1
View File
@@ -15,6 +15,12 @@ import type { ResourceApplyingStack } from "../../common/k8s/resource-stack";
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import type { KubernetesCluster } from "./catalog";
import type { KubeApiDataFrom, KubeObjectStoreOptions } from "../../common/k8s-api/kube-object.store";
import { KubeObjectStore as InternalKubeObjectStore } from "../../common/k8s-api/kube-object.store";
import type { KubeJsonApiDataFor, KubeObject } from "../../common/k8s-api/kube-object";
import type { KubeApi } from "../../common/k8s-api/kube-api";
import clusterFrameContextForNamespacedResourcesInjectable from "../../renderer/cluster-frame-context/for-namespaced-resources.injectable";
import type { ClusterContext } from "../../renderer/cluster-frame-context/cluster-frame-context";
export const apiManager = asLegacyGlobalForExtensionApi(apiManagerInjectable);
export const forCluster = asLegacyGlobalFunctionForExtensionApi(createKubeApiForClusterInjectable);
@@ -72,8 +78,44 @@ export {
type KubeJsonApiData,
} from "../../common/k8s-api/kube-json-api";
export abstract class KubeObjectStore<
K extends KubeObject = KubeObject,
A extends KubeApi<K, D> = KubeApi<K, KubeJsonApiDataFor<K>>,
D extends KubeJsonApiDataFor<K> = KubeApiDataFrom<K, A>,
> extends InternalKubeObjectStore<K, A, D> {
/**
* @deprecated This is no longer used and shouldn't have been every really used
*/
static readonly context = {
set: (ctx: ClusterContext) => {
console.warn("Setting KubeObjectStore.context is no longer supported");
void ctx;
},
get: () => asLegacyGlobalForExtensionApi(clusterFrameContextForNamespacedResourcesInjectable),
};
get context() {
return this.dependencies.context;
}
constructor(api: A, opts?: KubeObjectStoreOptions);
/**
* @deprecated Supply API instance through constructor
*/
constructor();
constructor(api?: A, opts?: KubeObjectStoreOptions) {
super(
{
context: asLegacyGlobalForExtensionApi(clusterFrameContextForNamespacedResourcesInjectable),
},
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api!,
opts,
);
}
}
export {
KubeObjectStore,
type JsonPatch,
type KubeObjectStoreLoadAllParams,
type KubeObjectStoreLoadingParams,
@@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { LensExtensionConstructor, LensExtensionManifest } from "../lens-extension";
export interface BundledExtension {
readonly manifest: LensExtensionManifest;
main: () => LensExtensionConstructor | null;
renderer: () => LensExtensionConstructor | null;
}
export const bundledExtensionInjectionToken = getInjectionToken<BundledExtension>({
id: "bundled-extension-path",
});
@@ -27,7 +27,6 @@ import getRelativePathInjectable from "../../common/path/get-relative-path.injec
import joinPathsInjectable from "../../common/path/join-paths.injectable";
import removePathInjectable from "../../common/fs/remove.injectable";
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
import applicationInformationInjectable from "../../common/vars/application-information.injectable";
import lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable";
const extensionDiscoveryInjectable = getInjectable({
@@ -58,7 +57,6 @@ const extensionDiscoveryInjectable = getInjectable({
getRelativePath: di.inject(getRelativePathInjectable),
joinPaths: di.inject(joinPathsInjectable),
homeDirectoryPath: di.inject(homeDirectoryPathInjectable),
applicationInformation: di.inject(applicationInformationInjectable),
}),
});
@@ -30,7 +30,6 @@ import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"
import type { GetRelativePath } from "../../common/path/get-relative-path.injectable";
import type { RemovePath } from "../../common/fs/remove.injectable";
import type TypedEventEmitter from "typed-emitter";
import type { ApplicationInformation } from "../../common/vars/application-information.injectable";
interface Dependencies {
readonly extensionLoader: ExtensionLoader;
@@ -42,7 +41,6 @@ interface Dependencies {
readonly isProduction: boolean;
readonly fileSystemSeparator: string;
readonly homeDirectoryPath: string;
readonly applicationInformation: ApplicationInformation;
isCompatibleExtension: (manifest: LensExtensionManifest) => boolean;
installExtension: (name: string) => Promise<void>;
readJsonFile: ReadJson;
@@ -384,42 +382,16 @@ export class ExtensionDiscovery {
}
async ensureExtensions(): Promise<Map<LensExtensionId, InstalledExtension>> {
const bundledExtensions = await this.loadBundledExtensions();
const userExtensions = await this.loadFromFolder(this.localFolderPath, bundledExtensions.map((extension) => extension.manifest.name));
const extensions = bundledExtensions.concat(userExtensions);
const userExtensions = await this.loadFromFolder(this.localFolderPath);
return this.extensions = new Map(extensions.map(extension => [extension.id, extension]));
return this.extensions = new Map(userExtensions.map(extension => [extension.id, extension]));
}
async loadBundledExtensions(): Promise<InstalledExtension[]> {
const extensions: InstalledExtension[] = [];
const extensionNames = this.dependencies.applicationInformation.config.extensions || [];
for (const dirName of extensionNames) {
const absPath = this.dependencies.joinPaths(__dirname, "..", "..", "node_modules", dirName);
const extension = await this.loadExtensionFromFolder(absPath, { isBundled: true });
if (!extension) {
throw new Error(`Couldn't load bundled extension: ${dirName}`);
}
extensions.push(extension);
}
this.dependencies.logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { extensions });
return extensions;
}
async loadFromFolder(folderPath: string, bundledExtensions: string[]): Promise<InstalledExtension[]> {
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
const extensions: InstalledExtension[] = [];
const paths = await this.dependencies.readDirectory(folderPath);
for (const fileName of paths) {
// do not allow to override bundled extensions
if (bundledExtensions.includes(fileName)) {
continue;
}
const absPath = this.dependencies.joinPaths(folderPath, fileName);
try {
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
export const extensionEntryPointNameInjectionToken = getInjectionToken<"main" | "renderer">({
id: "extension-entry-point-name-token",
});
@@ -12,6 +12,8 @@ import extensionInjectable from "./extension/extension.injectable";
import loggerInjectable from "../../common/logger.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
import { bundledExtensionInjectionToken } from "../extension-discovery/bundled-extension-token";
import { extensionEntryPointNameInjectionToken } from "./entry-point-name";
const extensionLoaderInjectable = getInjectable({
id: "extension-loader",
@@ -21,6 +23,8 @@ const extensionLoaderInjectable = getInjectable({
createExtensionInstance: di.inject(createExtensionInstanceInjectionToken),
extensionInstances: di.inject(extensionInstancesInjectable),
getExtension: (instance: LensExtension) => di.inject(extensionInjectable, instance),
bundledExtensions: di.injectMany(bundledExtensionInjectionToken),
extensionEntryPointName: di.inject(extensionEntryPointNameInjectionToken),
logger: di.inject(loggerInjectable),
joinPaths: di.inject(joinPathsInjectable),
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
@@ -21,12 +21,15 @@ import type { Extension } from "./extension/extension.injectable";
import type { Logger } from "../../common/logger";
import type { JoinPaths } from "../../common/path/join-paths.injectable";
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
import type { BundledExtension } from "../extension-discovery/bundled-extension-token";
const logModule = "[EXTENSIONS-LOADER]";
interface Dependencies {
readonly extensionInstances: ObservableMap<LensExtensionId, LensExtension>;
readonly bundledExtensions: BundledExtension[];
readonly logger: Logger;
readonly extensionEntryPointName: "main" | "renderer";
updateExtensionsState: (extensionsState: Record<LensExtensionId, LensExtensionState>) => void;
createExtensionInstance: CreateExtensionInstance;
getExtension: (instance: LensExtension) => Extension;
@@ -34,6 +37,12 @@ interface Dependencies {
getDirnameOfPath: GetDirnameOfPath;
}
interface ExtensionBeingActivated {
instance: LensExtension;
installedExtension: InstalledExtension;
activated: Promise<void>;
}
export interface ExtensionLoading {
isBundled: boolean;
loaded: Promise<void>;
@@ -248,14 +257,84 @@ export class ExtensionLoader {
});
}
protected async loadExtensions(installedExtensions: Map<string, InstalledExtension>) {
protected async loadBundledExtensions() {
return this.dependencies.bundledExtensions
.map(extension => {
try {
const LensExtensionClass = extension[this.dependencies.extensionEntryPointName]();
if (!LensExtensionClass) {
return null;
}
const installedExtension: InstalledExtension = {
absolutePath: "irrelevant",
id: extension.manifest.name,
isBundled: true,
isCompatible: true,
isEnabled: true,
manifest: extension.manifest,
manifestPath: "irrelevant",
};
const instance = this.dependencies.createExtensionInstance(
LensExtensionClass,
installedExtension,
);
this.dependencies.extensionInstances.set(extension.manifest.name, instance);
return {
instance,
installedExtension,
activated: instance.activate(),
} as ExtensionBeingActivated;
} catch (err) {
this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err });
return null;
}
})
.filter(isDefined);
}
protected async loadExtensions(extensions: ExtensionBeingActivated[]): Promise<ExtensionLoading[]> {
// We first need to wait until each extension's `onActivate` is resolved or rejected,
// as this might register new catalog categories. Afterwards we can safely .enable the extension.
await Promise.all(
extensions.map(extension =>
// If extension activation fails, log error
extension.activated.catch((error) => {
this.dependencies.logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error });
}),
),
);
extensions.forEach(({ instance }) => {
const extension = this.dependencies.getExtension(instance);
extension.register();
});
return extensions.map(extension => {
const loaded = extension.instance.enable().catch((err) => {
this.dependencies.logger.error(`${logModule}: failed to enable`, { ext: extension, err });
});
return {
isBundled: extension.installedExtension.isBundled,
loaded,
};
});
}
protected async loadUserExtensions(installedExtensions: Map<string, InstalledExtension>) {
// Steps of the function:
// 1. require and call .activate for each Extension
// 2. Wait until every extension's onActivate has been resolved
// 3. Call .enable for each extension
// 4. Return ExtensionLoading[]
const extensions = [...installedExtensions.entries()]
return [...installedExtensions.entries()]
.map(([extId, extension]) => {
const alreadyInit = this.dependencies.extensionInstances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
@@ -280,7 +359,7 @@ export class ExtensionLoader {
instance,
installedExtension: extension,
activated: instance.activate(),
};
} as ExtensionBeingActivated;
} catch (err) {
this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err });
}
@@ -290,52 +369,33 @@ export class ExtensionLoader {
return null;
})
// Remove null values
.filter(isDefined);
// We first need to wait until each extension's `onActivate` is resolved or rejected,
// as this might register new catalog categories. Afterwards we can safely .enable the extension.
await Promise.all(
extensions.map(extension =>
// If extension activation fails, log error
extension.activated.catch((error) => {
this.dependencies.logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error });
}),
),
);
extensions.forEach(({ instance }) => {
const extension = this.dependencies.getExtension(instance);
extension.register();
});
// Return ExtensionLoading[]
return extensions.map(extension => {
const loaded = extension.instance.enable().catch((err) => {
this.dependencies.logger.error(`${logModule}: failed to enable`, { ext: extension, err });
});
return {
isBundled: extension.installedExtension.isBundled,
loaded,
};
});
}
autoInitExtensions() {
async autoInitExtensions() {
this.dependencies.logger.info(`${logModule}: auto initializing extensions`);
// Setup reaction to load extensions on JSON changes
reaction(() => this.toJSON(), installedExtensions => this.loadExtensions(installedExtensions));
const bundledExtensions = await this.loadBundledExtensions();
const userExtensions = await this.loadUserExtensions(this.toJSON());
const loadedExtensions = await this.loadExtensions([
...bundledExtensions,
...userExtensions,
]);
// Load initial extensions
return this.loadExtensions(this.toJSON());
// Setup reaction to load extensions on JSON changes
reaction(() => this.toJSON(), installedExtensions => {
void (async () => {
const userExtensions = await this.loadUserExtensions(installedExtensions);
await this.loadExtensions(userExtensions);
})();
});
return loadedExtensions;
}
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null {
const entryPointName = ipcRenderer ? "renderer" : "main";
const extRelativePath = extension.manifest[entryPointName];
const extRelativePath = extension.manifest[this.dependencies.extensionEntryPointName];
if (!extRelativePath) {
return null;
@@ -344,11 +404,11 @@ export class ExtensionLoader {
const extAbsolutePath = this.dependencies.joinPaths(this.dependencies.getDirnameOfPath(extension.manifestPath), extRelativePath);
try {
return __non_webpack_require__(extAbsolutePath).default;
return require(/* webpackIgnore: true */ extAbsolutePath).default;
} catch (error) {
const message = (error instanceof Error ? error.stack : undefined) || error;
this.dependencies.logger.error(`${logModule}: can't load ${entryPointName} for "${extension.manifest.name}": ${message}`, { extension });
this.dependencies.logger.error(`${logModule}: can't load ${this.dependencies.extensionEntryPointName} for "${extension.manifest.name}": ${message}`, { extension });
}
return null;
-2
View File
@@ -1,2 +0,0 @@
dist/
__mocks__/
@@ -1,31 +0,0 @@
{
"name": "@k8slens/extensions",
"productName": "OpenLens extensions",
"description": "OpenLens - Open Source Kubernetes IDE: extensions",
"version": "6.0.0",
"copyright": "© 2022 OpenLens Authors",
"license": "MIT",
"main": "dist/src/extensions/extension-api.js",
"types": "dist/src/extensions/extension-api.d.ts",
"files": [
"dist/**/*.ts",
"__mocks__/*.ts",
"dist/**/*.js"
],
"author": {
"name": "OpenLens Authors"
},
"dependencies": {
"@material-ui/core": "^4.12.3",
"@types/node": "^16.11.58",
"@types/react": "^17.0.45",
"@types/react-dom": "^17.0.16",
"@types/react-router": "^5.1.18",
"@types/react-router-dom": "^5.3.3",
"conf": "^7.0.1",
"mobx": "^6.5.0",
"mobx-react": "^7.3.0",
"react-select": "^5.3.2",
"typed-emitter": "^1.3.1"
}
}
+14 -6
View File
@@ -3,8 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeResource } from "../../common/rbac";
import isAllowedResourceInjectable from "../../common/utils/is-allowed-resource.injectable";
import { castArray } from "lodash/fp";
import { apiResourceRecord } from "../../common/rbac";
import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import clusterRoleBindingApiInjectable from "../../common/k8s-api/endpoints/cluster-role-binding.api.injectable";
import clusterRoleApiInjectable from "../../common/k8s-api/endpoints/cluster-role.api.injectable";
@@ -37,13 +36,22 @@ import namespaceApiInjectable from "../../common/k8s-api/endpoints/namespace.api
import kubeEventApiInjectable from "../../common/k8s-api/endpoints/events.api.injectable";
import roleBindingApiInjectable from "../../common/k8s-api/endpoints/role-binding.api.injectable";
import customResourceDefinitionApiInjectable from "../../common/k8s-api/endpoints/custom-resource-definition.api.injectable";
import { shouldShowResourceInjectionToken } from "../../common/cluster-store/allowed-resources-injection-token";
export function isAllowedResource(resource: KubeResource | KubeResource[]) {
const resources = castArray(resource);
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
const di = getLegacyGlobalDiForExtensionApi();
return resources.every((resourceName: any) => {
const _isAllowedResource = di.inject(isAllowedResourceInjectable, resourceName);
return [resources].flat().every((resourceName) => {
const resource = apiResourceRecord[resourceName];
if (!resource) {
return true;
}
const _isAllowedResource = di.inject(shouldShowResourceInjectionToken, {
apiName: resourceName,
group: resource.group,
});
// Note: Legacy isAllowedResource does not advertise reactivity
return _isAllowedResource.get();
@@ -33,6 +33,7 @@
height: 100%;
left: 0;
opacity: 0.15;
z-index: -1;
transition: opacity 0.1s;
}
}
@@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationInformationInjectable from "../../../../../common/vars/application-information.injectable";
import applicationInformationToken from "../../../../../common/vars/application-information-token";
const publishIsConfiguredInjectable = getInjectable({
id: "publish-is-configured",
instantiate: (di) => Boolean(di.inject(applicationInformationInjectable).build.publish?.length),
instantiate: (di) => Boolean(di.inject(applicationInformationToken).build.publish?.length),
});
export default publishIsConfiguredInjectable;
@@ -10,8 +10,8 @@ import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injec
import type { Cluster } from "../../common/cluster/cluster";
import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable";
import createClusterInjectable from "../../renderer/cluster/create-cluster.injectable";
import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import createClusterInjectable from "../../renderer/create-cluster/create-cluster.injectable";
describe("opening catalog entity details panel", () => {
let builder: ApplicationBuilder;
@@ -14,7 +14,6 @@ import kubectlBinaryNameInjectable from "../../../main/kubectl/binary-name.injec
import kubectlDownloadingNormalizedArchInjectable from "../../../main/kubectl/normalized-arch.injectable";
import openDeleteClusterDialogInjectable, { type OpenDeleteClusterDialog } from "../../../renderer/components/delete-cluster-dialog/open.injectable";
import { type ApplicationBuilder, getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
@@ -90,7 +89,6 @@ describe("Deleting a cluster", () => {
});
builder.beforeWindowStart((windowDi) => {
windowDi.override(storesAndApisCanBeCreatedInjectable, () => true);
openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable);
});
Loaded 100 of 363 files, more files were not shown because too many files have changed in this diff. Show more