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

Merge remote-tracking branch 'origin/master' into mobx6-migration

# Conflicts:
#	src/renderer/components/+user-management/user-management.tsx
#	src/renderer/components/layout/sidebar.tsx
#	yarn.lock
This commit is contained in:
Roman 2021-05-12 17:19:26 +03:00
commit 16d4fb1818
37 changed files with 360 additions and 441 deletions

View File

@ -0,0 +1,5 @@
Fixes #
**Description of changes:**
-

View File

@ -0,0 +1,13 @@
## Changes since v
## 🚀 Features
*
## 🐛 Bug Fixes
*
## 🧰 Maintenance
*

View File

@ -41,6 +41,10 @@ lint:
release-version:
npm version $(CMD_ARGS) --git-tag-version false
.PHONY: tag-release
tag-release:
scripts/tag-release.sh
.PHONY: test
test: binaries/client
yarn run jest $(or $(CMD_ARGS), "src")

View File

@ -2,10 +2,20 @@
Lens releases are built by CICD automatically on git tags. The typical release process flow is the following:
1. Create a release branch `release/v{version}` from `master` branch or from existing release branch (for example, `release/v3.5`) on patch releases.
2. Update changelog in `static/RELEASE_NOTES.md` and bump version in `package.json`.
3. Create PR and put change log in description field.
4. After the PR is accepted, create a tag from release branch.
5. Push tag to GitHub.
6. Publish automatically created GitHub release.
7. Merge the release PR after the release is published and delete the release branch from GitHub.
1. From a clean and up to date `master` run `make release-version <version-type>` where `<version-type>` is one of the following:
- `major`
- `minor`
- `patch`
- `premajor`
- `preminor`
- `prepatch`
- `prerelease [--preid=<prerelease-id>]`
- where `<prerelease-id>` is generally one of:
- `alpha`
- `beta`
- `rc`
1. Create PR (git should have printed a link to GitHub in the console) with the contents of all the accepted PRs since the last release.
1. After the PR is accepted and passes CI. Go to the same branch and run `make tag-release`
1. Once CI passes again go to the releases tab on GitHub, create a new release from the tag that was created, make sure that the change log is the same as that of the PR.
1. Merge the release PR after the release is published. GitHub should delete the branch once it is merged.
1. If you have just released a new major or minor version then create a new `vMAJOR.MINOR` branch from that same tag and push it to master. This will be the target for future patch releases and shouldn't be deleted.

View File

