diff --git a/.github/PULL_REQUEST_TEMPLATE/default.md b/.github/PULL_REQUEST_TEMPLATE/default.md new file mode 100644 index 0000000000..a54a6ef38c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/default.md @@ -0,0 +1,5 @@ +Fixes # + +**Description of changes:** + +- diff --git a/.github/PULL_REQUEST_TEMPLATE/release.md b/.github/PULL_REQUEST_TEMPLATE/release.md new file mode 100644 index 0000000000..11e5822025 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/release.md @@ -0,0 +1,13 @@ +## Changes since v + +## 🚀 Features + +* + +## 🐛 Bug Fixes + +* + +## 🧰 Maintenance + +* diff --git a/Makefile b/Makefile index d2b7cc9dd8..d7223e2bc7 100644 --- a/Makefile +++ b/Makefile @@ -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") diff --git a/RELEASE_GUIDE.md b/RELEASE_GUIDE.md index da4bf01d94..5fdc1efa32 100644 --- a/RELEASE_GUIDE.md +++ b/RELEASE_GUIDE.md @@ -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. \ No newline at end of file +1. From a clean and up to date `master` run `make release-version ` where `` is one of the following: + - `major` + - `minor` + - `patch` + - `premajor` + - `preminor` + - `prepatch` + - `prerelease [--preid=]` + - where `` 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. diff --git a/docs/extensions/capabilities/color-reference.md b/docs/extensions/capabilities/color-reference.md deleted file mode 100644 index 6a38ba861c..0000000000 --- a/docs/extensions/capabilities/color-reference.md +++ /dev/null @@ -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. diff --git a/docs/extensions/capabilities/styling.md b/docs/extensions/capabilities/styling.md index cf80fd5530..62dbddde1a 100644 --- a/docs/extensions/capabilities/styling.md +++ b/docs/extensions/capabilities/styling.md @@ -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 `` element gets a "theme-light" class, or: ``. diff --git a/extensions/metrics-cluster-feature/package-lock.json b/extensions/metrics-cluster-feature/package-lock.json index 0f1d2365fc..c0c18bb359 100644 --- a/extensions/metrics-cluster-feature/package-lock.json +++ b/extensions/metrics-cluster-feature/package-lock.json @@ -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": { diff --git a/extensions/node-menu/package-lock.json b/extensions/node-menu/package-lock.json index 8dbb8b0beb..f14f34bf96 100644 --- a/extensions/node-menu/package-lock.json +++ b/extensions/node-menu/package-lock.json @@ -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": { diff --git a/extensions/pod-menu/package-lock.json b/extensions/pod-menu/package-lock.json index 0e1b1c9360..b5fa04c49f 100644 --- a/extensions/pod-menu/package-lock.json +++ b/extensions/pod-menu/package-lock.json @@ -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": { diff --git a/mkdocs.yml b/mkdocs.yml index c926bd8a19..0496f3001e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/package.json b/package.json index 4b4408201c..e497e55df4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/tag-release.sh b/scripts/tag-release.sh new file mode 100755 index 0000000000..3a32bd3189 --- /dev/null +++ b/scripts/tag-release.sh @@ -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 diff --git a/src/common/rbac.ts b/src/common/rbac.ts index 7d02e3be51..67b6647111 100644 --- a/src/common/rbac.ts +++ b/src/common/rbac.ts @@ -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 = { + "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)) { diff --git a/src/common/vars.ts b/src/common/vars.ts index 03d3f1c63d..ef39088258 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -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"); } }); diff --git a/src/extensions/registries/welcome-menu-registry.ts b/src/extensions/registries/welcome-menu-registry.ts index 14541538a8..8744d8bbe6 100644 --- a/src/extensions/registries/welcome-menu-registry.ts +++ b/src/extensions/registries/welcome-menu-registry.ts @@ -1,7 +1,7 @@ import { BaseRegistry } from "./base-registry"; export interface WelcomeMenuRegistration { - title: string; + title: string | (() => string); icon: string; click: () => void | Promise; } diff --git a/src/main/__test__/router.test.ts b/src/main/__test__/router.test.ts index 8c2fa9c822..393f2a035b 100644 --- a/src/main/__test__/router.test.ts +++ b/src/main/__test__/router.test.ts @@ -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); }); }); diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 2de8bff946..b79c7d3c33 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -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); diff --git a/src/main/lens-api.ts b/src/main/lens-api.ts deleted file mode 100644 index fafeffce91..0000000000 --- a/src/main/lens-api.ts +++ /dev/null @@ -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); - } -} diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts index 408fde02aa..750ccef7cc 100644 --- a/src/main/protocol-handler/__test__/router.test.ts +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -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"); }); diff --git a/src/main/router.ts b/src/main/router.ts index 6fa14e1444..5a53eac9df 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -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

