mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
fix: getAllowedResources for all namespaces using SelfSubjectRulesReview (#6614)
* fix: getAllowedResources for all namespaces using SelfSubjectRulesReview Signed-off-by: Andreas Hippler <andreas.hippler@goto.com> * fix: refresh accessibility every 15 min Signed-off-by: Andreas Hippler <andreas.hippler@goto.com> * chore: remove unused clusterRefreshHandler Signed-off-by: Andreas Hippler <andreas.hippler@goto.com> * fix: resolve SelfSubjectRulesReview globs Signed-off-by: Andreas Hippler <andreas.hippler@goto.com> Signed-off-by: Andreas Hippler <andreas.hippler@goto.com> Co-authored-by: Andreas Hippler <andreas.hippler@goto.com>
This commit is contained in:
parent
612538d9fc
commit
6d7090f8a7
@ -390,12 +390,6 @@ const scenarios = [
|
|||||||
sidebarItemTestId: "sidebar-item-link-for-service-accounts",
|
sidebarItemTestId: "sidebar-item-link-for-service-accounts",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
expectedSelector: "h5.title",
|
|
||||||
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
|
||||||
sidebarItemTestId: "sidebar-item-link-for-roles",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
expectedSelector: "h5.title",
|
expectedSelector: "h5.title",
|
||||||
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
||||||
@ -405,7 +399,7 @@ const scenarios = [
|
|||||||
{
|
{
|
||||||
expectedSelector: "h5.title",
|
expectedSelector: "h5.title",
|
||||||
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
||||||
sidebarItemTestId: "sidebar-item-link-for-role-bindings",
|
sidebarItemTestId: "sidebar-item-link-for-roles",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -417,7 +411,7 @@ const scenarios = [
|
|||||||
{
|
{
|
||||||
expectedSelector: "h5.title",
|
expectedSelector: "h5.title",
|
||||||
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
|
||||||
sidebarItemTestId: "sidebar-item-link-for-pod-security-policies",
|
sidebarItemTestId: "sidebar-item-link-for-role-bindings",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -195,13 +195,6 @@ export enum ClusterMetricsResourceType {
|
|||||||
*/
|
*/
|
||||||
export const initialNodeShellImage = "docker.io/alpine:3.13";
|
export const initialNodeShellImage = "docker.io/alpine:3.13";
|
||||||
|
|
||||||
/**
|
|
||||||
* The arguments for requesting to refresh a cluster's metadata
|
|
||||||
*/
|
|
||||||
export interface ClusterRefreshOptions {
|
|
||||||
refreshMetadata?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data representing a cluster's state, for passing between main and renderer
|
* The data representing a cluster's state, for passing between main and renderer
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
@ -5,42 +5,55 @@
|
|||||||
|
|
||||||
import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||||
import { AuthorizationV1Api } from "@kubernetes/client-node";
|
import { AuthorizationV1Api } from "@kubernetes/client-node";
|
||||||
import logger from "../logger";
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { Logger } from "../logger";
|
||||||
|
import loggerInjectable from "../logger.injectable";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the permissions for actions on the kube cluster
|
||||||
|
* @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed
|
||||||
|
* @returns `true` if the actions described are allowed
|
||||||
|
*/
|
||||||
export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise<boolean>;
|
export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
|
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
|
||||||
*/
|
*/
|
||||||
export function authorizationReview(proxyConfig: KubeConfig): CanI {
|
export type AuthorizationReview = (proxyConfig: KubeConfig) => CanI;
|
||||||
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
|
|
||||||
|
|
||||||
/**
|
interface Dependencies {
|
||||||
* Requests the permissions for actions on the kube cluster
|
logger: Logger;
|
||||||
* @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed
|
|
||||||
* @returns `true` if the actions described are allowed
|
|
||||||
*/
|
|
||||||
return async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const { body } = await api.createSelfSubjectAccessReview({
|
|
||||||
apiVersion: "authorization.k8s.io/v1",
|
|
||||||
kind: "SelfSubjectAccessReview",
|
|
||||||
spec: { resourceAttributes },
|
|
||||||
});
|
|
||||||
|
|
||||||
return body.status?.allowed ?? false;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes });
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authorizationReview = ({ logger }: Dependencies): AuthorizationReview => {
|
||||||
|
return (proxyConfig) => {
|
||||||
|
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
|
||||||
|
|
||||||
|
return async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const { body } = await api.createSelfSubjectAccessReview({
|
||||||
|
apiVersion: "authorization.k8s.io/v1",
|
||||||
|
kind: "SelfSubjectAccessReview",
|
||||||
|
spec: { resourceAttributes },
|
||||||
|
});
|
||||||
|
|
||||||
|
return body.status?.allowed ?? false;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes });
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const authorizationReviewInjectable = getInjectable({
|
const authorizationReviewInjectable = getInjectable({
|
||||||
id: "authorization-review",
|
id: "authorization-review",
|
||||||
instantiate: () => authorizationReview,
|
instantiate: (di) => {
|
||||||
|
const logger = di.inject(loggerInjectable);
|
||||||
|
|
||||||
|
return authorizationReview({ logger });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default authorizationReviewInjectable;
|
export default authorizationReviewInjectable;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { apiResourceRecord, apiResources } from "../rbac";
|
|||||||
import type { VersionDetector } from "../../main/cluster-detectors/version-detector";
|
import type { VersionDetector } from "../../main/cluster-detectors/version-detector";
|
||||||
import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
|
import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
|
||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types";
|
import type { ClusterState, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types";
|
||||||
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus, clusterModelIdChecker, updateClusterModelChecker } from "../cluster-types";
|
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus, clusterModelIdChecker, updateClusterModelChecker } from "../cluster-types";
|
||||||
import { disposer, isDefined, isRequestError, toJS } from "../utils";
|
import { disposer, isDefined, isRequestError, toJS } from "../utils";
|
||||||
import type { Response } from "request";
|
import type { Response } from "request";
|
||||||
@ -25,6 +25,8 @@ import assert from "assert";
|
|||||||
import type { Logger } from "../logger";
|
import type { Logger } from "../logger";
|
||||||
import type { BroadcastMessage } from "../ipc/broadcast-message.injectable";
|
import type { BroadcastMessage } from "../ipc/broadcast-message.injectable";
|
||||||
import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.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";
|
||||||
|
|
||||||
export interface ClusterDependencies {
|
export interface ClusterDependencies {
|
||||||
readonly directoryForKubeConfigs: string;
|
readonly directoryForKubeConfigs: string;
|
||||||
@ -34,6 +36,8 @@ export interface ClusterDependencies {
|
|||||||
createContextHandler: (cluster: Cluster) => ClusterContextHandler;
|
createContextHandler: (cluster: Cluster) => ClusterContextHandler;
|
||||||
createKubectl: (clusterVersion: string) => Kubectl;
|
createKubectl: (clusterVersion: string) => Kubectl;
|
||||||
createAuthorizationReview: (config: KubeConfig) => CanI;
|
createAuthorizationReview: (config: KubeConfig) => CanI;
|
||||||
|
createAuthorizationNamespaceReview: (config: KubeConfig) => RequestNamespaceResources;
|
||||||
|
createListApiResources: (cluster: Cluster) => RequestListApiResources;
|
||||||
createListNamespaces: (config: KubeConfig) => ListNamespaces;
|
createListNamespaces: (config: KubeConfig) => ListNamespaces;
|
||||||
createVersionDetector: (cluster: Cluster) => VersionDetector;
|
createVersionDetector: (cluster: Cluster) => VersionDetector;
|
||||||
broadcastMessage: BroadcastMessage;
|
broadcastMessage: BroadcastMessage;
|
||||||
@ -309,7 +313,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
protected bindEvents() {
|
protected bindEvents() {
|
||||||
this.dependencies.logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
this.dependencies.logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
||||||
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
|
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
|
||||||
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
|
const refreshMetadataTimer = setInterval(() => this.available && this.refreshAccessibilityAndMetadata(), 900000); // every 15 minutes
|
||||||
|
|
||||||
this.eventsDisposer.push(
|
this.eventsDisposer.push(
|
||||||
reaction(() => this.getState(), state => this.pushState(state)),
|
reaction(() => this.getState(), state => this.pushState(state)),
|
||||||
@ -439,66 +443,68 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
* @param opts refresh options
|
|
||||||
*/
|
*/
|
||||||
@action
|
@action
|
||||||
async refresh(opts: ClusterRefreshOptions = {}) {
|
async refresh() {
|
||||||
this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
|
|
||||||
if (this.accessible) {
|
|
||||||
await this.refreshAccessibility();
|
|
||||||
|
|
||||||
if (opts.refreshMetadata) {
|
|
||||||
this.refreshMetadata();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.pushState();
|
this.pushState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action
|
@action
|
||||||
async refreshMetadata() {
|
async refreshAccessibilityAndMetadata() {
|
||||||
this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
await this.refreshAccessibility();
|
||||||
const metadata = await this.dependencies.detectorRegistry.detectForCluster(this);
|
await this.refreshMetadata();
|
||||||
const existingMetadata = this.metadata;
|
|
||||||
|
|
||||||
this.metadata = Object.assign(existingMetadata, metadata);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
private async refreshAccessibility(): Promise<void> {
|
async refreshMetadata() {
|
||||||
const proxyConfig = await this.getProxyKubeconfig();
|
this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
||||||
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
|
const metadata = await this.dependencies.detectorRegistry.detectForCluster(this);
|
||||||
|
const existingMetadata = this.metadata;
|
||||||
|
|
||||||
this.isAdmin = await canI({
|
this.metadata = Object.assign(existingMetadata, metadata);
|
||||||
namespace: "kube-system",
|
}
|
||||||
resource: "*",
|
|
||||||
verb: "create",
|
/**
|
||||||
});
|
* @internal
|
||||||
this.isGlobalWatchEnabled = await canI({
|
*/
|
||||||
verb: "watch",
|
private async refreshAccessibility(): Promise<void> {
|
||||||
resource: "*",
|
this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.getMeta());
|
||||||
});
|
const proxyConfig = await this.getProxyKubeconfig();
|
||||||
this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig);
|
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
|
||||||
this.allowedResources = await this.getAllowedResources(canI);
|
const requestNamespaceResources = this.dependencies.createAuthorizationNamespaceReview(proxyConfig);
|
||||||
this.ready = true;
|
const listApiResources = this.dependencies.createListApiResources(this);
|
||||||
}
|
|
||||||
|
this.isAdmin = await canI({
|
||||||
|
namespace: "kube-system",
|
||||||
|
resource: "*",
|
||||||
|
verb: "create",
|
||||||
|
});
|
||||||
|
this.isGlobalWatchEnabled = await canI({
|
||||||
|
verb: "watch",
|
||||||
|
resource: "*",
|
||||||
|
});
|
||||||
|
this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig);
|
||||||
|
this.allowedResources = await this.getAllowedResources(listApiResources, requestNamespaceResources);
|
||||||
|
this.ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action
|
@action
|
||||||
async refreshConnectionStatus() {
|
async refreshConnectionStatus() {
|
||||||
const connectionStatus = await this.getConnectionStatus();
|
const connectionStatus = await this.getConnectionStatus();
|
||||||
|
|
||||||
this.online = connectionStatus > ClusterStatus.Offline;
|
this.online = connectionStatus > ClusterStatus.Offline;
|
||||||
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getKubeconfig(): Promise<KubeConfig> {
|
async getKubeconfig(): Promise<KubeConfig> {
|
||||||
const { config } = await this.dependencies.loadConfigfromFile(this.kubeConfigPath);
|
const { config } = await this.dependencies.loadConfigfromFile(this.kubeConfigPath);
|
||||||
@ -667,32 +673,48 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getAllowedResources(canI: CanI) {
|
protected async getAllowedResources(listApiResources:RequestListApiResources, requestNamespaceResources: RequestNamespaceResources) {
|
||||||
try {
|
try {
|
||||||
if (!this.allowedNamespaces.length) {
|
if (!this.allowedNamespaces.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const resources = apiResources.filter((resource) => this.resourceAccessStatuses.get(resource) === undefined);
|
|
||||||
const apiLimit = plimit(5); // 5 concurrent api requests
|
|
||||||
const requests = [];
|
|
||||||
|
|
||||||
for (const apiResource of resources) {
|
const unknownResources = new Map<string, KubeApiResource>(apiResources.map(resource => ([resource.apiName, resource])));
|
||||||
requests.push(apiLimit(async () => {
|
|
||||||
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
|
|
||||||
if (!this.resourceAccessStatuses.get(apiResource)) {
|
|
||||||
const result = await canI({
|
|
||||||
resource: apiResource.apiName,
|
|
||||||
group: apiResource.group,
|
|
||||||
verb: "list",
|
|
||||||
namespace,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.resourceAccessStatuses.set(apiResource, result);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await Promise.all(requests);
|
|
||||||
|
|
||||||
return apiResources
|
return apiResources
|
||||||
.filter((resource) => this.resourceAccessStatuses.get(resource))
|
.filter((resource) => this.resourceAccessStatuses.get(resource))
|
||||||
|
|||||||
91
src/common/cluster/list-api-resources.injectable.ts
Normal file
91
src/common/cluster/list-api-resources.injectable.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
export const clusterActivateHandler = "cluster:activate";
|
export const clusterActivateHandler = "cluster:activate";
|
||||||
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
|
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
|
||||||
export const clusterRefreshHandler = "cluster:refresh";
|
export const clusterVisibilityHandler = "cluster:visibility";
|
||||||
export const clusterDisconnectHandler = "cluster:disconnect";
|
export const clusterDisconnectHandler = "cluster:disconnect";
|
||||||
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
||||||
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
|
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
|||||||
import type { CreateCluster } from "../../common/cluster/create-cluster-injection-token";
|
import type { CreateCluster } from "../../common/cluster/create-cluster-injection-token";
|
||||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||||
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
||||||
|
import authorizationNamespaceReviewInjectable from "../../common/cluster/authorization-namespace-review.injectable";
|
||||||
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
||||||
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||||
import type { ClusterContextHandler } from "../context-handler/context-handler";
|
import type { ClusterContextHandler } from "../context-handler/context-handler";
|
||||||
@ -19,6 +20,8 @@ import directoryForTempInjectable from "../../common/app-paths/directory-for-tem
|
|||||||
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
|
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
|
||||||
import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable";
|
import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable";
|
||||||
import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable";
|
import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable";
|
||||||
|
import { apiResourceRecord, apiResources } from "../../common/rbac";
|
||||||
|
import listApiResourcesInjectable from "../../common/cluster/list-api-resources.injectable";
|
||||||
|
|
||||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||||
|
|
||||||
@ -39,6 +42,8 @@ describe("create clusters", () => {
|
|||||||
di.override(normalizedPlatformInjectable, () => "darwin");
|
di.override(normalizedPlatformInjectable, () => "darwin");
|
||||||
di.override(broadcastMessageInjectable, () => async () => {});
|
di.override(broadcastMessageInjectable, () => async () => {});
|
||||||
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
|
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
|
||||||
|
di.override(authorizationNamespaceReviewInjectable, () => () => () => Promise.resolve(Object.keys(apiResourceRecord)));
|
||||||
|
di.override(listApiResourcesInjectable, () => () => () => Promise.resolve(apiResources));
|
||||||
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
||||||
di.override(createContextHandlerInjectable, () => (cluster) => ({
|
di.override(createContextHandlerInjectable, () => (cluster) => ({
|
||||||
restartServer: jest.fn(),
|
restartServer: jest.fn(),
|
||||||
|
|||||||
@ -11,7 +11,9 @@ import createKubectlInjectable from "../kubectl/create-kubectl.injectable";
|
|||||||
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||||
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
||||||
|
import createAuthorizationNamespaceReview from "../../common/cluster/authorization-namespace-review.injectable";
|
||||||
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
||||||
|
import createListApiResourcesInjectable from "../../common/cluster/list-api-resources.injectable";
|
||||||
import loggerInjectable from "../../common/logger.injectable";
|
import loggerInjectable from "../../common/logger.injectable";
|
||||||
import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable";
|
import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable";
|
||||||
import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable";
|
import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable";
|
||||||
@ -28,6 +30,8 @@ const createClusterInjectable = getInjectable({
|
|||||||
createKubectl: di.inject(createKubectlInjectable),
|
createKubectl: di.inject(createKubectlInjectable),
|
||||||
createContextHandler: di.inject(createContextHandlerInjectable),
|
createContextHandler: di.inject(createContextHandlerInjectable),
|
||||||
createAuthorizationReview: di.inject(authorizationReviewInjectable),
|
createAuthorizationReview: di.inject(authorizationReviewInjectable),
|
||||||
|
createAuthorizationNamespaceReview: di.inject(createAuthorizationNamespaceReview),
|
||||||
|
createListApiResources: di.inject(createListApiResourcesInjectable),
|
||||||
createListNamespaces: di.inject(listNamespacesInjectable),
|
createListNamespaces: di.inject(listNamespacesInjectable),
|
||||||
logger: di.inject(loggerInjectable),
|
logger: di.inject(loggerInjectable),
|
||||||
detectorRegistry: di.inject(detectorRegistryInjectable),
|
detectorRegistry: di.inject(detectorRegistryInjectable),
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
import type { IpcMainInvokeEvent } from "electron";
|
import type { IpcMainInvokeEvent } from "electron";
|
||||||
import { BrowserWindow, Menu } from "electron";
|
import { BrowserWindow, Menu } from "electron";
|
||||||
import { clusterFrameMap } from "../../../../common/cluster-frames";
|
import { clusterFrameMap } from "../../../../common/cluster-frames";
|
||||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../../../../common/ipc/cluster";
|
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../../../../common/ipc/cluster";
|
||||||
import type { ClusterId } from "../../../../common/cluster-types";
|
import type { ClusterId } from "../../../../common/cluster-types";
|
||||||
import { ClusterStore } from "../../../../common/cluster-store/cluster-store";
|
import { ClusterStore } from "../../../../common/cluster-store/cluster-store";
|
||||||
import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../../common/ipc";
|
import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../../common/ipc";
|
||||||
@ -61,12 +61,6 @@ export const setupIpcMainHandlers = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
|
||||||
return ClusterStore.getInstance()
|
|
||||||
.getById(clusterId)
|
|
||||||
?.refresh({ refreshMetadata: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMainHandle(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
|
ipcMainHandle(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
|
||||||
emitAppEvent({ name: "cluster", action: "stop" });
|
emitAppEvent({ name: "cluster", action: "stop" });
|
||||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||||
|
|||||||
@ -27,7 +27,9 @@ const createClusterInjectable = getInjectable({
|
|||||||
createKubectl: () => { throw new Error("Tried to access back-end feature in front-end.");},
|
createKubectl: () => { throw new Error("Tried to access back-end feature in front-end.");},
|
||||||
createContextHandler: () => undefined as never,
|
createContextHandler: () => undefined as never,
|
||||||
createAuthorizationReview: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
createAuthorizationReview: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
||||||
|
createAuthorizationNamespaceReview: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
||||||
createListNamespaces: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
createListNamespaces: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
||||||
|
createListApiResources: ()=> { throw new Error("Tried to access back-end feature in front-end."); },
|
||||||
detectorRegistry: undefined as never,
|
detectorRegistry: undefined as never,
|
||||||
createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user