@ -1,131 +0,0 @@
# Theme Color Reference
You can use theme-based CSS Variables to style an extension according to the active theme.
## Base Colors
- `--blue`: blue color.
- `--magenta`: magenta color.
- `--golden`: gold/yellow color.
- `--halfGray`: gray with some apacity applied.
- `--primary`: Lens brand (blue) color.
- `--colorSuccess`: successfull operations color.
- `--colorOk`: successfull operations (bright version) color.
- `--colorInfo`: informational, in-progress color.
- `--colorError`: critical error color.
- `--colorSoftError`: error color.
- `--colorWarning`: warning color.
- `--colorVague`: soft gray color for notices, hints etc.
- `--colorTerminated`: terminated, closed, stale color.
- `--boxShadow`: semi-transparent box-shadow color.
## Text Colors
- `--textColorPrimary`: foreground text color.
- `--textColorSecondary`: foreground text color for different paragraps, parts of text.
- `--textColorAccent`: foreground text color to highlight its parts.
## Border Colors
- `--borderColor`: border color.
- `--borderFaintColor`: fainted (lighter or darker, which depends on the theme) border color.
## Layout Colors
- `--mainBackground`: main background color for the app.
- `--contentColor`: background color for panels contains some data.
- `--layoutBackground`: background color for layout parts.
- `--layoutTabsBackground`: background color for general tabs.
- `--layoutTabsActiveColor`: foreground color for general tabs.
- `--layoutTabsLineColor`: background color for lines under general tabs.
## Sidebar Colors
- `--sidebarLogoBackground`: background color behind logo in sidebar.
- `--sidebarActiveColor`: foreground color for active menu items in sidebar.
- `--sidebarSubmenuActiveColor`: foreground color for active submenu items in sidebar.
- `--sidebarBackground`: background color for sidebar.
## Button Colors
- `--buttonPrimaryBackground`: button background color for primary actions.
- `--buttonDefaultBackground`: default button background color.
- `--buttonLightBackground`: light button background color.
- `--buttonAccentBackground`: accent button background color.
- `--buttonDisabledBackground`: disabled button background color.
## Table Colors
- `--tableBgcStripe`: background color for odd rows in table.
- `--tableBgcSelected`: background color for selected row in table.
- `--tableHeaderBackground`: background color for table header.
- `--tableHeaderBorderWidth`: border width under table header.
- `--tableHeaderBorderColor`: border color for line under table header.
- `--tableHeaderColor`: foreground color for table header.
- `--tableSelectedRowColor`: foreground color for selected row in table.
## Dock Colors
- `--dockHeadBackground`: background color for dock's header.
- `--dockInfoBackground`: background color for dock's info panel.
- `--dockInfoBorderColor`: border color for dock's info panel.
## Helm Chart Colors
- `--helmLogoBackground`: background color for chart logo.
- `--helmImgBackground`: background color for chart image.
- `--helmStableRepo`: background color for stable repo.
- `--helmIncubatorRepo`: background color for incubator repo.
- `--helmDescriptionHr`: Helm chart description separator line color.
- `--helmDescriptionBlockqouteColor`: Helm chart description blockquote color.
- `--helmDescriptionBlockqouteBorder`: Helm chart description blockquote border color.
- `--helmDescriptionBlockquoteBackground`: Helm chart description blockquote background color.
- `--helmDescriptionHeaders`: Helm chart description headers color.
- `--helmDescriptionH6`: Helm chart description header foreground color.
- `--helmDescriptionTdBorder`: Helm chart description table cell border color.
- `--helmDescriptionTrBackground`: Helm chart description table row background color.
- `--helmDescriptionCodeBackground`: Helm chart description code background color.
- `--helmDescriptionPreBackground`: Helm chart description pre background color.
- `--helmDescriptionPreColor`: Helm chart description pre foreground color.
## Terminal Colors
- `--terminalBackground`: Terminal background color.
- `--terminalForeground`: Terminal foreground color.
- `--terminalCursor`: Terminal cursor color.
- `--terminalCursorAccent`: Terminal cursor accent color.
- `--terminalSelection`: Terminal selection background color.
- `--terminalBlack`: Terminal black color.
- `--terminalRed`: Terminal red color.
- `--terminalGreen`: Terminal green color.
- `--terminalYellow`: Terminal yellow color.
- `--terminalBlue`: Terminal blue color.
- `--terminalMagenta`: Terminal magenta color.
- `--terminalCyan`: Terminal cyan color.
- `--terminalWhite`: Terminal white color.
- `--terminalBrightBlack`: Terminal bright black color.
- `--terminalBrightRed`: Terminal bright red color.
- `--terminalBrightGreen`: Terminal bright green color.
- `--terminalBrightYellow`: Terminal bright yellow color.
- `--terminalBrightBlue`: Terminal bright blue color.
- `--terminalBrightMagenta`: Terminal bright magenta color.
- `--terminalBrightCyan`: Terminal bright cyan color.
- `--terminalBrightWhite`: Terminal bright white color.
## Dialog Colors
- `--dialogHeaderBackground`: background color for dialog header.
- `--dialogFooterBackground`: background color for dialog footer.
## Detail Panel (Drawer) Colors
- `--drawerTitleText`: drawer title foreground color.
- `--drawerSubtitleBackground`: drawer subtitle foreground color.
- `--drawerItemNameColor`: foreground color for item name in drawer.
- `--drawerItemValueColor`: foreground color for item value in drawer.
## Misc Colors
- `--logsBackground`: background color for pod logs.
- `--clusterMenuBackground`: background color for cluster menu.
- `--clusterMenuBorderColor`: border color for cluster menu.
- `--clusterSettingsBackground`: background color for cluster settings.
- `--addClusterIconColor`: add cluster button background color.
- `--iconActiveColor`: active cluster icon foreground color.
- `--iconActiveBackground`: active cluster icon background color.
- `--filterAreaBackground`: page filter area (where selected namespaces are lister) background color.
- `--chartStripesColor`: bar chart zebra stripes background color.
- `--chartCapacityColor`: background color for capacity values in bar charts.
- `--pieChartDefaultColor`: default background color for pie chart values.
- `--selectOptionHoveredColor`: foregrond color for selected element in dropdown list.
- `--lineProgressBackground`: background color for progress line.
- `--radioActiveBackground`: background color for active radio buttons.
- `--menuActiveBackground`: background color for active menu items.
In most cases you would only need base, text and some of the layout colors.

View File