{ } } +function getMimeType(filename: string) { + const mimeTypes: Record = { + 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 { @@ -90,97 +101,80 @@ export class Router { }; } - protected getMimeType(filename: string) { - const mimeTypes: Record = { - 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 { + 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); } } diff --git a/src/main/routes/helm-route.ts b/src/main/routes/helm-route.ts index e530e2edf0..89e229b626 100644 --- a/src/main/routes/helm-route.ts +++ b/src/main/routes/helm-route.ts @@ -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(); diff --git a/src/main/routes/kubeconfig-route.ts b/src/main/routes/kubeconfig-route.ts index bad5ecd57a..d2e3941a29 100644 --- a/src/main/routes/kubeconfig-route.ts +++ b/src/main/routes/kubeconfig-route.ts @@ -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(); diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts index b6fbdadb61..0dec1fc1ff 100644 --- a/src/main/routes/metrics-route.ts +++ b/src/main/routes/metrics-route.ts @@ -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)[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(); diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts index 6edd81e537..ca8be52dcd 100644 --- a/src/main/routes/port-forward-route.ts +++ b/src/main/routes/port-forward-route.ts @@ -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(); diff --git a/src/main/routes/resource-applier-route.ts b/src/main/routes/resource-applier-route.ts index 1e532dde46..6183ece931 100644 --- a/src/main/routes/resource-applier-route.ts +++ b/src/main/routes/resource-applier-route.ts @@ -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(); diff --git a/src/main/routes/version-route.ts b/src/main/routes/version-route.ts index 81ada9eca7..8eaa96ca17 100644 --- a/src/main/routes/version-route.ts +++ b/src/main/routes/version-route.ts @@ -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(); diff --git a/src/main/utils/http-responses.ts b/src/main/utils/http-responses.ts new file mode 100644 index 0000000000..d41b124fb6 --- /dev/null +++ b/src/main/utils/http-responses.ts @@ -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); +} diff --git a/src/renderer/api/__tests__/kube-object.test.ts b/src/renderer/api/__tests__/kube-object.test.ts index b3f84ea1ae..11ca8b8717 100644 --- a/src/renderer/api/__tests__/kube-object.test.ts +++ b/src/renderer/api/__tests__/kube-object.test.ts @@ -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) => { diff --git a/src/renderer/api/endpoints/pod-metrics.api.ts b/src/renderer/api/endpoints/pod-metrics.api.ts index acf6e7b20f..abb391b0d1 100644 --- a/src/renderer/api/endpoints/pod-metrics.api.ts +++ b/src/renderer/api/endpoints/pod-metrics.api.ts @@ -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"; diff --git a/src/renderer/api/kube-object.ts b/src/renderer/api/kube-object.ts index cc14ff9a7b..c7395319ac 100644 --- a/src/renderer/api/kube-object.ts +++ b/src/renderer/api/kube-object.ts @@ -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 { + 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 { return ( isObject(object) && hasOptionalProperty(object, "kind", isString) && hasOptionalProperty(object, "apiVersion", isString) - && hasOptionalProperty(object, "metadata", KubeObject.isKubeJsonApiMetadata) + && hasOptionalProperty(object, "metadata", KubeObject.isPartialJsonApiMetadata) ); } diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 74c4332960..f43e9a6f6d 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -167,15 +167,13 @@ async function validatePackage(filePath: string): Promise return manifest; } -async function createTempFilesAndValidate({ fileName, dataP }: InstallRequest, disposer: ExtendableDisposer): Promise { +async function createTempFilesAndValidate({ fileName, dataP }: InstallRequest): Promise { // 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 { const dispose = disposer(ExtensionInstallationStateStore.startPreInstall(), d); - const validatedRequest = await createTempFilesAndValidate(request, dispose); + const validatedRequest = await createTempFilesAndValidate(request); if (!validatedRequest) { return dispose(); diff --git a/src/renderer/components/+user-management/user-management.tsx b/src/renderer/components/+user-management/user-management.tsx index 83fede82ae..ea7d4b4e9e 100644 --- a/src/renderer/components/+user-management/user-management.tsx +++ b/src/renderer/components/+user-management/user-management.tsx @@ -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({ diff --git a/src/renderer/components/+welcome/welcome.tsx b/src/renderer/components/+welcome/welcome.tsx index aff6ebf002..5b29f8af19 100644 --- a/src/renderer/components/+welcome/welcome.tsx +++ b/src/renderer/components/+welcome/welcome.tsx @@ -47,7 +47,7 @@ export class Welcome extends React.Component {

diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index a5ff91ced2..3ef4136768 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -255,6 +255,7 @@ export class Sidebar extends React.Component { id="users" text="Access Control" isActive={isActiveRoute(usersManagementRoute)} + isHidden={UserManagement.tabRoutes.length === 0} url={usersManagementURL()} icon={} > diff --git a/src/renderer/components/select/select.scss b/src/renderer/components/select/select.scss index 3d42b2afa7..7b75fadbe6 100644 --- a/src/renderer/components/select/select.scss +++ b/src/renderer/components/select/select.scss @@ -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; + } } } } diff --git a/src/renderer/utils/rbac.ts b/src/renderer/utils/rbac.ts index 36737ccf3a..c716e4b27c 100644 --- a/src/renderer/utils/rbac.ts +++ b/src/renderer/utils/rbac.ts @@ -26,4 +26,9 @@ export const ResourceNames: Record = { "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" }; diff --git a/yarn.lock b/yarn.lock index e15157322e..b151dfffd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"