From 6e73c8ffc2d9e04692b7d27ce035c3a4ff39ece7 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 20 Nov 2020 09:29:38 +0200 Subject: [PATCH 01/13] Fix extension cluster menu highlight/routing issues (#1459) Signed-off-by: Jari Kolehmainen --- src/renderer/components/app.tsx | 2 +- src/renderer/components/layout/sidebar.tsx | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 7aea1125ad..fb7d4a7df0 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -100,7 +100,7 @@ export class App extends React.Component { const tabRoutes = this.getTabLayoutRoutes(menu); if (tabRoutes.length > 0) { const pageComponent = () => ; - return ; + return tab.routePath)} />; } else { const page = clusterPageRegistry.getByPageMenuTarget(menu.target); if (page) { diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index 0c56923c28..32e55bebc4 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -98,23 +98,30 @@ export class Sidebar extends React.Component { } renderRegisteredMenus() { - return clusterPageMenuRegistry.getRootItems().map((menuItem) => { + return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => { const registeredPage = clusterPageRegistry.getByPageMenuTarget(menuItem.target); + const tabRoutes = this.getTabLayoutRoutes(menuItem); let pageUrl: string; + let routePath: string; let isActive = false; if (registeredPage) { const { extensionId, id: pageId } = registeredPage; pageUrl = getExtensionPageUrl({ extensionId, pageId, params: menuItem.target.params }); - isActive = pageUrl === navigation.location.pathname; - } - const tabRoutes = this.getTabLayoutRoutes(menuItem); - if (!registeredPage && tabRoutes.length == 0) { + routePath = registeredPage.routePath; + isActive = isActiveRoute(registeredPage.routePath); + } else if (tabRoutes.length > 0) { + pageUrl = tabRoutes[0].url; + routePath = tabRoutes[0].routePath; + isActive = isActiveRoute(tabRoutes.map((tab) => tab.routePath)); + } else { return; } return ( } + key={"registered-item-" + index} + url={pageUrl} + text={menuItem.title} + icon={} isActive={isActive} subMenus={tabRoutes} /> From 57a879c2e8b5a78f2b498120570d745603960a96 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Fri, 20 Nov 2020 12:55:16 +0300 Subject: [PATCH 02/13] Making text selection bg color transparent a bit (#1463) Signed-off-by: Alex Andreev --- src/renderer/components/dock/terminal-window.scss | 7 ------- src/renderer/themes/lens-light.json | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/renderer/components/dock/terminal-window.scss b/src/renderer/components/dock/terminal-window.scss index b3a7f99bda..5bc45c1e1f 100644 --- a/src/renderer/components/dock/terminal-window.scss +++ b/src/renderer/components/dock/terminal-window.scss @@ -18,11 +18,4 @@ .xterm-viewport { @include custom-scrollbar; } - - // fix: safari won't handle paste event for textarea with zero size block - .xterm-helper-textarea { - width: 10px !important; - height: 10px !important; - pointer-events: none; - } } \ No newline at end of file diff --git a/src/renderer/themes/lens-light.json b/src/renderer/themes/lens-light.json index 10020f3a2b..302a52a699 100644 --- a/src/renderer/themes/lens-light.json +++ b/src/renderer/themes/lens-light.json @@ -67,7 +67,7 @@ "terminalForeground": "#2d2d2d", "terminalCursor": "#2d2d2d", "terminalCursorAccent": "#ffffff", - "terminalSelection": "#bfbfbf", + "terminalSelection": "#bfbfbf66", "terminalBlack": "#2d2d2d", "terminalRed": "#cd3734 ", "terminalGreen": "#18cf12", From 22ff706f4cabf19b4f182a2cf678d8bc45caa2d2 Mon Sep 17 00:00:00 2001 From: Panu Horsmalahti Date: Fri, 20 Nov 2020 11:59:11 +0200 Subject: [PATCH 03/13] Support networking.k8s.io/v1 for Ingress (#1439) * Support networking.k8s.io/v1 for Ingress Signed-off-by: Panu Horsmalahti * Add helper method to Ingress Signed-off-by: Panu Horsmalahti * Fix lint errors. Signed-off-by: Panu Horsmalahti --- src/renderer/api/__tests__/kube-api.test.ts | 82 ++++++++++++++++ src/renderer/api/endpoints/ingress.api.ts | 87 +++++++++++++---- src/renderer/api/kube-api.ts | 94 ++++++++++++++++++- .../+network-ingresses/ingress-details.tsx | 16 +++- 4 files changed, 254 insertions(+), 25 deletions(-) create mode 100644 src/renderer/api/__tests__/kube-api.test.ts diff --git a/src/renderer/api/__tests__/kube-api.test.ts b/src/renderer/api/__tests__/kube-api.test.ts new file mode 100644 index 0000000000..41078e77a3 --- /dev/null +++ b/src/renderer/api/__tests__/kube-api.test.ts @@ -0,0 +1,82 @@ +import { KubeApi } from "../kube-api"; + +describe("KubeApi", () => { + it("uses url from apiBase if apiBase contains the resource", async () => { + (fetch as any).mockResponse(async (request: any) => { + if (request.url === "/api-kube/apis/networking.k8s.io/v1") { + return { + body: JSON.stringify({ + resources: [{ + name: "ingresses" + }] as any [] + }) + }; + } else if (request.url === "/api-kube/apis/extensions/v1beta1") { + // Even if the old API contains ingresses, KubeApi should prefer the apiBase url + return { + body: JSON.stringify({ + resources: [{ + name: "ingresses" + }] as any [] + }) + }; + } else { + return { + body: JSON.stringify({ + resources: [] as any [] + }) + }; + } + }); + + const apiBase = "/apis/networking.k8s.io/v1/ingresses"; + const fallbackApiBase = "/apis/extensions/v1beta1/ingresses"; + const kubeApi = new KubeApi({ + apiBase, + fallbackApiBases: [fallbackApiBase], + checkPreferredVersion: true, + }); + + await kubeApi.get(); + expect(kubeApi.apiPrefix).toEqual("/apis"); + expect(kubeApi.apiGroup).toEqual("networking.k8s.io"); + }); + + it("uses url from fallbackApiBases if apiBase lacks the resource", async () => { + (fetch as any).mockResponse(async (request: any) => { + if (request.url === "/api-kube/apis/networking.k8s.io/v1") { + return { + body: JSON.stringify({ + resources: [] as any [] + }) + }; + } else if (request.url === "/api-kube/apis/extensions/v1beta1") { + return { + body: JSON.stringify({ + resources: [{ + name: "ingresses" + }] as any [] + }) + }; + } else { + return { + body: JSON.stringify({ + resources: [] as any [] + }) + }; + } + }); + + const apiBase = "apis/networking.k8s.io/v1/ingresses"; + const fallbackApiBase = "/apis/extensions/v1beta1/ingresses"; + const kubeApi = new KubeApi({ + apiBase, + fallbackApiBases: [fallbackApiBase], + checkPreferredVersion: true, + }); + + await kubeApi.get(); + expect(kubeApi.apiPrefix).toEqual("/apis"); + expect(kubeApi.apiGroup).toEqual("extensions"); + }); +}); \ No newline at end of file diff --git a/src/renderer/api/endpoints/ingress.api.ts b/src/renderer/api/endpoints/ingress.api.ts index 0594e3446e..a476b29efa 100644 --- a/src/renderer/api/endpoints/ingress.api.ts +++ b/src/renderer/api/endpoints/ingress.api.ts @@ -29,11 +29,42 @@ export interface ILoadBalancerIngress { hostname?: string; ip?: string; } + +// extensions/v1beta1 +interface IExtensionsBackend { + serviceName: string; + servicePort: number; +} + +// networking.k8s.io/v1 +interface INetworkingBackend { + service: IIngressService; +} + +export type IIngressBackend = IExtensionsBackend | INetworkingBackend; + +export interface IIngressService { + name: string; + port: { + name?: string; + number?: number; + } +} + +export const getBackendServiceNamePort = (backend: IIngressBackend) => { + // .service is available with networking.k8s.io/v1, otherwise using extensions/v1beta1 interface + const serviceName = "service" in backend ? backend.service.name : backend.serviceName; + // Port is specified either with a number or name + const servicePort = "service" in backend ? backend.service.port.number ?? backend.service.port.name : backend.servicePort; + + return { serviceName, servicePort }; +}; + @autobind() export class Ingress extends KubeObject { static kind = "Ingress"; static namespaced = true; - static apiBase = "/apis/extensions/v1beta1/ingresses"; + static apiBase = "/apis/networking.k8s.io/v1/ingresses"; spec: { tls: { @@ -44,17 +75,20 @@ export class Ingress extends KubeObject { http: { paths: { path?: string; - backend: { - serviceName: string; - servicePort: number; - }; + backend: IIngressBackend; }[]; }; }[]; - backend?: { - serviceName: string; - servicePort: number; - }; + // extensions/v1beta1 + backend?: IExtensionsBackend; + // networking.k8s.io/v1 + defaultBackend?: INetworkingBackend & { + resource: { + apiGroup: string; + kind: string; + name: string; + } + } }; status: { loadBalancer: { @@ -75,7 +109,9 @@ export class Ingress extends KubeObject { const host = rule.host ? rule.host : "*"; if (rule.http && rule.http.paths) { rule.http.paths.forEach(path => { - routes.push(protocol + "://" + host + (path.path || "/") + " ⇢ " + path.backend.serviceName + ":" + path.backend.servicePort); + const { serviceName, servicePort } = getBackendServiceNamePort(path.backend); + + routes.push(protocol + "://" + host + (path.path || "/") + " ⇢ " + serviceName + ":" + servicePort); }); } }); @@ -83,6 +119,17 @@ export class Ingress extends KubeObject { return routes; } + getServiceNamePort() { + const { spec } = this; + const serviceName = spec?.defaultBackend?.service.name ?? spec?.backend?.serviceName; + const servicePort = spec?.defaultBackend?.service.port.number ?? spec?.defaultBackend?.service.port.name ?? spec?.backend?.servicePort; + + return { + serviceName, + servicePort + }; + } + getHosts() { const { spec: { rules } } = this; if (!rules) return []; @@ -91,22 +138,24 @@ export class Ingress extends KubeObject { getPorts() { const ports: number[] = []; - const { spec: { tls, rules, backend } } = this; + const { spec: { tls, rules, backend, defaultBackend } } = this; const httpPort = 80; const tlsPort = 443; + + // Note: not using the port name (string) + const servicePort = defaultBackend?.service.port.number ?? backend?.servicePort; + if (rules && rules.length > 0) { if (rules.some(rule => rule.hasOwnProperty("http"))) { ports.push(httpPort); } - } - else { - if (backend && backend.servicePort) { - ports.push(backend.servicePort); - } + } else if (servicePort !== undefined) { + ports.push(Number(servicePort)); } if (tls && tls.length > 0) { ports.push(tlsPort); } + return ports.join(", "); } @@ -121,4 +170,8 @@ export class Ingress extends KubeObject { export const ingressApi = new IngressApi({ objectConstructor: Ingress, -}); + // Add fallback for Kubernetes <1.19 + checkPreferredVersion: true, + fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"], + logStuff: true +} as any); diff --git a/src/renderer/api/kube-api.ts b/src/renderer/api/kube-api.ts index d4840d3620..0a0d153e18 100644 --- a/src/renderer/api/kube-api.ts +++ b/src/renderer/api/kube-api.ts @@ -8,10 +8,22 @@ import { apiKube } from "./index"; import { kubeWatchApi } from "./kube-watch-api"; import { apiManager } from "./api-manager"; import { createKubeApiURL, parseKubeApi } from "./kube-api-parse"; -import { apiKubePrefix, isDevelopment } from "../../common/vars"; +import { apiKubePrefix, isDevelopment, isTestEnv } from "../../common/vars"; export interface IKubeApiOptions { - apiBase?: string; // base api-path for listing all resources, e.g. "/api/v1/pods" + /** + * base api-path for listing all resources, e.g. "/api/v1/pods" + */ + apiBase?: string; + + /** + * If the API uses a different API endpoint (e.g. apiBase) depending on the cluster version, + * fallback API bases can be listed individually. + * The first (existing) API base is used in the requests, if apiBase is not found. + * This option only has effect if checkPreferredVersion is true. + */ + fallbackApiBases?: string[]; + objectConstructor?: IKubeObjectConstructor; request?: KubeJsonApi; isNamespaced?: boolean; @@ -35,6 +47,17 @@ export interface IKubePreferredVersion { } } +export interface IKubeResourceList { + resources: { + kind: string; + name: string; + namespaced: boolean; + singularName: string; + storageVersionHash: string; + verbs: string[]; + }[]; +} + export interface IKubeApiCluster { id: string; } @@ -85,7 +108,7 @@ export class KubeApi { if (!options.apiBase) { options.apiBase = objectConstructor.apiBase; } - const { apiBase, apiPrefix, apiGroup, apiVersion, apiVersionWithGroup, resource } = KubeApi.parseApi(options.apiBase); + const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = KubeApi.parseApi(options.apiBase); this.kind = kind; this.isNamespaced = isNamespaced; @@ -108,8 +131,73 @@ export class KubeApi { .join("/"); } + /** + * Returns the latest API prefix/group that contains the required resource. + * First tries options.apiBase, then urls in order from options.fallbackApiBases. + */ + private async getLatestApiPrefixGroup() { + // Note that this.options.apiBase is the "full" url, whereas this.apiBase is parsed + const apiBases = [this.options.apiBase, ...this.options.fallbackApiBases]; + + for (const apiUrl of apiBases) { + // Split e.g. "/apis/extensions/v1beta1/ingresses" to parts + const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = KubeApi.parseApi(apiUrl); + + // Request available resources + try { + const response = await this.request.get(`${apiPrefix}/${apiVersionWithGroup}`); + + // If the resource is found in the group, use this apiUrl + if (response.resources?.find(kubeResource => kubeResource.name === resource)) { + return { apiPrefix, apiGroup }; + } + } catch (error) { + // Exception is ignored as we can try the next url + console.error(error); + } + } + + // Avoid throwing in tests + if (isTestEnv) { + return { + apiPrefix: this.apiPrefix, + apiGroup: this.apiGroup + }; + } + + throw new Error(`Can't find working API for the Kubernetes resource ${this.apiResource}`); + } + + /** + * Get the apiPrefix and apiGroup to be used for fetching the preferred version. + */ + private async getPreferredVersionPrefixGroup() { + if (this.options.fallbackApiBases) { + return this.getLatestApiPrefixGroup(); + } else { + return { + apiPrefix: this.apiPrefix, + apiGroup: this.apiGroup + }; + } + } + protected async checkPreferredVersion() { + if (this.options.fallbackApiBases && !this.options.checkPreferredVersion) { + throw new Error("checkPreferredVersion must be enabled if fallbackApiBases is set in KubeApi"); + } + if (this.options.checkPreferredVersion && this.apiVersionPreferred === undefined) { + const { apiPrefix, apiGroup } = await this.getPreferredVersionPrefixGroup(); + + // The apiPrefix and apiGroup might change due to fallbackApiBases, so we must override them + Object.defineProperty(this, "apiPrefix", { + value: apiPrefix + }); + Object.defineProperty(this, "apiGroup", { + value: apiGroup + }); + const res = await this.request.get(`${this.apiPrefix}/${this.apiGroup}`); Object.defineProperty(this, "apiVersionPreferred", { value: res?.preferredVersion?.version ?? null, diff --git a/src/renderer/components/+network-ingresses/ingress-details.tsx b/src/renderer/components/+network-ingresses/ingress-details.tsx index a258294abe..e2d80da651 100644 --- a/src/renderer/components/+network-ingresses/ingress-details.tsx +++ b/src/renderer/components/+network-ingresses/ingress-details.tsx @@ -14,6 +14,7 @@ import { KubeObjectDetailsProps } from "../kube-object"; import { IngressCharts } from "./ingress-charts"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +import { getBackendServiceNamePort } from "../../api/endpoints/ingress.api"; interface Props extends KubeObjectDetailsProps { } @@ -48,7 +49,9 @@ export class IngressDetails extends React.Component { { rule.http.paths.map((path, index) => { - const backend = `${path.backend.serviceName}:${path.backend.servicePort}`; + const { serviceName, servicePort } = getBackendServiceNamePort(path.backend); + const backend =`${serviceName}:${servicePort}`; + return ( {path.path || ""} @@ -100,6 +103,9 @@ export class IngressDetails extends React.Component { Network, Duration, ]; + + const { serviceName, servicePort } = ingress.getServiceNamePort(); + return (
{ {spec.tls.map((tls, index) =>

{tls.secretName}

)} } - {spec.backend && spec.backend.serviceName && spec.backend.servicePort && + {serviceName && servicePort && Service}> - {spec.backend.serviceName}:{spec.backend.servicePort} + {serviceName}:{servicePort} } Rules}/> @@ -134,14 +140,14 @@ export class IngressDetails extends React.Component { kubeObjectDetailRegistry.add({ kind: "Ingress", - apiVersions: ["extensions/v1beta1"], + apiVersions: ["networking.k8s.io/v1", "extensions/v1beta1"], components: { Details: (props) => } }); kubeObjectDetailRegistry.add({ kind: "Ingress", - apiVersions: ["extensions/v1beta1"], + apiVersions: ["networking.k8s.io/v1", "extensions/v1beta1"], priority: 5, components: { Details: (props) => From 5fd1abe99d8317a24c775b364647a7738bd85c43 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 20 Nov 2020 13:01:12 +0200 Subject: [PATCH 04/13] Release v4.0.0-beta.4 (#1462) Signed-off-by: Jari Kolehmainen --- package.json | 2 +- static/RELEASE_NOTES.md | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 49c7f58bf5..92b14c0053 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.0.0-beta.3", + "version": "4.0.0-beta.4", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index a111b2a50b..3bbff0b8a5 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,7 @@ Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 4.0.0-beta.3 (current version) +## 4.0.0-beta.4 (current version) - Extension API - Improved pod logs @@ -10,6 +10,7 @@ Here you can find description of changes we've built into each release. While we - Tray icon - Add last-status information for container - Add LoadBalancer information to Ingress view +- Add search by ip to Pod view - Move tracker to an extension - Add support page (as an extension) - Ability to restart deployment @@ -19,6 +20,8 @@ Here you can find description of changes we've built into each release. While we - Add +/- buttons in scale deployment popup screen - Update chart details when selecting another chart - Use latest alpine version (3.12) for shell sessions +- Fix errors on app quit +- Fix kube-auth-proxy to accept only target cluster hostname ## 3.6.8 - Fix cluster connection issue when opening cluster settings for disconnected clusters From 8c738619629f08ffaf4d81cb4d73c06d3104d409 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Fri, 20 Nov 2020 14:53:28 +0300 Subject: [PATCH 05/13] Open last active cluster after switching workspaces (#1444) * Save and restore lastActiveClusterId Signed-off-by: Alex Andreev * Activate clusters from workspaces page Signed-off-by: Alex Andreev * Fix saving last cluster while jumping from tray Signed-off-by: Alex Andreev * Adding workspace switch tests Signed-off-by: Alex Andreev * Remove console.log() Signed-off-by: Alex Andreev * Cleaning up Signed-off-by: Alex Andreev * Clean duplicated ClusterId definition Signed-off-by: Alex Andreev * Moving lastActiveClusterId field into WorkspaceModel Signed-off-by: Alex Andreev * fix extensionLoader error on dev environments where renderer might start early (#1447) Signed-off-by: Jari Kolehmainen * Add Search by Ip to Pod View (#1445) Signed-off-by: Pavel Ashevskii * Make BaseStore abstract (#1431) * make BaseStore abstract so that implementers are forced to decide how to store data Signed-off-by: Sebastian Malton * Enforce semicolons in eslint Signed-off-by: Panu Horsmalahti * Add a few missing folders to be linted. Signed-off-by: Panu Horsmalahti * Use @typescript-eslint/semi. Signed-off-by: Panu Horsmalahti * Allow extension cluster menus to have a parent (#1452) Signed-off-by: Jari Kolehmainen * fix SwitchCase indent rule in eslint (#1454) Signed-off-by: Sebastian Malton * Revert "fix SwitchCase indent rule in eslint (#1454)" This reverts commit 082774fe6eb7158db55389c786f8c9be89f66290. * Revert "Allow extension cluster menus to have a parent (#1452)" This reverts commit 622c45cd6d532a3e3ff3c49c7724bc0573ddb264. * Revert "Use @typescript-eslint/semi." This reverts commit 890fa5dd9e238dd173e7069c5b4a35e5c4967524. * Revert "Add a few missing folders to be linted." This reverts commit c7b24c29224646bdf3f0ee8a6c37db3c54694a02. * Revert "Enforce semicolons in eslint" This reverts commit ca67caea6097137c304d0a8f7737a34bacdeacf1. * Revert "Make BaseStore abstract (#1431)" This reverts commit 4b56ab7c611f6848de720e47b3b4f3f7f7433511. * Revert "Add Search by Ip to Pod View (#1445)" This reverts commit 4079214dc15c9e7fed8eb7b64bf021cc19a62b7f. * Revert "fix extensionLoader error on dev environments where renderer might start early (#1447)" This reverts commit 8a3613ac6f4aa9b8686a4a08abce6f351ffeed04. * Split workspace tests to smaller ones Signed-off-by: Alex Andreev * Missing semicolons Signed-off-by: Alex Andreev * Split workspace tests a bit more Signed-off-by: Alex Andreev * Adding extra click in Add Cluster button Signed-off-by: Alex Andreev * Adding more awaits to check running cluster Signed-off-by: Alex Andreev * Wait for minikube before running tests Signed-off-by: Alex Andreev Co-authored-by: Jari Kolehmainen Co-authored-by: pashevskii <53330707+pashevskii@users.noreply.github.com> Co-authored-by: Sebastian Malton Co-authored-by: Panu Horsmalahti --- integration/__tests__/app.tests.ts | 83 ++++++++++++++----- src/common/__tests__/cluster-store.test.ts | 1 + src/common/cluster-store.ts | 9 +- src/common/workspace-store.ts | 16 +++- src/main/tray.ts | 1 - .../components/+workspaces/workspace-menu.tsx | 17 +++- .../components/+workspaces/workspaces.tsx | 9 +- .../cluster-manager/clusters-menu.tsx | 1 + 8 files changed, 106 insertions(+), 31 deletions(-) diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index ace852904d..c91897ef27 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -35,6 +35,29 @@ describe("Lens integration tests", () => { await app.client.waitUntilTextExists("h1", "Welcome"); }; + const minikubeReady = (): boolean => { + // determine if minikube is running + let status = spawnSync("minikube status", { shell: true }); + if (status.status !== 0) { + console.warn("minikube not running"); + return false; + } + + // Remove TEST_NAMESPACE if it already exists + status = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true }); + if (status.status === 0) { + console.warn(`Removing existing ${TEST_NAMESPACE} namespace`); + status = spawnSync(`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, { shell: true }); + if (status.status !== 0) { + console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${status.stderr.toString()}`); + return false; + } + console.log(status.stdout.toString()); + } + return true; + }; + const ready = minikubeReady(); + describe("app start", () => { beforeAll(appStart, 20000); @@ -73,28 +96,48 @@ describe("Lens integration tests", () => { }); }); - const minikubeReady = (): boolean => { - // determine if minikube is running - let status = spawnSync("minikube status", { shell: true }); - if (status.status !== 0) { - console.warn("minikube not running"); - return false; - } + describeif(ready)("workspaces", () => { + beforeAll(appStart, 20000); - // Remove TEST_NAMESPACE if it already exists - status = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true }); - if (status.status === 0) { - console.warn(`Removing existing ${TEST_NAMESPACE} namespace`); - status = spawnSync(`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, { shell: true }); - if (status.status !== 0) { - console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${status.stderr.toString()}`); - return false; + afterAll(async () => { + if (app && app.isRunning()) { + return util.tearDown(app); } - console.log(status.stdout.toString()); - } - return true; - }; - const ready = minikubeReady(); + }); + + it('creates new workspace', async () => { + await clickWhatsNew(app); + await app.client.click('#current-workspace .Icon'); + await app.client.click('a[href="/workspaces"]'); + await app.client.click('.Workspaces button.Button'); + await app.client.keys("test-workspace"); + await app.client.click('.Workspaces .Input.description input'); + await app.client.keys("test description"); + await app.client.click('.Workspaces .workspace.editing .Icon'); + await app.client.waitUntilTextExists(".workspace .name a", "test-workspace"); + }); + + it('adds cluster in default workspace', async () => { + await addMinikubeCluster(app); + await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); + await app.client.waitForExist(`iframe[name="minikube"]`); + await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active"); + }); + + it('adds cluster in test-workspace', async () => { + await app.client.click('#current-workspace .Icon'); + await app.client.click('.WorkspaceMenu li[title="test description"]'); + await addMinikubeCluster(app); + await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); + await app.client.waitForExist(`iframe[name="minikube"]`); + }); + + it('checks if default workspace has active cluster', async () => { + await app.client.click('#current-workspace .Icon'); + await app.client.click('.WorkspaceMenu > li:first-of-type'); + await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active"); + }); + }); const addMinikubeCluster = async (app: Application) => { await app.client.click("div.add-cluster"); diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 693c2e352c..f0e03b1b12 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -65,6 +65,7 @@ describe("empty config", () => { it("sets active cluster", () => { clusterStore.setActive("foo"); expect(clusterStore.active.id).toBe("foo"); + expect(workspaceStore.currentWorkspace.lastActiveClusterId).toBe("foo"); }); }); diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 24c89d7ca9..4a5f358571 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -1,4 +1,4 @@ -import type { WorkspaceId } from "./workspace-store"; +import { workspaceStore } from "./workspace-store"; import path from "path"; import { app, ipcRenderer, remote, webFrame } from "electron"; import { unlink } from "fs-extra"; @@ -11,9 +11,10 @@ import { appEventBus } from "./event-bus"; import { dumpConfigYaml } from "./kube-helpers"; import { saveToAppFiles } from "./utils/saveToAppFiles"; import { KubeConfig } from "@kubernetes/client-node"; +import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; import _ from "lodash"; import move from "array-move"; -import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; +import type { WorkspaceId } from "./workspace-store"; export interface ClusterIconUpload { clusterId: string; @@ -142,7 +143,9 @@ export class ClusterStore extends BaseStore { @action setActive(id: ClusterId) { - this.activeCluster = this.clusters.has(id) ? id : null; + const clusterId = this.clusters.has(id) ? id : null; + this.activeCluster = clusterId; + workspaceStore.setLastActiveClusterId(clusterId); } @action diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index 67e1c4d22a..75ab36f19e 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -5,12 +5,13 @@ import { clusterStore } from "./cluster-store"; import { appEventBus } from "./event-bus"; import { broadcastMessage } from "../common/ipc"; import logger from "../main/logger"; +import type { ClusterId } from "./cluster-store"; export type WorkspaceId = string; export interface WorkspaceStoreModel { + workspaces: WorkspaceModel[]; currentWorkspace?: WorkspaceId; - workspaces: WorkspaceModel[] } export interface WorkspaceModel { @@ -18,6 +19,7 @@ export interface WorkspaceModel { name: string; description?: string; ownerRef?: string; + lastActiveClusterId?: ClusterId; } export interface WorkspaceState { @@ -30,6 +32,7 @@ export class Workspace implements WorkspaceModel, WorkspaceState { @observable description?: string; @observable ownerRef?: string; @observable enabled: boolean; + @observable lastActiveClusterId?: ClusterId; constructor(data: WorkspaceModel) { Object.assign(this, data); @@ -66,7 +69,8 @@ export class Workspace implements WorkspaceModel, WorkspaceState { id: this.id, name: this.name, description: this.description, - ownerRef: this.ownerRef + ownerRef: this.ownerRef, + lastActiveClusterId: this.lastActiveClusterId }); } } @@ -138,13 +142,12 @@ export class WorkspaceStore extends BaseStore { } @action - setActive(id = WorkspaceStore.defaultId, reset = true) { + setActive(id = WorkspaceStore.defaultId) { if (id === this.currentWorkspaceId) return; if (!this.getById(id)) { throw new Error(`workspace ${id} doesn't exist`); } this.currentWorkspaceId = id; - clusterStore.activeCluster = null; // fixme: handle previously selected cluster from current workspace } @action @@ -184,6 +187,11 @@ export class WorkspaceStore extends BaseStore { clusterStore.removeByWorkspaceId(id); } + @action + setLastActiveClusterId(clusterId?: ClusterId, workspaceId = this.currentWorkspaceId) { + this.getById(workspaceId).lastActiveClusterId = clusterId; + } + @action protected fromStore({ currentWorkspace, workspaces = [] }: WorkspaceStoreModel) { if (currentWorkspace) { diff --git a/src/main/tray.ts b/src/main/tray.ts index f84ea21896..f98c064bdd 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -95,7 +95,6 @@ export function createTrayMenu(windowManager: WindowManager): Menu { toolTip: clusterId, async click() { workspaceStore.setActive(workspace); - clusterStore.setActive(clusterId); windowManager.navigate(clusterViewURL({ params: { clusterId } })); } }; diff --git a/src/renderer/components/+workspaces/workspace-menu.tsx b/src/renderer/components/+workspaces/workspace-menu.tsx index 78e61f87aa..5cc29c8ae2 100644 --- a/src/renderer/components/+workspaces/workspace-menu.tsx +++ b/src/renderer/components/+workspaces/workspace-menu.tsx @@ -7,8 +7,11 @@ import { Trans } from "@lingui/macro"; import { Menu, MenuItem, MenuProps } from "../menu"; import { Icon } from "../icon"; import { observable } from "mobx"; -import { workspaceStore } from "../../../common/workspace-store"; +import { WorkspaceId, workspaceStore } from "../../../common/workspace-store"; import { cssNames } from "../../utils"; +import { navigate } from "../../navigation"; +import { clusterViewURL } from "../cluster-manager/cluster-view.route"; +import { landingURL } from "../+landing-page"; interface Props extends Partial { } @@ -17,6 +20,16 @@ interface Props extends Partial { export class WorkspaceMenu extends React.Component { @observable menuVisible = false; + activateWorkspace = (id: WorkspaceId) => { + const clusterId = workspaceStore.getById(id).lastActiveClusterId; + workspaceStore.setActive(id); + if (clusterId) { + navigate(clusterViewURL({ params: { clusterId } })); + } else { + navigate(landingURL()); + } + }; + render() { const { className, ...menuProps } = this.props; const { enabledWorkspacesList, currentWorkspace } = workspaceStore; @@ -38,7 +51,7 @@ export class WorkspaceMenu extends React.Component { key={workspaceId} title={description} active={workspaceId === currentWorkspace.id} - onClick={() => workspaceStore.setActive(workspaceId)} + onClick={() => this.activateWorkspace(workspaceId)} > {name} diff --git a/src/renderer/components/+workspaces/workspaces.tsx b/src/renderer/components/+workspaces/workspaces.tsx index 459f095cec..0f1d600137 100644 --- a/src/renderer/components/+workspaces/workspaces.tsx +++ b/src/renderer/components/+workspaces/workspaces.tsx @@ -13,6 +13,7 @@ import { Input } from "../input"; import { cssNames, prevDefault } from "../../utils"; import { Button } from "../button"; import { isRequired, InputValidator } from "../input/input_validators"; +import { clusterStore } from "../../../common/cluster-store"; @observer export class Workspaces extends React.Component { @@ -70,6 +71,12 @@ export class Workspaces extends React.Component { this.editingWorkspaces.set(id, toJS(workspace)); }; + activateWorkspace = (id: WorkspaceId) => { + const clusterId = workspaceStore.getById(id).lastActiveClusterId; + workspaceStore.setActive(id); + clusterStore.setActive(clusterId); + }; + clearEditing = (id: WorkspaceId) => { this.editingWorkspaces.delete(id); }; @@ -135,7 +142,7 @@ export class Workspaces extends React.Component { {!isEditing && ( - workspaceStore.setActive(workspaceId))}>{name} + this.activateWorkspace(workspaceId))}>{name} {isActive && (current)} {description} diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 7f11cf395f..ab608815aa 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -77,6 +77,7 @@ export class ClustersMenu extends React.Component { ok: () => { if (clusterStore.activeClusterId === cluster.id) { navigate(landingURL()); + clusterStore.setActive(null); } clusterStore.removeById(cluster.id); }, From 6a0dd4edfb466dad367da79a00d40834e7dd8c99 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Mon, 23 Nov 2020 09:07:37 +0200 Subject: [PATCH 06/13] Replace deprecated stable helm repository with bitnami (#1479) * Replace deprecated stable repo with bitnami Signed-off-by: Lauri Nevala --- integration/__tests__/app.tests.ts | 2 +- src/main/helm/helm-repo-manager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index c91897ef27..35891ee5fb 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -84,7 +84,7 @@ describe("Lens integration tests", () => { }); it('ensures helm repos', async () => { - await app.client.waitUntilTextExists("div.repos #message-stable", "stable"); // wait for the helm-cli to fetch the stable repo + await app.client.waitUntilTextExists("div.repos #message-bitnami", "bitnami"); // wait for the helm-cli to fetch the bitnami repo await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text) }); diff --git a/src/main/helm/helm-repo-manager.ts b/src/main/helm/helm-repo-manager.ts index dff372a301..30ce8e3435 100644 --- a/src/main/helm/helm-repo-manager.ts +++ b/src/main/helm/helm-repo-manager.ts @@ -83,7 +83,7 @@ export class HelmRepoManager extends Singleton { repositories: [] })); if (!repositories.length) { - await this.addRepo({ name: "stable", url: "https://kubernetes-charts.storage.googleapis.com/" }); + await this.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" }); return await this.repositories(); } return repositories.map(repo => ({ From 4b4baf2d4b0c9ddfca43a86a16386b64d808c92f Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 23 Nov 2020 11:37:30 +0200 Subject: [PATCH 07/13] Add contributing/development pages (#1480) Signed-off-by: Jari Kolehmainen --- CODE_OF_CONDUCT.md | 132 ++++++++++++++++++++++++ CONTRIBUTING.md | 3 + Makefile | 4 + README.md | 45 +------- docs/contributing/development.md | 39 ++++++- docs/contributing/documentation.md | 11 ++ docs/contributing/github_workflow.md | 148 +++++++++++++++++++++++++++ docs/contributing/testing.md | 45 ++++++++ 8 files changed, 385 insertions(+), 42 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/contributing/github_workflow.md create mode 100644 docs/contributing/testing.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..6eaa07c2fd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +info@k8slens.dev. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..1c745f852d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing to Lens + +See [Contributing to Lens](https://docs.k8slens.dev/latest/contributing/) documentation. diff --git a/Makefile b/Makefile index a1970bee5d..1f8d4f5392 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,10 @@ publish-npm: build-npm npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}" cd src/extensions/npm/extensions && npm publish --access=public +.PHONY: docs +docs: + yarn mkdocs-serve-local + .PHONY: clean-extensions clean-extensions: ifeq "$(DETECTED_OS)" "Windows" diff --git a/README.md b/README.md index 8d09dc4a3d..7d86bf6c52 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Lens | The Kubernetes IDE [![Build Status](https://dev.azure.com/lensapp/lensapp/_apis/build/status/lensapp.lens?branchName=master)](https://dev.azure.com/lensapp/lensapp/_build/latest?definitionId=1&branchName=master) -[![Releases](https://img.shields.io/github/downloads/lensapp/lens/total.svg)](https://github.com/lensapp/lens/releases) +[![Releases](https://img.shields.io/github/downloads/lensapp/lens/total.svg)](https://github.com/lensapp/lens/releases?label=Downloads) [![Chat on Slack](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack&longCache=true&style=flat)](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI) World’s most popular Kubernetes IDE provides a simplified, consistent entry point for developers, testers, integrators, and DevOps, to ship code faster at scale. Lens is the only IDE you’ll ever need to take control of your Kubernetes clusters. It is a standalone application for MacOS, Windows and Linux operating systems. Lens is an open source project and free! @@ -25,49 +25,12 @@ World’s most popular Kubernetes IDE provides a simplified, consistent entry po ## Installation -Download a pre-built package from the [releases](https://github.com/lensapp/lens/releases) page. Lens can be also installed via [snapcraft](https://snapcraft.io/kontena-lens) (Linux only). - -Alternatively on Mac: -``` -brew cask install lens -``` +See [Getting Started](https://docs.k8slens.dev/latest/getting-started/) page. ## Development -> Prerequisites: Nodejs v12, make, yarn - -* `make dev` - builds and starts the app -* `make test` - run tests - -## Development (advanced) - -Allows for faster separate re-runs of some of the more involved processes: - -1. `yarn dev:main` compiles electron's main process app part -1. `yarn dev:renderer` compiles electron's renderer app part -1. `yarn dev:extension-types` compile declaration types for `@k8slens/extensions` -1. `yarn dev-run` runs app in dev-mode and auto-restart when main process file has changed - -## Development (documentation) - -Run a local instance of `mkdocs serve` in a docker container for developing the Lens Documentation. - -> Prerequisites: docker, yarn - -* `yarn mkdocs-serve-local` - local build and serve of mkdocs with auto update enabled - -Go to [localhost:8000](http://127.0.0.1:8000) - -## Developer's ~~RTFM~~ recommended list: - -- [TypeScript](https://www.typescriptlang.org/docs/home.html) (front-end/back-end) -- [ReactJS](https://reactjs.org/docs/getting-started.html) (front-end, ui) -- [MobX](https://mobx.js.org/) (app-state-management, back-end/front-end) -- [ElectronJS](https://www.electronjs.org/docs) (chrome/node) -- [NodeJS](https://nodejs.org/dist/latest-v12.x/docs/api/) (api docs) - - +See [Development](https://docs.k8slens.dev/latest/contributing/development/) page. ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/lensapp/lens. \ No newline at end of file +See [Contributing](https://docs.k8slens.dev/latest/contributing/) page. diff --git a/docs/contributing/development.md b/docs/contributing/development.md index 8e5633cda9..b92edd319e 100644 --- a/docs/contributing/development.md +++ b/docs/contributing/development.md @@ -1,3 +1,40 @@ # Development -TBD +Thank you for taking the time to make a contribution to Lens. The following document is a set of guidelines and instructions for contributing to Lens. + +When contributing to this repository, please consider first discussing the change you wish to make by opening an issue. + +## Recommended Reading: + +- [TypeScript](https://www.typescriptlang.org/docs/home.html) (front-end/back-end) +- [ReactJS](https://reactjs.org/docs/getting-started.html) (front-end, ui) +- [MobX](https://mobx.js.org/) (app-state-management, back-end/front-end) +- [ElectronJS](https://www.electronjs.org/docs) (chrome/node) +- [NodeJS](https://nodejs.org/dist/latest-v12.x/docs/api/) (api docs) + +## Local Development Environment + +> Prerequisites: Nodejs v12, make, yarn + +* `make dev` - builds and starts the app +* `make clean` - cleanup local environment build artifacts + + +## Github Workflow + +We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), so all code changes are tracked via Pull Requests. +A detailed guide on the recommended workflow can be found below: + +* [Github Workflow](./github_workflow.md) + +## Code Testing + +All submitted PRs go through a set of tests and reviews. You can run most of these tests *before* a PR is submitted. +In fact, we recommend it, because it will save on many possible review iterations and automated tests. +The testing guidelines can be found here: + +* [Contributor's Guide to Testing](./testing.md) + +## License + +By contributing, you agree that your contributions will be licensed as described in [LICENSE](https://github.com/lensapp/lens/blob/master/LICENSE). diff --git a/docs/contributing/documentation.md b/docs/contributing/documentation.md index 2157a1b1d4..501c57779f 100644 --- a/docs/contributing/documentation.md +++ b/docs/contributing/documentation.md @@ -20,3 +20,14 @@ When you create a new pull request, we expect some requirements to be met. * When updating documentation, add `Update Documentation:` before the title. E.g. `Update Documentation: Getting Started` * If your Pull Request closes an issue, you must write `Closes #ISSUE_NUMBER` where the ISSUE_NUMBER is the number in the end of the link url or the relevent issue. This will link your pull request to the issue, and when it is merged, the issue will close. * For each pull request made, we run tests to check if there are any broken links, the markdown formatting is valid, and the linter is passing. + + +## Testing Documentation Site Locally + +Run a local instance of `mkdocs` in a docker container for developing the Lens Documentation. + +> Prerequisites: docker, yarn + +* `make docs` - local build and serve of mkdocs with auto update enabled + +Go to [localhost:8000](http://127.0.0.1:8000). diff --git a/docs/contributing/github_workflow.md b/docs/contributing/github_workflow.md new file mode 100644 index 0000000000..3509645b47 --- /dev/null +++ b/docs/contributing/github_workflow.md @@ -0,0 +1,148 @@ +# Github Workflow + + +- [Fork The Project](#fork-the-project) +- [Adding the Forked Remote](#adding-the-forked-remote) +- [Create & Rebase Your Feature Branch](#create--rebase-your-feature-branch) +- [Commit & Push](#commit--push) +- [Open a Pull Request](#open-a-pull-request) + - [Get a code review](#get-a-code-review) + - [Squash commits](#squash-commits) + - [Push Your Final Changes](#push-your-final-changes) + + +This guide assumes you have already cloned the upstream repo to your system via git clone. + +## Fork The Project + +1. Go to [http://github.com/lensapp/lens](http://github.com/lensapp/lens) +2. On the top, right-hand side, click on "fork" and select your username for the fork destination. + +## Adding the Forked Remote + +``` +export GITHUB_USER={ your github's username } + +cd $WORKDIR/lens +git remote add $GITHUB_USER git@github.com:${GITHUB_USER}/lens.git + +# Prevent push to Upstream +git remote set-url --push origin no_push + +# Set your fork remote as a default push target +git push --set-upstream $GITHUB_USER master +``` + +Your remotes should look something like this: + +``` +➜ git remote -v +origin https://github.com/lensapp/lens (fetch) +origin no_push (push) +my_fork git@github.com:{ github_username }/lens.git (fetch) +my_fork git@github.com:{ github_username }/lens.git (push) +``` + +## Create & Rebase Your Feature Branch + +Create a feature branch: + +``` +git branch -b my_feature_branch +``` + +Rebase your branch: + +``` +git fetch origin + +git rebase origin/master +Current branch my_feature_branch is up to date. +``` + +Please don't use `git pull` instead of the above `fetch / rebase`. `git pull` does a merge, which leaves merge commits. These make the commit history messy and violate the principle that commits ought to be individually understandable and useful. + +## Commit & Push + +Commit and sign your changes: + +``` +git commit -m "my commit title" --signoff +``` + +You can go back and edit/build/test some more, then `commit --amend` in a few cycles. + +When ready, push your changes to your fork's repository: + +``` +git push --set-upstream my_fork my_feature_branch +``` + +## Open a Pull Request + +See [Github Docs](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork). + +### Get a code review + +Once your pull request has been opened it will be assigned to one or more reviewers, and will go through a series of smoke tests. + +Commit changes made in response to review comments should be added to the same branch on your fork. + +Very small PRs are easy to review. Very large PRs are very difficult to review. + +### Squashing Commits +Commits on your branch should represent meaningful milestones or units of work. +Small commits that contain typo fixes, rebases, review feedbacks, etc should be squashed. + +To do that, it's best to perform an [interactive rebase](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History): + +#### Example +If you PR has 3 commits, count backwards from your last commit using `HEAD~3`: +``` +git rebase -i HEAD~3 +``` +Output would be similar to this: +``` +pick f7f3f6d Changed some code +pick 310154e fixed some typos +pick a5f4a0d made some review changes + +# Rebase 710f0f8..a5f4a0d onto 710f0f8 +# +# Commands: +# p, pick = use commit +# r, reword = use commit, but edit the commit message +# e, edit = use commit, but stop for amending +# s, squash = use commit, but meld into previous commit +# f, fixup = like "squash", but discard this commit's log message +# x, exec = run command (the rest of the line) using shell +# b, break = stop here (continue rebase later with 'git rebase --continue') +# d, drop = remove commit +# l, label