@ -103,8 +103,6 @@ For example:
}
```
A complete list of theme colors can be found in the [Color Reference](../color-reference).
### Theme Switching
When the light theme is active, the `<body>` element gets a "theme-light" class, or: `<body class="theme-light">`.

View File

@ -2954,9 +2954,9 @@
}
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"html-encoding-sniffer": {
@ -4038,9 +4038,9 @@
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"lodash.sortby": {

View File

@ -2934,9 +2934,9 @@
}
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"html-encoding-sniffer": {
@ -4026,9 +4026,9 @@
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"lodash.sortby": {

View File

@ -3571,9 +3571,9 @@
}
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"html-encoding-sniffer": {
@ -4663,9 +4663,9 @@
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"lodash.sortby": {

View File

@ -27,7 +27,6 @@ nav:
- Extension Capabilities:
- Common Capabilities: extensions/capabilities/common-capabilities.md
- Styling: extensions/capabilities/styling.md
- Color Reference: extensions/capabilities/color-reference.md
- Extension Guides:
- Overview: extensions/guides/README.md
- Generator: extensions/guides/generator.md

View File

@ -43,7 +43,7 @@
"version-checkout": "cat package.json | jq '.version' -r | xargs printf \"release/v%s\" | xargs git checkout -b",
"version-commit": "cat package.json | jq '.version' -r | xargs printf \"release v%s\" | git commit --no-edit -s -F -",
"version": "yarn run version-checkout && git add package.json && yarn run version-commit",
"postversion": "git push --set-upstream origin release/v$npm_package_version"
"postversion": "git push --set-upstream ${GIT_REMOTE:-origin} release/v$npm_package_version"
},
"config": {
"bundledKubectlVersion": "1.18.15",
@ -199,7 +199,7 @@
"filenamify": "^4.1.0",
"fs-extra": "^9.0.1",
"grapheme-splitter": "^1.0.4",
"handlebars": "^4.7.6",
"handlebars": "^4.7.7",
"http-proxy": "^1.18.1",
"immer": "^8.0.1",
"js-yaml": "^3.14.0",
@ -234,7 +234,7 @@
"tar": "^6.0.5",
"tcp-port-used": "^1.0.1",
"tempy": "^0.5.0",
"url-parse": "^1.4.7",
"url-parse": "^1.5.1",
"uuid": "^8.3.2",
"win-ca": "^3.2.0",
"winston": "^3.2.1",

10
scripts/tag-release.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
if [[ ${git branch --show-current} =~ ^release/v ]]
then
VERSION_STRING=$(cat package.json | jq '.version' -r | xargs printf "v%s")
git tag ${VERSION_STRING}
git push ${GIT_REMOTE:-origin} ${VERSION_STRING}
else
echo "You must be in a release branch"
fi

View File

@ -4,42 +4,54 @@ export type KubeResource =
"namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" |
"secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" |
"pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" |
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets";
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets" |
"roles" | "clusterroles" | "rolebindings" | "clusterrolebindings" | "serviceaccounts";
export interface KubeApiResource {
kind: string; // resource type (e.g. "Namespace")
export interface KubeApiResource extends KubeApiResourceData {
apiName: KubeResource; // valid api resource name (e.g. "namespaces")
}
export interface KubeApiResourceData {
kind: string; // resource type (e.g. "Namespace")
group?: string; // api-group
}
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" },
"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" },
"ingresses": { kind: "Ingress", group: "networking.k8s.io" },
"jobs": { kind: "Job", group: "batch" },
"namespaces": { kind: "Namespace" },
"limitranges": { kind: "LimitRange" },
"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" },
"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", group: "core" },
"services": { kind: "Service" },
"statefulsets": { kind: "StatefulSet", group: "apps" },
"storageclasses": { kind: "StorageClass", group: "storage.k8s.io" },
};
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
export const apiResources: KubeApiResource[] = [
{ kind: "ConfigMap", apiName: "configmaps" },
{ kind: "CronJob", apiName: "cronjobs", group: "batch" },
{ kind: "CustomResourceDefinition", apiName: "customresourcedefinitions", group: "apiextensions.k8s.io" },
{ kind: "DaemonSet", apiName: "daemonsets", group: "apps" },
{ kind: "Deployment", apiName: "deployments", group: "apps" },
{ kind: "Endpoint", apiName: "endpoints" },
{ kind: "Event", apiName: "events" },
{ kind: "HorizontalPodAutoscaler", apiName: "horizontalpodautoscalers" },
{ kind: "Ingress", apiName: "ingresses", group: "networking.k8s.io" },
{ kind: "Job", apiName: "jobs", group: "batch" },
{ kind: "Namespace", apiName: "namespaces" },
{ kind: "LimitRange", apiName: "limitranges" },
{ kind: "NetworkPolicy", apiName: "networkpolicies", group: "networking.k8s.io" },
{ kind: "Node", apiName: "nodes" },
{ kind: "PersistentVolume", apiName: "persistentvolumes" },
{ kind: "PersistentVolumeClaim", apiName: "persistentvolumeclaims" },
{ kind: "Pod", apiName: "pods" },
{ kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets", group: "policy" },
{ kind: "PodSecurityPolicy", apiName: "podsecuritypolicies" },
{ kind: "ResourceQuota", apiName: "resourcequotas" },
{ kind: "ReplicaSet", apiName: "replicasets", group: "apps" },
{ kind: "Secret", apiName: "secrets" },
{ kind: "Service", apiName: "services" },
{ kind: "StatefulSet", apiName: "statefulsets", group: "apps" },
{ kind: "StorageClass", apiName: "storageclasses", group: "storage.k8s.io" },
];
export const apiResources: KubeApiResource[] = Object.entries(apiResourceRecord)
.map(([apiName, data]) => ({ apiName: apiName as KubeResource, ...data }));
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
if (!Array.isArray(resources)) {

View File

@ -30,11 +30,11 @@ export const webpackDevServerPort = 9009;
// Special runtime paths
defineGlobal("__static", {
get() {
if (isDevelopment) {
return path.resolve(contextDir, "static");
}
const root = isDevelopment
? contextDir
: (process.resourcesPath ?? contextDir);
return path.resolve(process.resourcesPath, "static");
return path.resolve(root, "static");
}
});

View File

@ -1,7 +1,7 @@
import { BaseRegistry } from "./base-registry";
export interface WelcomeMenuRegistration {
title: string;
title: string | (() => string);
icon: string;
click: () => void | Promise<void>;
}

View File

@ -1,40 +1,42 @@
import { Router } from "../router";
const staticRoot = __dirname;
class TestRouter extends Router {
protected resolveStaticRootPath() {
return staticRoot;
}
}
describe("Router", () => {
it("blocks path traversal attacks", async () => {
const router = new TestRouter();
const res = {
const response: any = {
statusCode: 200,
end: jest.fn()
};
await router.handleStaticFile("../index.ts", res as any, {} as any, 0);
await (Router as any).handleStaticFile({
params: {
path: "../index.ts",
},
response,
raw: {},
});
expect(res.statusCode).toEqual(404);
expect(response.statusCode).toEqual(404);
});
it("serves files under static root", async () => {
const router = new TestRouter();
const res = {
const response: any = {
statusCode: 200,
write: jest.fn(),
setHeader: jest.fn(),
end: jest.fn()
};
const req = {
const req: any = {
url: ""
};
await router.handleStaticFile("router.test.ts", res as any, req as any, 0);
await (Router as any).handleStaticFile({
params: {
path: "router.test.ts",
},
response,
raw: { req },
});
expect(res.statusCode).toEqual(200);
expect(response.statusCode).toEqual(200);
});
});

View File

@ -7,7 +7,7 @@ import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttribu
import { Kubectl } from "./kubectl";
import { KubeconfigManager } from "./kubeconfig-manager";
import { loadConfig, validateKubeConfig } from "../common/kube-helpers";
import { apiResources, KubeApiResource } from "../common/rbac";
import { apiResourceRecord, apiResources, KubeApiResource, KubeResource } from "../common/rbac";
import logger from "./logger";
import { VersionDetector } from "./cluster-detectors/version-detector";
import { detectorRegistry } from "./cluster-detectors/detector-registry";
@ -676,7 +676,11 @@ export class Cluster implements ClusterModel, ClusterState {
}
isAllowedResource(kind: string): boolean {
const apiResource = apiResources.find(resource => resource.kind === kind || resource.apiName === kind);
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);

View File

@ -1,17 +0,0 @@
import http from "http";
export abstract class LensApi {
protected respondJson(res: http.ServerResponse, content: {}, status = 200) {
this.respond(res, JSON.stringify(content), "application/json", status);
}
protected respondText(res: http.ServerResponse, content: string, status = 200) {
this.respond(res, content, "text/plain", status);
}
protected respond(res: http.ServerResponse, content: string, contentType: string, status = 200) {
res.setHeader("Content-Type", contentType);
res.statusCode = status;
res.end(content);
}
}

View File

@ -1,11 +1,12 @@
import { LensProtocolRouterMain } from "../router";
import { noop } from "../../../common/utils";
import { ExtensionsStore } from "../../../extensions/extensions-store";
import { ExtensionLoader } from "../../../extensions/extension-loader";
import * as uuid from "uuid";
import { LensMainExtension } from "../../../extensions/core-api";
import { broadcastMessage } from "../../../common/ipc";
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
import { noop } from "../../../common/utils";
import { LensMainExtension } from "../../../extensions/core-api";
import { ExtensionLoader } from "../../../extensions/extension-loader";
import { ExtensionsStore } from "../../../extensions/extensions-store";
import { LensProtocolRouterMain } from "../router";
jest.mock("../../../common/ipc");
@ -54,7 +55,7 @@ describe("protocol router tests", () => {
}
});
it("should not throw when has valid host", async () => {
it.only("should not throw when has valid host", async () => {
const extId = uuid.v4();
const ext = new LensMainExtension({
id: extId,
@ -85,14 +86,13 @@ describe("protocol router tests", () => {
expect(throwIfDefined(error)).not.toThrow();
}
try {
expect(await lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
} catch (error) {
expect(throwIfDefined(error)).not.toThrow();
}
expect(broadcastMessage).toHaveBeenNthCalledWith(1, ProtocolHandlerInternal, "lens://app");
expect(broadcastMessage).toHaveBeenNthCalledWith(1, ProtocolHandlerInternal, "lens://app/");
expect(broadcastMessage).toHaveBeenNthCalledWith(2, ProtocolHandlerExtension, "lens://extension/@mirantis/minikube");
});

View File

@ -5,7 +5,7 @@ import path from "path";
import { readFile } from "fs-extra";
import { Cluster } from "./cluster";
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, versionRoute } from "./routes";
import { HelmApiRoute, KubeconfigRoute, MetricsRoute, PortForwardRoute, ResourceApplierApiRoute, VersionRoute } from "./routes";
import logger from "./logger";
export interface RouterRequestOpts {
@ -38,18 +38,29 @@ export interface LensApiRequest<P = any> {
}
}
function getMimeType(filename: string) {
const mimeTypes: Record<string, string> = {
html: "text/html",
txt: "text/plain",
css: "text/css",
gif: "image/gif",
jpg: "image/jpeg",
png: "image/png",
svg: "image/svg+xml",
js: "application/javascript",
woff2: "font/woff2",
ttf: "font/ttf"
};
return mimeTypes[path.extname(filename).slice(1)] || "text/plain";
}
export class Router {
protected router: any;
protected staticRootPath: string;
protected router = new Call.Router();
protected static rootPath = path.resolve(__static);
public constructor() {
this.router = new Call.Router();
this.addRoutes();
this.staticRootPath = this.resolveStaticRootPath();
}
protected resolveStaticRootPath() {
return path.resolve(__static);
}
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
@ -90,97 +101,80 @@ export class Router {
};
}
protected getMimeType(filename: string) {
const mimeTypes: Record<string, string> = {
html: "text/html",
txt: "text/plain",
css: "text/css",
gif: "image/gif",
jpg: "image/jpeg",
png: "image/png",
svg: "image/svg+xml",
js: "application/javascript",
woff2: "font/woff2",
ttf: "font/ttf"
};
protected static async handleStaticFile({ params, response, raw: { req } }: LensApiRequest): Promise<void> {
let filePath = params.path;
return mimeTypes[path.extname(filename).slice(1)] || "text/plain";
}
for (let retryCount = 0; retryCount < 5; retryCount += 1) {
const asset = path.join(Router.rootPath, filePath);
const normalizedFilePath = path.resolve(asset);
async handleStaticFile(filePath: string, res: http.ServerResponse, req: http.IncomingMessage, retryCount = 0) {
const asset = path.join(this.staticRootPath, filePath);
const normalizedFilePath = path.resolve(asset);
if (!normalizedFilePath.startsWith(Router.rootPath)) {
response.statusCode = 404;
if (!normalizedFilePath.startsWith(this.staticRootPath)) {
res.statusCode = 404;
res.end();
return response.end();
}
return;
try {
const filename = path.basename(req.url);
// redirect requests to [appName].js, [appName].html /sockjs-node/ to webpack-dev-server (for hot-reload support)
const toWebpackDevServer = filename.includes(appName) || filename.includes("hot-update") || req.url.includes("sockjs-node");
if (isDevelopment && toWebpackDevServer) {
const redirectLocation = `http://localhost:${webpackDevServerPort}${req.url}`;
response.statusCode = 307;
response.setHeader("Location", redirectLocation);
return response.end();
}
const data = await readFile(asset);
response.setHeader("Content-Type", getMimeType(asset));
response.write(data);
response.end();
} catch (err) {
if (retryCount > 5) {
logger.error("handleStaticFile:", err.toString());
response.statusCode = 404;
return response.end();
}
filePath = `${publicPath}/${appName}.html`;
}
}
try {
const filename = path.basename(req.url);
// redirect requests to [appName].js, [appName].html /sockjs-node/ to webpack-dev-server (for hot-reload support)
const toWebpackDevServer = filename.includes(appName) || filename.includes("hot-update") || req.url.includes("sockjs-node");
if (isDevelopment && toWebpackDevServer) {
const redirectLocation = `http://localhost:${webpackDevServerPort}${req.url}`;
res.statusCode = 307;
res.setHeader("Location", redirectLocation);
res.end();
return;
}
const data = await readFile(asset);
res.setHeader("Content-Type", this.getMimeType(asset));
res.write(data);
res.end();
} catch (err) {
if (retryCount > 5) {
logger.error("handleStaticFile:", err.toString());
res.statusCode = 404;
res.end();
return;
}
this.handleStaticFile(`${publicPath}/${appName}.html`, res, req, Math.max(retryCount, 0) + 1);
}
}
protected addRoutes() {
// Static assets
this.router.add(
{ method: "get", path: "/{path*}" },
({ params, response, raw: { req } }: LensApiRequest) => {
this.handleStaticFile(params.path, response, req);
});
this.router.add({ method: "get", path: "/{path*}" }, Router.handleStaticFile);
this.router.add({ method: "get", path: "/version"}, versionRoute.getVersion.bind(versionRoute));
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute));
this.router.add({ method: "get", path: "/version" }, VersionRoute.getVersion);
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, KubeconfigRoute.routeServiceAccountRoute);
// Metrics API
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute));
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics);
// Port-forward API
this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, portForwardRoute.routePortForward.bind(portForwardRoute));
this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, PortForwardRoute.routePortForward);
// Helm API
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, helmRoute.listCharts.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}` }, helmRoute.getChart.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}/values` }, helmRoute.getChartValues.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, HelmApiRoute.listCharts);
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}` }, HelmApiRoute.getChart);
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}/values` }, HelmApiRoute.getChartValues);
this.router.add({ method: "post", path: `${apiPrefix}/v2/releases` }, helmRoute.installChart.bind(helmRoute));
this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.updateRelease.bind(helmRoute));
this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback` }, helmRoute.rollbackRelease.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace?}` }, helmRoute.listReleases.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.getRelease.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/values` }, helmRoute.getReleaseValues.bind(helmRoute));
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/history` }, helmRoute.getReleaseHistory.bind(helmRoute));
this.router.add({ method: "delete", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.deleteRelease.bind(helmRoute));
this.router.add({ method: "post", path: `${apiPrefix}/v2/releases` }, HelmApiRoute.installChart);
this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, HelmApiRoute.updateRelease);
this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback` }, HelmApiRoute.rollbackRelease);
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace?}` }, HelmApiRoute.listReleases);
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, HelmApiRoute.getRelease);
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/values` }, HelmApiRoute.getReleaseValues);
this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/history` }, HelmApiRoute.getReleaseHistory);
this.router.add({ method: "delete", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, HelmApiRoute.deleteRelease);
// Resource Applier API
this.router.add({ method: "post", path: `${apiPrefix}/stack` }, resourceApplierRoute.applyResource.bind(resourceApplierRoute));
this.router.add({ method: "post", path: `${apiPrefix}/stack` }, ResourceApplierApiRoute.applyResource);
}
}

