From a7cb8d2d0416b849a5f4bd2ed56f6d890f5484e4 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 5 Jun 2020 08:38:56 +0300 Subject: [PATCH 01/10] Dynamic dashboard UI based on RBAC rules (#366) Signed-off-by: Jari Kolehmainen --- dashboard/client/api/json-api.ts | 5 +- dashboard/client/api/kube-watch-api.ts | 8 ++- dashboard/client/api/rbac.ts | 15 ++++++ .../client/components/+cluster/cluster.tsx | 4 ++ .../client/components/+config/config.tsx | 30 +++++++----- .../+namespaces/namespace-select.tsx | 5 +- .../client/components/+network/network.tsx | 31 +++++++----- .../overview-statuses.scss | 5 +- .../+workloads-overview/overview-statuses.tsx | 27 +++++++--- .../+workloads-overview/overview.tsx | 43 +++++++++++----- .../components/+workloads/workloads.tsx | 46 +++++++++++------ dashboard/client/components/app.tsx | 3 +- .../item-object-list/item-list-layout.tsx | 11 +++-- .../client/components/layout/sidebar.tsx | 16 ++++-- dashboard/server/common/license.ts | 12 ----- src/main/routes/config.ts | 49 ++++++++++++++----- 16 files changed, 213 insertions(+), 97 deletions(-) create mode 100644 dashboard/client/api/rbac.ts delete mode 100644 dashboard/server/common/license.ts diff --git a/dashboard/client/api/json-api.ts b/dashboard/client/api/json-api.ts index abf278497a..5f50296b0d 100644 --- a/dashboard/client/api/json-api.ts +++ b/dashboard/client/api/json-api.ts @@ -105,8 +105,9 @@ export class JsonApi { this.onData.emit(data, res); this.writeLog({ ...log, data }); return data; - } - else { + } else if (log.method === "GET" && res.status === 403) { + this.writeLog({ ...log, data }); + } else { const error = new JsonApiErrorParsed(data, this.parseError(data, res)); this.onError.emit(error, res); this.writeLog({ ...log, error }) diff --git a/dashboard/client/api/kube-watch-api.ts b/dashboard/client/api/kube-watch-api.ts index b6abe9fb18..3628f9d790 100644 --- a/dashboard/client/api/kube-watch-api.ts +++ b/dashboard/client/api/kube-watch-api.ts @@ -107,8 +107,12 @@ export class KubeWatchApi { const { apiBase, namespace } = KubeApi.parseApi(url); const api = apiManager.getApi(apiBase); if (api) { - await api.refreshResourceVersion({ namespace }); - this.reconnect(); + try { + await api.refreshResourceVersion({ namespace }); + this.reconnect(); + } catch(error) { + console.debug("failed to refresh resource version", error) + } } } } diff --git a/dashboard/client/api/rbac.ts b/dashboard/client/api/rbac.ts new file mode 100644 index 0000000000..b15b83353e --- /dev/null +++ b/dashboard/client/api/rbac.ts @@ -0,0 +1,15 @@ +import { configStore } from "../config.store"; +import { isArray } from "util"; + +export function isAllowedResource(resources: string|string[]) { + if (!isArray(resources)) { + resources = [resources]; + } + const { allowedResources } = configStore; + for (const resource of resources) { + if (!allowedResources.includes(resource)) { + return false; + } + } + return true; +} diff --git a/dashboard/client/components/+cluster/cluster.tsx b/dashboard/client/components/+cluster/cluster.tsx index 979f811067..13df2cad0a 100644 --- a/dashboard/client/components/+cluster/cluster.tsx +++ b/dashboard/client/components/+cluster/cluster.tsx @@ -13,6 +13,7 @@ import { nodesStore } from "../+nodes/nodes.store"; import { podsStore } from "../+workloads-pods/pods.store"; import { clusterStore } from "./cluster.store"; import { eventStore } from "../+events/event.store"; +import { isAllowedResource } from "../../api/rbac"; @observer export class Cluster extends React.Component { @@ -25,6 +26,9 @@ export class Cluster extends React.Component { async componentDidMount() { const { dependentStores } = this; + if (!isAllowedResource("nodes")) { + dependentStores.splice(dependentStores.indexOf(nodesStore), 1) + } this.watchers.forEach(watcher => watcher.start(true)); await Promise.all([ diff --git a/dashboard/client/components/+config/config.tsx b/dashboard/client/components/+config/config.tsx index 935cd9affd..71c6ead907 100644 --- a/dashboard/client/components/+config/config.tsx +++ b/dashboard/client/components/+config/config.tsx @@ -9,8 +9,8 @@ import { namespaceStore } from "../+namespaces/namespace.store"; import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config-resource-quotas"; import { configURL } from "./config.route"; import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers"; -import { Certificates, ClusterIssuers, Issuers } from "../+custom-resources/certmanager.k8s.io"; import { buildURL } from "../../navigation"; +import { isAllowedResource } from "../../api/rbac" export const certificatesURL = buildURL("/certificates"); export const issuersURL = buildURL("/issuers"); @@ -20,32 +20,40 @@ export const clusterIssuersURL = buildURL("/clusterissuers"); export class Config extends React.Component { static get tabRoutes(): TabRoute[] { const query = namespaceStore.getContextParams() - return [ - { + const routes: TabRoute[] = [] + if (isAllowedResource("configmaps")) { + routes.push({ title: ConfigMaps, component: ConfigMaps, url: configMapsURL({ query }), path: configMapsRoute.path, - }, - { + }) + } + if (isAllowedResource("secrets")) { + routes.push({ title: Secrets, component: Secrets, url: secretsURL({ query }), path: secretsRoute.path, - }, - { + }) + } + if (isAllowedResource("resourcequotas")) { + routes.push({ title: Resource Quotas, component: ResourceQuotas, url: resourceQuotaURL({ query }), path: resourceQuotaRoute.path, - }, - { + }) + } + if (isAllowedResource("horizontalpodautoscalers")) { + routes.push({ title: HPA, component: HorizontalPodAutoscalers, url: hpaURL({ query }), path: hpaRoute.path, - }, - ] + }) + } + return routes; } render() { diff --git a/dashboard/client/components/+namespaces/namespace-select.tsx b/dashboard/client/components/+namespaces/namespace-select.tsx index b5004685c9..b4b5fd15cb 100644 --- a/dashboard/client/components/+namespaces/namespace-select.tsx +++ b/dashboard/client/components/+namespaces/namespace-select.tsx @@ -11,6 +11,7 @@ import { namespaceStore } from "./namespace.store"; import { _i18n } from "../../i18n"; import { FilterIcon } from "../item-object-list/filter-icon"; import { FilterType } from "../item-object-list/page-filters.store"; +import { isAllowedResource } from "../../api/rbac" interface Props extends SelectProps { showIcons?: boolean; @@ -33,7 +34,9 @@ export class NamespaceSelect extends React.Component { private unsubscribe = noop; async componentDidMount() { - if (!namespaceStore.isLoaded) await namespaceStore.loadAll(); + if (isAllowedResource("namespaces") && !namespaceStore.isLoaded) { + await namespaceStore.loadAll(); + } this.unsubscribe = namespaceStore.subscribe(); } diff --git a/dashboard/client/components/+network/network.tsx b/dashboard/client/components/+network/network.tsx index 409bde8fc2..768d732547 100644 --- a/dashboard/client/components/+network/network.tsx +++ b/dashboard/client/components/+network/network.tsx @@ -12,6 +12,7 @@ import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses"; import { NetworkPolicies, networkPoliciesRoute, networkPoliciesURL } from "../+network-policies"; import { namespaceStore } from "../+namespaces/namespace.store"; import { networkURL } from "./network.route"; +import { isAllowedResource } from "../../api/rbac"; interface Props extends RouteComponentProps<{}> { } @@ -20,32 +21,40 @@ interface Props extends RouteComponentProps<{}> { export class Network extends React.Component { static get tabRoutes(): TabRoute[] { const query = namespaceStore.getContextParams() - return [ - { + const routes: TabRoute[] = []; + if (isAllowedResource("services")) { + routes.push({ title: Services, component: Services, url: servicesURL({ query }), path: servicesRoute.path, - }, - { + }) + } + if (isAllowedResource("endpoints")) { + routes.push({ title: Endpoints, component: Endpoints, url: endpointURL({ query }), path: endpointRoute.path, - }, - { + }) + } + if (isAllowedResource("ingresses")) { + routes.push({ title: Ingresses, component: Ingresses, url: ingressURL({ query }), path: ingressRoute.path, - }, - { + }) + } + if (isAllowedResource("networkpolicies")) { + routes.push({ title: Network Policies, component: NetworkPolicies, url: networkPoliciesURL({ query }), path: networkPoliciesRoute.path, - }, - ] + }) + } + return routes } render() { @@ -59,4 +68,4 @@ export class Network extends React.Component { ) } -} \ No newline at end of file +} diff --git a/dashboard/client/components/+workloads-overview/overview-statuses.scss b/dashboard/client/components/+workloads-overview/overview-statuses.scss index 7160d67821..21e6e17cfb 100644 --- a/dashboard/client/components/+workloads-overview/overview-statuses.scss +++ b/dashboard/client/components/+workloads-overview/overview-statuses.scss @@ -16,10 +16,11 @@ .workloads { display: grid; grid-template-columns: repeat(auto-fit, 155px); - justify-content: space-between; + justify-content: space-evenly; grid-gap: $margin; padding: $padding * 2; + .workload { margin-bottom: $margin * 2; @@ -32,4 +33,4 @@ } } } -} \ No newline at end of file +} diff --git a/dashboard/client/components/+workloads-overview/overview-statuses.tsx b/dashboard/client/components/+workloads-overview/overview-statuses.tsx index 3c0a8cac0d..83575fec86 100644 --- a/dashboard/client/components/+workloads-overview/overview-statuses.tsx +++ b/dashboard/client/components/+workloads-overview/overview-statuses.tsx @@ -15,17 +15,20 @@ import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; import { namespaceStore } from "../+namespaces/namespace.store"; import { PageFiltersList } from "../item-object-list/page-filters-list"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select"; +import { configStore } from "../../config.store"; +import { isAllowedResource } from "../../api/rbac"; @observer export class OverviewStatuses extends React.Component { render() { + const { allowedResources } = configStore; const { contextNs } = namespaceStore; - const pods = podsStore.getAllByNs(contextNs); - const deployments = deploymentStore.getAllByNs(contextNs); - const statefulSets = statefulSetStore.getAllByNs(contextNs); - const daemonSets = daemonSetStore.getAllByNs(contextNs); - const jobs = jobStore.getAllByNs(contextNs); - const cronJobs = cronJobStore.getAllByNs(contextNs); + const pods = isAllowedResource("pods") ? podsStore.getAllByNs(contextNs) : []; + const deployments = isAllowedResource("deployments") ? deploymentStore.getAllByNs(contextNs) : []; + const statefulSets = isAllowedResource("statefulsets") ? statefulSetStore.getAllByNs(contextNs) : []; + const daemonSets = isAllowedResource("daemonsets") ? daemonSetStore.getAllByNs(contextNs) : []; + const jobs = isAllowedResource("jobs") ? jobStore.getAllByNs(contextNs) : []; + const cronJobs = isAllowedResource("cronjobs") ? cronJobStore.getAllByNs(contextNs) : []; return (
@@ -34,30 +37,42 @@ export class OverviewStatuses extends React.Component {
+ { isAllowedResource("pods") &&
Pods ({pods.length})
+ } + { isAllowedResource("deployments") &&
Deployments ({deployments.length})
+ } + { isAllowedResource("statefulsets") &&
StatefulSets ({statefulSets.length})
+ } + { isAllowedResource("daemonsets") &&
DaemonSets ({daemonSets.length})
+ } + { isAllowedResource("jobs") &&
Jobs ({jobs.length})
+ } + { isAllowedResource("cronjobs") &&
CronJobs ({cronJobs.length})
+ }
) diff --git a/dashboard/client/components/+workloads-overview/overview.tsx b/dashboard/client/components/+workloads-overview/overview.tsx index f47fd7ae19..c3f08d13c2 100644 --- a/dashboard/client/components/+workloads-overview/overview.tsx +++ b/dashboard/client/components/+workloads-overview/overview.tsx @@ -16,6 +16,8 @@ import { jobStore } from "../+workloads-jobs/job.store"; import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; import { Spinner } from "../spinner"; import { Events } from "../+events"; +import { KubeObjectStore } from "../../kube-object.store"; +import { isAllowedResource } from "../../api/rbac" interface Props extends RouteComponentProps { } @@ -26,16 +28,31 @@ export class WorkloadsOverview extends React.Component { @observable isUnmounting = false; async componentDidMount() { - const stores = [ - podsStore, - deploymentStore, - daemonSetStore, - statefulSetStore, - replicaSetStore, - jobStore, - cronJobStore, - eventStore, - ]; + const stores: KubeObjectStore[] = []; + if (isAllowedResource("pods")) { + stores.push(podsStore); + } + if (isAllowedResource("deployments")) { + stores.push(deploymentStore); + } + if (isAllowedResource("daemonsets")) { + stores.push(daemonSetStore); + } + if (isAllowedResource("statefulsets")) { + stores.push(statefulSetStore); + } + if (isAllowedResource("replicasets")) { + stores.push(replicaSetStore); + } + if (isAllowedResource("jobs")) { + stores.push(jobStore); + } + if (isAllowedResource("cronjobs")) { + stores.push(cronJobStore); + } + if (isAllowedResource("events")) { + stores.push(eventStore); + } this.isReady = stores.every(store => store.isLoaded); await Promise.all(stores.map(store => store.loadAll())); this.isReady = true; @@ -55,11 +72,11 @@ export class WorkloadsOverview extends React.Component { return ( <> - + /> } ) } @@ -71,4 +88,4 @@ export class WorkloadsOverview extends React.Component { ) } -} \ No newline at end of file +} diff --git a/dashboard/client/components/+workloads/workloads.tsx b/dashboard/client/components/+workloads/workloads.tsx index 855151e72c..895f1ab3a2 100644 --- a/dashboard/client/components/+workloads/workloads.tsx +++ b/dashboard/client/components/+workloads/workloads.tsx @@ -15,6 +15,7 @@ import { DaemonSets } from "../+workloads-daemonsets"; import { StatefulSets } from "../+workloads-statefulsets"; import { Jobs } from "../+workloads-jobs"; import { CronJobs } from "../+workloads-cronjobs"; +import { isAllowedResource } from "../../api/rbac" interface Props extends RouteComponentProps { } @@ -23,50 +24,63 @@ interface Props extends RouteComponentProps { export class Workloads extends React.Component { static get tabRoutes(): TabRoute[] { const query = namespaceStore.getContextParams(); - return [ + const routes: TabRoute[] = [ { title: Overview, component: WorkloadsOverview, url: overviewURL({ query }), path: overviewRoute.path - }, - { + } + ] + if (isAllowedResource("pods")) { + routes.push({ title: Pods, component: Pods, url: podsURL({ query }), path: podsRoute.path - }, - { + }) + } + if (isAllowedResource("deployments")) { + routes.push({ title: Deployments, component: Deployments, url: deploymentsURL({ query }), path: deploymentsRoute.path, - }, - { + }) + } + if (isAllowedResource("daemonsets")) { + routes.push({ title: DaemonSets, component: DaemonSets, url: daemonSetsURL({ query }), path: daemonSetsRoute.path, - }, - { + }) + } + if (isAllowedResource("statefulsets")) { + routes.push({ title: StatefulSets, component: StatefulSets, url: statefulSetsURL({ query }), path: statefulSetsRoute.path, - }, - { + }) + } + if (isAllowedResource("jobs")) { + routes.push({ title: Jobs, component: Jobs, url: jobsURL({ query }), path: jobsRoute.path, - }, - { + }) + } + if (isAllowedResource("cronjobs")) { + routes.push({ title: CronJobs, component: CronJobs, url: cronJobsURL({ query }), path: cronJobsRoute.path, - }, - ] + }) + } + return routes; }; render() { @@ -80,4 +94,4 @@ export class Workloads extends React.Component { ) } -} \ No newline at end of file +} diff --git a/dashboard/client/components/app.tsx b/dashboard/client/components/app.tsx index 516c2d4903..220409ec12 100755 --- a/dashboard/client/components/app.tsx +++ b/dashboard/client/components/app.tsx @@ -32,6 +32,7 @@ import { PodLogsDialog } from "./+workloads-pods/pod-logs-dialog"; import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale-dialog"; import { CustomResources } from "./+custom-resources/custom-resources"; import { crdRoute } from "./+custom-resources"; +import { isAllowedResource } from "../api/rbac"; @observer class App extends React.Component { @@ -46,7 +47,7 @@ class App extends React.Component { }; render() { - const homeUrl = clusterURL(); + const homeUrl = (isAllowedResource(["events", "nodes", "pods"])) ? clusterURL() : workloadsURL(); return ( diff --git a/dashboard/client/components/item-object-list/item-list-layout.tsx b/dashboard/client/components/item-object-list/item-list-layout.tsx index 06cda88045..eb5dac108c 100644 --- a/dashboard/client/components/item-object-list/item-list-layout.tsx +++ b/dashboard/client/components/item-object-list/item-list-layout.tsx @@ -114,10 +114,15 @@ export class ItemListLayout extends React.Component { const { store, dependentStores, isClusterScoped } = this.props; const stores = [store, ...dependentStores]; if (!isClusterScoped) stores.push(namespaceStore); - await Promise.all(stores.map(store => store.loadAll())); - const subscriptions = stores.map(store => store.subscribe()); + try { + await Promise.all(stores.map(store => store.loadAll())); + const subscriptions = stores.map(store => store.subscribe()); + + subscriptions.forEach(dispose => dispose()); // unsubscribe all + } catch(error) { + console.log("catched", error) + } await when(() => this.isUnmounting); - subscriptions.forEach(dispose => dispose()); // unsubscribe all } componentWillUnmount() { diff --git a/dashboard/client/components/layout/sidebar.tsx b/dashboard/client/components/layout/sidebar.tsx index e005ecd87c..5cd77c6746 100644 --- a/dashboard/client/components/layout/sidebar.tsx +++ b/dashboard/client/components/layout/sidebar.tsx @@ -28,6 +28,7 @@ import { crdStore } from "../+custom-resources/crd.store"; import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resources"; import { CustomResources } from "../+custom-resources/custom-resources"; import { navigation } from "../../navigation"; +import { isAllowedResource } from "../../api/rbac" const SidebarContext = React.createContext({ pinned: false }); type SidebarContextValue = { @@ -43,7 +44,7 @@ interface Props { @observer export class Sidebar extends React.Component { async componentDidMount() { - if (!crdStore.isLoaded) crdStore.loadAll() + if (!crdStore.isLoaded && isAllowedResource('customresourcedefinitions')) crdStore.loadAll() } renderCustomResources() { @@ -71,7 +72,7 @@ export class Sidebar extends React.Component { render() { const { toggle, isPinned, className } = this.props; - const { isClusterAdmin, allowedResources } = configStore; + const { allowedResources } = configStore; const query = namespaceStore.getContextParams(); return ( @@ -91,19 +92,21 @@ export class Sidebar extends React.Component {
Cluster} icon={} /> Nodes} icon={} /> { /> { /> { /> { /> } text={Namespaces} /> } @@ -165,7 +173,7 @@ export class Sidebar extends React.Component { /> cluster.canI({ - resource: resource, - verb: "list" + apiResources.map(apiResource => cluster.canI({ + resource: apiResource.resource, + group: apiResource.group, + verb: "list", + namespace: namespaces[0] })) ) - return resources - .filter((resource, i) => resourceAccessStatuses[i]) + return apiResources + .filter((resource, i) => resourceAccessStatuses[i]).map(apiResource => apiResource.resource) } catch(error) { return [] } @@ -55,6 +77,7 @@ class ConfigRoute extends LensApi { public async routeConfig(request: LensApiRequest) { const { params, response, cluster} = request + const namespaces = await getAllowedNamespaces(cluster) const data = { clusterName: cluster.contextName, lensVersion: getAppVersion(), @@ -62,8 +85,8 @@ class ConfigRoute extends LensApi { kubeVersion: cluster.version, chartsEnabled: true, isClusterAdmin: cluster.isAdmin, - allowedResources: await getAllowedResources(cluster), - allowedNamespaces: await getAllowedNamespaces(cluster) + allowedResources: await getAllowedResources(cluster, namespaces), + allowedNamespaces: namespaces }; this.respondJson(response, data) From 9b680640c909766d0c20d66ce948154fa5492482 Mon Sep 17 00:00:00 2001 From: Jon Stelly <967068+jonstelly@users.noreply.github.com> Date: Fri, 5 Jun 2020 00:41:08 -0500 Subject: [PATCH 02/10] Add arch node selector for hybrid clusters (#400) Signed-off-by: Jon Stelly <967068+jonstelly@users.noreply.github.com> --- .../metrics/14-kube-state-metrics-deployment.yml.hb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/features/metrics/14-kube-state-metrics-deployment.yml.hb b/src/features/metrics/14-kube-state-metrics-deployment.yml.hb index 575eb41e95..cb13c8112d 100644 --- a/src/features/metrics/14-kube-state-metrics-deployment.yml.hb +++ b/src/features/metrics/14-kube-state-metrics-deployment.yml.hb @@ -23,11 +23,19 @@ spec: operator: In values: - linux + - key: kubernetes.io/arch + operator: In + values: + - amd64 - matchExpressions: - key: beta.kubernetes.io/os operator: In values: - linux + - key: beta.kubernetes.io/arch + operator: In + values: + - amd64 serviceAccountName: kube-state-metrics containers: - name: kube-state-metrics From ac03982c80ac009cf2747875c70056d2133695f0 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 5 Jun 2020 09:11:35 +0300 Subject: [PATCH 03/10] Change community slack urls (#405) Signed-off-by: Jari Kolehmainen --- dashboard/client/components/error-boundary/error-boundary.tsx | 4 ++-- src/main/menu.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboard/client/components/error-boundary/error-boundary.tsx b/dashboard/client/components/error-boundary/error-boundary.tsx index 378f381e36..c30d858e30 100644 --- a/dashboard/client/components/error-boundary/error-boundary.tsx +++ b/dashboard/client/components/error-boundary/error-boundary.tsx @@ -38,7 +38,7 @@ export class ErrorBoundary extends React.Component { render() { const { error, errorInfo } = this.state; if (error) { - const slackLink = Slack + const slackLink = Slack const githubLink = Github const pageUrl = location.href; return ( @@ -72,4 +72,4 @@ export class ErrorBoundary extends React.Component { } return this.props.children; } -} \ No newline at end of file +} diff --git a/src/main/menu.ts b/src/main/menu.ts index f834d33cbc..75635627bc 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -183,7 +183,7 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) { { label: 'Community Slack', click: async () => { - shell.openExternal('https://join.slack.com/t/kontenacommunity/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI'); + shell.openExternal('https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI'); }, }, { From ffb521071f922fdde3bc9e1dc84cdac7c9acf894 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 5 Jun 2020 09:51:41 +0300 Subject: [PATCH 04/10] Release v3.5.0-beta.1 (#403) Signed-off-by: Jari Kolehmainen --- package.json | 2 +- static/RELEASE_NOTES.md | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a11c1c6209..007bb9cca6 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "copyright": "© 2020, Lakend Labs, Inc.", "license": "MIT", "description": "Lens - The Kubernetes IDE", - "version": "3.4.0", + "version": "3.5.0-beta.1", "main": "main.ts", "config": { "bundledKubectlVersion": "1.17.4", diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index 01cbed5abe..1b688e944a 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,17 @@ 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 your might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 3.4.0 (current version) +## 3.5.0-beta.1 (current version) + +- Dynamic dashboard UI based on RBAC rules +- Show object reference for all objects +- Unify scrollbars/paddings +- Fix: add arch node selector for hybrid clusters +- Fix pod shell command on Windows +- Translation correction: transit to transmit +- Remove Kontena reference from Lens logo + +## 3.4.0 - Auto-detect Prometheus installation - Allow to select Prometheus query style From 5af12bdbbd4360019960bd31db2417b88537acf5 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 5 Jun 2020 22:47:30 +0300 Subject: [PATCH 05/10] Add download-bins target to make (#406) Signed-off-by: Jari Kolehmainen --- Makefile | 3 +++ README.md | 1 + 2 files changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 1e1d58364b..dfab88c228 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,9 @@ endif .PHONY: dev build test clean +download-bins: + yarn download:bins + dev: app-deps dashboard-deps yarn dev diff --git a/README.md b/README.md index 014c5275c0..cbf66a602d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Download a pre-built package from the [releases](https://github.com/lensapp/lens > Prerequisities: Nodejs v12, make, yarn +* `make download-bins` - downloads bundled binaries to dev environment * `make dev` - builds and starts the app * `make test` - run tests From af24da976b92834a5383c44b0ee89f7e278035e4 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Tue, 9 Jun 2020 13:16:39 +0300 Subject: [PATCH 06/10] Kill shell process by pid on Windows (#411) Signed-off-by: Lauri Nevala --- src/main/shell-session.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index bc2235ad36..7efa43b8bf 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -159,7 +159,7 @@ export class ShellSession extends EventEmitter { } protected exit(code = 1000) { - this.websocket.close(code) + if (this.websocket.readyState == this.websocket.OPEN) this.websocket.close(code) this.emit('exit') } @@ -179,12 +179,25 @@ export class ShellSession extends EventEmitter { protected exitProcessOnWebsocketClose() { this.websocket.on("close", () => { - if (this.shellProcess) { - this.shellProcess.kill(); - } + this.killShellProcess() }) } + protected killShellProcess(){ + if(this.running) { + // On Windows we need to kill the shell process by pid, since Lens won't respond after a while if using `this.shellProcess.kill()` + if (process.platform == "win32") { + try { + process.kill(this.shellProcess.pid) + } catch(e) { + return + } + } else { + this.shellProcess.kill() + } + } + } + protected sendResponse(msg: string) { this.websocket.send("1" + Buffer.from(msg).toString("base64")) } From deb4bf2f7b97617f4a036912d66ca54c45a2fea8 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 9 Jun 2020 13:18:24 +0300 Subject: [PATCH 07/10] Integration tests using spectron (#410) Signed-off-by: Jari Kolehmainen --- .azure-pipelines.yml | 13 +- Makefile | 12 + package.json | 11 +- spec/integration/helpers/utils.ts | 34 ++ spec/integration/specs/app_spec.ts | 56 +++ src/main/window-manager.ts | 5 +- src/renderer/components/AddClusterPage.vue | 1 + src/renderer/components/LandingPage.vue | 10 +- yarn.lock | 400 ++++++++++++++++++++- 9 files changed, 513 insertions(+), 29 deletions(-) create mode 100644 spec/integration/helpers/utils.ts create mode 100644 spec/integration/specs/app_spec.ts diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 05d7117e19..2a4751ff33 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -44,7 +44,6 @@ jobs: WIN_CSC_KEY_PASSWORD: $(WIN_CSC_KEY_PASSWORD) GH_TOKEN: $(GH_TOKEN) - job: macOS - condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" pool: vmImage: macOS-10.14 strategy: @@ -75,7 +74,10 @@ jobs: displayName: Lint - script: make test displayName: Run tests + - script: make integration-mac + displayName: Run integration tests - script: make build + condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" displayName: Build env: APPLEID: $(APPLEID) @@ -119,6 +121,15 @@ jobs: displayName: Lint - script: make test displayName: Run tests + - bash: | + sudo apt-get update + sudo apt-get install libgconf-2-4 conntrack -y + curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 + sudo install minikube-linux-amd64 /usr/local/bin/minikube + sudo minikube start --driver=none + displayName: Install integration test dependencies + - script: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux + displayName: Run integration tests - bash: | sudo chown root:root / sudo apt-get update && sudo apt-get install -y snapd diff --git a/Makefile b/Makefile index dfab88c228..6139afdfba 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,18 @@ dev: app-deps dashboard-deps test: test-app test-dashboard +integration-linux: + yarn build:linux + yarn integration + +integration-mac: + yarn build:mac + yarn integration + +integration-win: + yarn build:win + yarn integration + lint: yarn lint diff --git a/package.json b/package.json index 007bb9cca6..82906dce0b 100644 --- a/package.json +++ b/package.json @@ -109,16 +109,17 @@ "dev-dashboard": "cd dashboard && yarn dev", "dev-electron": "electron-webpack dev", "compile": "yarn download:bins && electron-webpack", - "build:linux": "yarn compile && electron-builder --linux --dir", - "build:mac": "yarn compile && electron-builder --mac --dir", - "build:win": "yarn compile && electron-builder --win --dir", + "build:linux": "yarn compile && electron-builder --linux --dir -c.productName=LensDev", + "build:mac": "yarn compile && electron-builder --mac --dir -c.productName=LensDev", + "build:win": "yarn compile && electron-builder --win --dir -c.productName=LensDev", "dist": "yarn compile && electron-builder -p onTag", "dist:win": "yarn compile && electron-builder -p onTag --x64 --ia32", "dist:dir": "yarn dist --dir -c.compression=store -c.mac.identity=null", "lint": "eslint $@ --ext js,ts,vue --max-warnings=0 src/", "lint-dashboard": "eslint $@ --ext js,ts,tsx --max-warnings=0 dashboard/client dashboard/server", "postinstall": "patch-package", - "test": "node_modules/.bin/jest", + "test": "node_modules/.bin/jest spec/src/", + "integration": "node_modules/.bin/jest spec/integration/", "download:bins": "concurrently \"yarn download:kubectl\" \"yarn download:helm\"", "download:kubectl": "yarn run ts-node build/download_kubectl.ts", "download:helm": "yarn run ts-node build/download_helm.ts" @@ -184,6 +185,7 @@ "@types/tempy": "0.1.0", "@types/universal-analytics": "^0.4.3", "@types/uuid": "^3.4.5", + "@types/webdriverio": "^4.13.0", "@typescript-eslint/eslint-plugin": "^2.7.0", "@typescript-eslint/parser": "^2.7.0", "bootstrap": "^4.3.1", @@ -211,6 +213,7 @@ "postinstall-postinstall": "^2.0.0", "prismjs": "^1.17.1", "sass-loader": "^8.0.0", + "spectron": "^8.0.0", "ts-jest": "^24.1.0", "ts-loader": "^6.0.4", "ts-node": "^8.4.1", diff --git a/spec/integration/helpers/utils.ts b/spec/integration/helpers/utils.ts new file mode 100644 index 0000000000..ee138be56e --- /dev/null +++ b/spec/integration/helpers/utils.ts @@ -0,0 +1,34 @@ +import { Application } from "spectron"; + +let appPath = "" +switch(process.platform) { +case "win32": + appPath = "./dist/win-unpacked/Lens.exe" + break +case "linux": + appPath = "./dist/linux-unpacked/kontena-lens" + break +case "darwin": + appPath = "./dist/mac/LensDev.app/Contents/MacOS/LensDev" + break +} + +export function setup() { + return new Application({ + // path to electron app + args: [], + path: appPath, + startTimeout: 30000, + waitTimeout: 30000, + }) +} + +export async function tearDown(app: Application) { + const pid = app.mainProcess.pid + await app.stop() + try { + process.kill(pid, 0); + } catch(e) { + return + } +} diff --git a/spec/integration/specs/app_spec.ts b/spec/integration/specs/app_spec.ts new file mode 100644 index 0000000000..414c3288bd --- /dev/null +++ b/spec/integration/specs/app_spec.ts @@ -0,0 +1,56 @@ +import { Application } from "spectron" +import * as util from "../helpers/utils" +import { spawnSync } from "child_process" +import { stat } from "fs" + +jest.setTimeout(20000) + +describe("app start", () => { + let app: Application + const clickWhatsNew = async (app: Application) => { + await app.client.waitUntilTextExists("h1", "What's new") + await app.client.click("button.btn-primary") + await app.client.waitUntilTextExists("h1", "Welcome") + } + + beforeEach(async () => { + app = util.setup() + await app.start() + const windowCount = await app.client.getWindowCount() + await app.client.windowByIndex(windowCount - 1) + await app.client.waitUntilWindowLoaded() + }, 20000) + + it('shows "whats new"', async () => { + await clickWhatsNew(app) + }) + + it('allows to add a cluster', async () => { + const status = spawnSync("minikube status", {shell: true}) + if (status.status !== 0) { + console.warn("minikube not running, skipping test") + return + } + await clickWhatsNew(app) + await app.client.click("a#add-cluster") + await app.client.waitUntilTextExists("legend", "Choose config:") + await app.client.selectByVisibleText("select#kubecontext-select", "minikube (new)") + await app.client.click("button.btn-primary") + await app.client.waitUntilTextExists("pre.auth-output", "Authentication proxy started") + let windowCount = await app.client.getWindowCount() + // wait for webview to appear on window count + while (windowCount == 1) { + windowCount = await app.client.getWindowCount() + } + await app.client.windowByIndex(windowCount - 1) + await app.client.waitUntilTextExists("span.link-text", "Cluster") + await app.client.click('a[href="/nodes"]') + await app.client.waitUntilTextExists("div.TableCell", "minikube") + }) + + afterEach(async () => { + if (app && app.isRunning()) { + return util.tearDown(app) + } + }) +}) diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index b585f69761..9baa5e1d4e 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -27,7 +27,10 @@ export class WindowManager { center: true, frame: false, resizable: false, - show: false + show: false, + webPreferences: { + nodeIntegration: true + } }) if (showSplash) { this.splashWindow.loadFile(path.join(__static, "/splash.html")) diff --git a/src/renderer/components/AddClusterPage.vue b/src/renderer/components/AddClusterPage.vue index 6f105a3950..74811a35b2 100644 --- a/src/renderer/components/AddClusterPage.vue +++ b/src/renderer/components/AddClusterPage.vue @@ -11,6 +11,7 @@ label="Choose config:" >
Status} className="status" labelsOnly> diff --git a/dashboard/client/components/+apps-releases/release-menu.tsx b/dashboard/client/components/+apps-releases/release-menu.tsx index 9884f72778..aaea69ade9 100644 --- a/dashboard/client/components/+apps-releases/release-menu.tsx +++ b/dashboard/client/components/+apps-releases/release-menu.tsx @@ -37,7 +37,6 @@ export class HelmReleaseMenu extends React.Component { const { release, toolbar } = this.props; if (!release) return; const hasRollback = release && release.getRevision() > 1; - const hasNewVersion = release.hasNewVersion(); return ( <> {hasRollback && ( @@ -46,12 +45,6 @@ export class HelmReleaseMenu extends React.Component { Rollback )} - {hasNewVersion && ( - - - Upgrade - - )} ) } diff --git a/dashboard/client/components/+apps-releases/releases.tsx b/dashboard/client/components/+apps-releases/releases.tsx index 558aee7d36..234a812543 100644 --- a/dashboard/client/components/+apps-releases/releases.tsx +++ b/dashboard/client/components/+apps-releases/releases.tsx @@ -5,9 +5,7 @@ import kebabCase from "lodash/kebabCase"; import { observer } from "mobx-react"; import { Trans } from "@lingui/macro"; import { RouteComponentProps } from "react-router"; -import { autobind, interval } from "../../utils"; import { releaseStore } from "./release.store"; -import { helmChartStore } from "../+apps-helm-charts/helm-chart.store"; import { IReleaseRouteParams, releaseURL } from "./release.route"; import { HelmRelease } from "../../api/endpoints/helm-releases.api"; import { ReleaseDetails } from "./release-details"; @@ -15,9 +13,7 @@ import { ReleaseRollbackDialog } from "./release-rollback-dialog"; import { navigation } from "../../navigation"; import { ItemListLayout } from "../item-object-list/item-list-layout"; import { HelmReleaseMenu } from "./release-menu"; -import { Icon } from "../icon"; import { secretsStore } from "../+config-secrets/secrets.store"; -import { when } from "mobx"; enum sortBy { name = "name", @@ -33,31 +29,16 @@ interface Props extends RouteComponentProps { @observer export class HelmReleases extends Component { - private versionsWatcher = interval(3600, this.checkVersions); componentDidMount() { // Watch for secrets associated with releases and react to their changes releaseStore.watch(); - this.versionsWatcher.start(); - when(() => releaseStore.isLoaded, this.checkVersions); } componentWillUnmount() { releaseStore.unwatch(); - this.versionsWatcher.stop(); } - // Check all available versions every 1 hour for installed releases. - // This required to show "upgrade" icon in the list and upgrade button in the details view. - @autobind() - checkVersions() { - const charts = releaseStore.items.map(release => release.getChart()); - return charts.reduce((promise, chartName) => { - const loadVersions = () => helmChartStore.getVersions(chartName, true); - return promise.then(loadVersions, loadVersions); - }, Promise.resolve({})) - }; - get selectedRelease() { const { match: { params: { name, namespace } } } = this.props; return releaseStore.items.find(release => { @@ -130,7 +111,6 @@ export class HelmReleases extends Component { ]} renderTableContents={(release: HelmRelease) => { const version = release.getVersion(); - const lastVersion = release.getLastVersion(); return [ release.getName(), release.getNs(), @@ -138,20 +118,6 @@ export class HelmReleases extends Component { release.getRevision(), <> {version} - {!lastVersion && ( - Checking update} - /> - )} - {release.hasNewVersion() && ( - New version: {lastVersion}} - /> - )} , release.appVersion, { title: release.getStatus(), className: kebabCase(release.getStatus()) }, From f9e669bc453982dccb779968c17eb69085b084a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2020 14:34:29 +0300 Subject: [PATCH 09/10] Bump websocket-extensions from 0.1.3 to 0.1.4 in /dashboard (#408) Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4. - [Release notes](https://github.com/faye/websocket-extensions-node/releases) - [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md) - [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dashboard/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboard/yarn.lock b/dashboard/yarn.lock index f0953833de..ee67aba073 100644 --- a/dashboard/yarn.lock +++ b/dashboard/yarn.lock @@ -10669,9 +10669,9 @@ websocket-driver@>=0.5.1: websocket-extensions ">=0.1.1" websocket-extensions@>=0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" - integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: version "1.0.5" From a36a279bceccee071d02f5aed998f98a300604e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2020 14:35:03 +0300 Subject: [PATCH 10/10] Bump websocket-extensions from 0.1.3 to 0.1.4 (#409) Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4. - [Release notes](https://github.com/faye/websocket-extensions-node/releases) - [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md) - [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a5079aa7c7..db1e213f97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11992,9 +11992,9 @@ websocket-driver@>=0.5.1: websocket-extensions ">=0.1.1" websocket-extensions@>=0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" - integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== wgxpath@~1.0.0: version "1.0.0"