View File

@ -1,143 +1,141 @@
import { LensApiRequest } from "../router";
import { helmService } from "../helm/helm-service";
import { LensApi } from "../lens-api";
import { respondJson, respondText } from "../utils/http-responses";
import logger from "../logger";
class HelmApiRoute extends LensApi {
public async listCharts(request: LensApiRequest) {
export class HelmApiRoute {
static async listCharts(request: LensApiRequest) {
const { response } = request;
const charts = await helmService.listCharts();
this.respondJson(response, charts);
respondJson(response, charts);
}
public async getChart(request: LensApiRequest) {
static async getChart(request: LensApiRequest) {
const { params, query, response } = request;
try {
const chart = await helmService.getChart(params.repo, params.chart, query.get("version"));
this.respondJson(response, chart);
respondJson(response, chart);
} catch (error) {
this.respondText(response, error, 422);
respondText(response, error, 422);
}
}
public async getChartValues(request: LensApiRequest) {
static async getChartValues(request: LensApiRequest) {
const { params, query, response } = request;
try {
const values = await helmService.getChartValues(params.repo, params.chart, query.get("version"));
this.respondJson(response, values);
respondJson(response, values);
} catch (error) {
this.respondText(response, error, 422);
respondText(response, error, 422);
}
}
public async installChart(request: LensApiRequest) {
static async installChart(request: LensApiRequest) {
const { payload, cluster, response } = request;
try {
const result = await helmService.installChart(cluster, payload);
this.respondJson(response, result, 201);
respondJson(response, result, 201);
} catch (error) {
logger.debug(error);
this.respondText(response, error, 422);
respondText(response, error, 422);
}
}
public async updateRelease(request: LensApiRequest) {
static async updateRelease(request: LensApiRequest) {
const { cluster, params, payload, response } = request;
try {
const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload );
this.respondJson(response, result);
respondJson(response, result);
} catch (error) {
logger.debug(error);
this.respondText(response, error, 422);
respondText(response, error, 422);
}
}
public async rollbackRelease(request: LensApiRequest) {
static async rollbackRelease(request: LensApiRequest) {
const { cluster, params, payload, response } = request;
try {
const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
this.respondJson(response, result);
respondJson(response, result);
} catch (error) {
logger.debug(error);
this.respondText(response, error, 422);
respondText(response, error, 422);
}
}
public async listReleases(request: LensApiRequest) {
static async listReleases(request: LensApiRequest) {
const { cluster, params, response } = request;
try {
const result = await helmService.listReleases(cluster, params.namespace);
this.respondJson(response, result);
respondJson(response, result);
} catch(error) {
logger.debug(error);
this.respondText(response, error, 422);
respondText(response, error, 422);
}
}
public async getRelease(request: LensApiRequest) {
static async getRelease(request: LensApiRequest) {
const { cluster, params, response } = request;
try {
const result = await helmService.getRelease(cluster, params.release, params.namespace);
this.respondJson(response, result);
respondJson(response, result);
} catch (error) {
logger.debug(error);
this.respondText(response, error, 422);
respondText(response, error, 422);
}
}
public async getReleaseValues(request: LensApiRequest) {
static async getReleaseValues(request: LensApiRequest) {
const { cluster, params, response, query } = request;
try {
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace, query.has("all"));
this.respondText(response, result);
respondText(response, result);
} catch (error) {
logger.debug(error);
this.respondText(response, error, 422);
respondText(response, error, 422);
}
}
public async getReleaseHistory(request: LensApiRequest) {
static async getReleaseHistory(request: LensApiRequest) {
const { cluster, params, response } = request;
try {
const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace);
this.respondJson(response, result);
respondJson(response, result);
} catch (error) {
logger.debug(error);
this.respondText(response, error, 422);
respondText(response, error, 422);
}
}
public async deleteRelease(request: LensApiRequest) {
static async deleteRelease(request: LensApiRequest) {
const { cluster, params, response } = request;
try {
const result = await helmService.deleteRelease(cluster, params.release, params.namespace);
this.respondJson(response, result);
respondJson(response, result);
} catch (error) {
logger.debug(error);
this.respondText(response, error, 422);
respondText(response, error, 422);
}
}
}
export const helmRoute = new HelmApiRoute();

View File

@ -1,5 +1,5 @@
import { LensApiRequest } from "../router";
import { LensApi } from "../lens-api";
import { respondJson } from "../utils/http-responses";
import { Cluster } from "../cluster";
import { CoreV1Api, V1Secret } from "@kubernetes/client-node";
@ -40,9 +40,8 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
};
}
class KubeconfigRoute extends LensApi {
public async routeServiceAccountRoute(request: LensApiRequest) {
export class KubeconfigRoute {
static async routeServiceAccountRoute(request: LensApiRequest) {
const { params, response, cluster} = request;
const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
const secretList = await client.listNamespacedSecret(params.namespace);
@ -53,8 +52,6 @@ class KubeconfigRoute extends LensApi {
});
const data = generateKubeConfig(params.account, secret, cluster);
this.respondJson(response, data);
respondJson(response, data);
}
}
export const kubeconfigRoute = new KubeconfigRoute();

View File

@ -1,6 +1,6 @@
import _ from "lodash";
import { LensApiRequest } from "../router";
import { LensApi } from "../lens-api";
import { respondJson } from "../utils/http-responses";
import { Cluster, ClusterMetadataKey } from "../cluster";
import { ClusterPrometheusMetadata } from "../../common/cluster-store";
import logger from "../logger";
@ -41,8 +41,8 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
return Promise.all(queries.map(loadMetric));
}
class MetricsRoute extends LensApi {
async routeMetrics({ response, cluster, payload, query }: LensApiRequest) {
export class MetricsRoute {
static async routeMetrics({ response, cluster, payload, query }: LensApiRequest) {
const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
const prometheusMetadata: ClusterPrometheusMetadata = {};
@ -57,20 +57,19 @@ class MetricsRoute extends LensApi {
if (!prometheusPath) {
prometheusMetadata.success = false;
this.respondJson(response, {});
return;
return respondJson(response, {});
}
// return data in same structure as query
if (typeof payload === "string") {
const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams);
this.respondJson(response, data);
respondJson(response, data);
} else if (Array.isArray(payload)) {
const data = await loadMetrics(payload, cluster, prometheusPath, queryParams);
this.respondJson(response, data);
respondJson(response, data);
} else {
const queries = Object.entries(payload).map(([queryName, queryOpts]) => (
(prometheusProvider.getQueries(queryOpts) as Record<string, string>)[queryName]
@ -78,16 +77,14 @@ class MetricsRoute extends LensApi {
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]));
this.respondJson(response, data);
respondJson(response, data);
}
prometheusMetadata.success = true;
} catch {
prometheusMetadata.success = false;
this.respondJson(response, {});
respondJson(response, {});
} finally {
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
}
}
}
export const metricsRoute = new MetricsRoute();

View File

@ -1,11 +1,11 @@
import { LensApiRequest } from "../router";
import { LensApi } from "../lens-api";
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
import { Kubectl } from "../kubectl";
import { shell } from "electron";
import * as tcpPortUsed from "tcp-port-used";
import logger from "../logger";
import { getPortFrom } from "../utils/get-port";
import { respondJson } from "../utils/http-responses";
interface PortForwardArgs {
clusterId: string;
@ -95,9 +95,8 @@ class PortForward {
}
}
class PortForwardRoute extends LensApi {
public async routePortForward(request: LensApiRequest) {
export class PortForwardRoute {
static async routePortForward(request: LensApiRequest) {
const { params, response, cluster} = request;
const { namespace, port, resourceType, resourceName } = params;
let portForward = PortForward.getPortforward({
@ -117,18 +116,14 @@ class PortForwardRoute extends LensApi {
const started = await portForward.start();
if (!started) {
this.respondJson(response, {
return respondJson(response, {
message: "Failed to open port-forward"
}, 400);
return;
}
}
portForward.open();
this.respondJson(response, {});
respondJson(response, {});
}
}
export const portForwardRoute = new PortForwardRoute();

View File

@ -1,19 +1,17 @@
import { LensApiRequest } from "../router";
import { LensApi } from "../lens-api";
import { respondJson, respondText } from "../utils/http-responses";
import { ResourceApplier } from "../resource-applier";
class ResourceApplierApiRoute extends LensApi {
public async applyResource(request: LensApiRequest) {
export class ResourceApplierApiRoute {
static async applyResource(request: LensApiRequest) {
const { response, cluster, payload } = request;
try {
const resource = await new ResourceApplier(cluster).apply(payload);
this.respondJson(response, [resource], 200);
respondJson(response, [resource], 200);
} catch (error) {
this.respondText(response, error, 422);
respondText(response, error, 422);
}
}
}
export const resourceApplierRoute = new ResourceApplierApiRoute();

View File

@ -1,13 +1,11 @@
import { LensApiRequest } from "../router";
import { LensApi } from "../lens-api";
import { respondJson } from "../utils/http-responses";
import { getAppVersion } from "../../common/utils";
class VersionRoute extends LensApi {
public async getVersion(request: LensApiRequest) {
export class VersionRoute {
static async getVersion(request: LensApiRequest) {
const { response } = request;
this.respondJson(response, { version: getAppVersion()}, 200);
respondJson(response, { version: getAppVersion()}, 200);
}
}
export const versionRoute = new VersionRoute();

View File

@ -0,0 +1,15 @@
import http from "http";
export function respondJson(res: http.ServerResponse, content: any, status = 200) {
respond(res, JSON.stringify(content), "application/json", status);
}
export function respondText(res: http.ServerResponse, content: string, status = 200) {
respond(res, content, "text/plain", status);
}
export function respond(res: http.ServerResponse, content: string, contentType: string, status = 200) {
res.setHeader("Content-Type", contentType);
res.statusCode = status;
res.end(content);
}

View File

@ -107,19 +107,6 @@ describe("KubeObject", () => {
});
}
{
type TestCase = [string, any];
const tests: TestCase[] = [
["metadata.uid", { kind: "", apiVersion: "", metadata: { name: "", resourceVersion: "", selfLink: ""} }],
["metadata.name", { kind: "", apiVersion: "", metadata: { uid: "", resourceVersion: "", selfLink: "" } }],
["metadata.resourceVersion", { kind: "", apiVersion: "", metadata: { uid: "", name: "", selfLink: "" } }],
];
it.each(tests)("should reject with missing non-top level field: %s", (missingField, input) => {
expect(KubeObject.isPartialJsonApiData(input)).toBe(false);
});
}
{
type TestCase = [string, any];
const tests: TestCase[] = [
@ -192,7 +179,6 @@ describe("KubeObject", () => {
["kind", { apiVersion: "", items: [], metadata: { resourceVersion: "", selfLink: "" } }],
["apiVersion", { kind: "", items: [], metadata: { resourceVersion: "", selfLink: "" } }],
["metadata", { kind: "", items: [], apiVersion: "" }],
["metadata.resourceVersion", { kind: "", items: [], apiVersion: "", metadata: { selfLink: "" } }],
];
it.each(tests)("should reject with missing: %s", (missingField, input) => {

View File

@ -2,7 +2,7 @@ import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
export class PodMetrics extends KubeObject {
static kind = "Pod";
static kind = "PodMetrics";
static namespaced = true;
static apiBase = "/apis/metrics.k8s.io/v1beta1/pods";

View File

@ -90,7 +90,7 @@ export class KubeObject implements ItemObject {
static isKubeJsonApiListMetadata(object: unknown): object is KubeJsonApiListMetadata {
return (
isObject(object)
&& hasTypedProperty(object, "resourceVersion", isString)
&& hasOptionalProperty(object, "resourceVersion", isString)
&& hasOptionalProperty(object, "selfLink", isString)
);
}
@ -111,12 +111,28 @@ export class KubeObject implements ItemObject {
);
}
static isPartialJsonApiMetadata(object: unknown): object is Partial<KubeJsonApiMetadata> {
return (
isObject(object)
&& hasOptionalProperty(object, "uid", isString)
&& hasOptionalProperty(object, "name", isString)
&& hasOptionalProperty(object, "resourceVersion", isString)
&& hasOptionalProperty(object, "selfLink", isString)
&& hasOptionalProperty(object, "namespace", isString)
&& hasOptionalProperty(object, "creationTimestamp", isString)
&& hasOptionalProperty(object, "continue", isString)
&& hasOptionalProperty(object, "finalizers", bindPredicate(isTypedArray, isString))
&& hasOptionalProperty(object, "labels", bindPredicate(isRecord, isString, isString))
&& hasOptionalProperty(object, "annotations", bindPredicate(isRecord, isString, isString))
);
}
static isPartialJsonApiData(object: unknown): object is Partial<KubeJsonApiData> {
return (
isObject(object)
&& hasOptionalProperty(object, "kind", isString)
&& hasOptionalProperty(object, "apiVersion", isString)
&& hasOptionalProperty(object, "metadata", KubeObject.isKubeJsonApiMetadata)
&& hasOptionalProperty(object, "metadata", KubeObject.isPartialJsonApiMetadata)
);
}

View File

@ -167,15 +167,13 @@ async function validatePackage(filePath: string): Promise<LensExtensionManifest>
return manifest;
}
async function createTempFilesAndValidate({ fileName, dataP }: InstallRequest, disposer: ExtendableDisposer): Promise<InstallRequestValidated | null> {
async function createTempFilesAndValidate({ fileName, dataP }: InstallRequest): Promise<InstallRequestValidated | null> {
// copy files to temp
await fse.ensureDir(getExtensionPackageTemp());
// validate packages
const tempFile = getExtensionPackageTemp(fileName);
disposer.push(() => fse.unlink(tempFile));
try {
const data = await dataP;
@ -318,7 +316,7 @@ export async function attemptInstallByInfo({ name, version, requireConfirmation
async function attemptInstall(request: InstallRequest, d?: ExtendableDisposer): Promise<void> {
const dispose = disposer(ExtensionInstallationStateStore.startPreInstall(), d);
const validatedRequest = await createTempFilesAndValidate(request, dispose);
const validatedRequest = await createTempFilesAndValidate(request);
if (!validatedRequest) {
return dispose();

View File

@ -14,26 +14,34 @@ export class UserManagement extends React.Component {
static get tabRoutes() {
const tabRoutes: TabLayoutRoute[] = [];
tabRoutes.push(
{
if (isAllowedResource("serviceaccounts")) {
tabRoutes.push({
title: "Service Accounts",
component: ServiceAccounts,
url: serviceAccountsURL(),
routePath: serviceAccountsRoute.path.toString(),
},
{
});
}
if (isAllowedResource("rolebindings") || isAllowedResource("clusterrolebindings")) {
// TODO: seperate out these two pages
tabRoutes.push({
title: "Role Bindings",
component: RoleBindings,
url: roleBindingsURL(),
routePath: roleBindingsRoute.path.toString(),
},
{
});
}
if (isAllowedResource("roles") || isAllowedResource("clusterroles")) {
// TODO: seperate out these two pages
tabRoutes.push({
title: "Roles",
component: Roles,
url: rolesURL(),
routePath: rolesRoute.path.toString(),
},
);
});
}
if (isAllowedResource("podsecuritypolicies")) {
tabRoutes.push({

View File

@ -47,7 +47,7 @@ export class Welcome extends React.Component {
<ul className="box">
{ welcomeMenuRegistry.getItems().map((item, index) => (
<li key={index} className="flex grid-12" onClick={() => item.click()}>
<Icon material={item.icon} className="box col-1" /> <a className="box col-10">{item.title}</a> <Icon material="navigate_next" className="box col-1" />
<Icon material={item.icon} className="box col-1" /> <a className="box col-10">{typeof item.title === "string" ? item.title : item.title()}</a> <Icon material="navigate_next" className="box col-1" />
</li>
))}
</ul>

View File

@ -255,6 +255,7 @@ export class Sidebar extends React.Component<Props> {
id="users"
text="Access Control"
isActive={isActiveRoute(usersManagementRoute)}
isHidden={UserManagement.tabRoutes.length === 0}
url={usersManagementURL()}
icon={<Icon material="security"/>}
>

View File

@ -106,7 +106,7 @@ html {
&--is-disabled {
cursor: not-allowed;
background: none !important;
color: $contentColor;
color: inherit;
opacity: .75;
}
@ -166,6 +166,10 @@ html {
&--is-focused {
color: white;
}
&--is-disabled:hover {
color: inherit;
}
}
}
}

View File

@ -26,4 +26,9 @@ export const ResourceNames: Record<KubeResource, string> = {
"podsecuritypolicies": "Pod Security Policies",
"poddisruptionbudgets": "Pod Disruption Budgets",
"limitranges": "Limit Ranges",
"roles": "Roles",
"rolebindings": "Role Bindings",
"clusterrolebindings": "Cluster Role Bindings",
"clusterroles": "Cluster Roles",
"serviceaccounts": "Service Accounts"
};

View File

@ -6587,10 +6587,10 @@ handle-thing@^2.0.0:
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==
handlebars@^4.7.2, handlebars@^4.7.6:
version "4.7.6"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e"
integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==
handlebars@^4.7.2, handlebars@^4.7.6, handlebars@^4.7.7:
version "4.7.7"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
dependencies:
minimist "^1.2.5"
neo-async "^2.6.0"
@ -6767,9 +6767,9 @@ homedir-polyfill@^1.0.1:
parse-passwd "^1.0.0"
hosted-git-info@^2.1.4:
version "2.8.8"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
hosted-git-info@^2.7.1, hosted-git-info@^2.8.9:
version "2.8.9"
@ -14117,10 +14117,10 @@ url-parse-lax@^3.0.0:
dependencies:
prepend-http "^2.0.0"
url-parse@^1.4.3, url-parse@^1.4.7:
version "1.4.7"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
url-parse@^1.4.3, url-parse@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b"
integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==
dependencies:
querystringify "^2.1.1"
requires-port "^1.0.0"