From 8ac2f2178fa4067e48b666f8eeea591ed89d6866 Mon Sep 17 00:00:00 2001 From: Anthony Elizondo Date: Tue, 2 Apr 2019 11:05:44 -0400 Subject: [PATCH 001/103] typo fix --- client/src/views/dashboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/views/dashboard.js b/client/src/views/dashboard.js index 45336a6d..dad65074 100644 --- a/client/src/views/dashboard.js +++ b/client/src/views/dashboard.js @@ -34,7 +34,7 @@ export default class Dashboard extends Base { return (
this.setState({filter: x})} /> From 24b2d4793bfe4470575c6d282a96d390c7a8614f Mon Sep 17 00:00:00 2001 From: Eric Herbrandson Date: Tue, 2 Apr 2019 13:05:16 -0500 Subject: [PATCH 002/103] Lazily request docs (so we don't request them before we've auth-ed) --- client/src/services/docs.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/services/docs.js b/client/src/services/docs.js index 3c7d7db1..a6692cff 100644 --- a/client/src/services/docs.js +++ b/client/src/services/docs.js @@ -1,7 +1,7 @@ import Swagger from 'swagger-parser'; import apis from './api'; -const docsPromise = getDocs(); // Don't wait here. Just kick off the request +let docsPromise; async function getDocs() { const docs = await apis.swagger(); @@ -9,6 +9,10 @@ async function getDocs() { } export default async function getDocDefinitions(apiVersion, kind) { + if (!docsPromise) { + docsPromise = getDocs(); // Don't wait here. Just kick off the request + } + const {definitions} = await docsPromise; let [group, version] = apiVersion.split('/'); From 74ce499787e3e0cf9ce1ef05caabf0df1dde085e Mon Sep 17 00:00:00 2001 From: "Ivan.Kirianov" Date: Wed, 3 Apr 2019 23:37:42 +0300 Subject: [PATCH 003/103] Chart added --- helm/Chart.yaml | 8 ++++++++ helm/templates/deployment.yaml | 32 ++++++++++++++++++++++++++++++++ helm/templates/service.yaml | 17 +++++++++++++++++ helm/values.yaml | 20 ++++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 helm/Chart.yaml create mode 100644 helm/templates/deployment.yaml create mode 100644 helm/templates/service.yaml create mode 100644 helm/values.yaml diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 00000000..22149848 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +version: 0.0.1 +appVersion: 0.0.1 +name: k8dash +description: A Helm chart for Kubernetes K8Dash +maintainers: + - name: Ivan Kirianov + email: kiryanov.i@gmail.com \ No newline at end of file diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 00000000..75d09f19 --- /dev/null +++ b/helm/templates/deployment.yaml @@ -0,0 +1,32 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: "{{ .Chart.Name }}" + labels: + app: k8dash + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: k8dash + release: {{ .Release.Name }} + template: + metadata: + labels: + app: k8dash + release: {{ .Release.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.registry }}/{{ .Values.image.name }}:{{ .Values.image.tag }}" + pullPolicy: IfNotPresent + resources: + requests: + cpu: "25m" + memory: "100Mi" + limits: + cpu: "100m" + memory: "200Mi" diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 00000000..ff21dc28 --- /dev/null +++ b/helm/templates/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: k8dash + labels: + app: k8dash + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +spec: + ports: + - name: k8dash + port: 80 + targetport: 4654 + selector: + app: k8dash + release: {{ .Release.Name }} \ No newline at end of file diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 00000000..3cc6c897 --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,20 @@ +replicaCount: 1 +revisionHistoryLimit: 1 +image: + registry: herbrandson + name: k8dash + tag: latest + pullPolicy: IfNotPresent + resources: + requests: + cpu: 200m + memory: 300Mi +livenessProbe: + initialDelaySeconds: 30 + periodSeconds: 30 +service: + name: Node + type: NodePort + servicePort: 4654 + + From dcfd2f11b759e6eea94061289635aa622e01997b Mon Sep 17 00:00:00 2001 From: Eric Herbrandson Date: Wed, 3 Apr 2019 22:17:08 -0500 Subject: [PATCH 004/103] Refactoring. Better charts. Fixed a bug parsing cpu usage. Using a better api for validating tokens. Added some server logging at startup to help track down issues. --- client/src/components/chart.js | 4 +- client/src/components/cpuChart.js | 9 ++- client/src/components/podsPanel.js | 108 ++++++++++++++++++----------- client/src/components/ramChart.js | 11 ++- client/src/services/api.js | 14 ++-- client/src/utils/unitHelpers.js | 1 + client/src/views/auth.js | 17 ++++- client/src/views/node.js | 37 ++++++++++ client/src/views/nodes.js | 68 ++++++++++++++---- server/index.js | 25 ++++++- 10 files changed, 218 insertions(+), 76 deletions(-) diff --git a/client/src/components/chart.js b/client/src/components/chart.js index 048f5b2a..3d35ae22 100644 --- a/client/src/components/chart.js +++ b/client/src/components/chart.js @@ -10,7 +10,9 @@ const options = { showLabel: false, }; -export default function Chart({used = 0, usedSuffix, available = 0, availableSuffix, pending = 0, decimals = 1}) { // eslint-disable-line max-len +export default function Chart(props) { + const {used = 0, usedSuffix, available = 0, availableSuffix, pending = 0, decimals = 1} = props; + const fixedUsed = _.round(used, decimals); const fixedPending = _.round(pending, decimals); const fixedAvailable = _.round(available, decimals); diff --git a/client/src/components/cpuChart.js b/client/src/components/cpuChart.js index b46f70a9..59234932 100644 --- a/client/src/components/cpuChart.js +++ b/client/src/components/cpuChart.js @@ -6,15 +6,20 @@ import {parseCpu, TO_ONE_CPU} from '../utils/unitHelpers'; export default function CpuChart({items, metrics}) { const totals = getPodCpuTotals(items, metrics); + const decimals = totals && totals.used > 10 ? 1 : 2; return (
{totals ? ( - + ) : ( )} -
Requested Cpu Use
+
Cores Used
); } diff --git a/client/src/components/podsPanel.js b/client/src/components/podsPanel.js index e94490bf..1a1bf904 100644 --- a/client/src/components/podsPanel.js +++ b/client/src/components/podsPanel.js @@ -7,6 +7,24 @@ import Sorter from './sorter'; import {parseRam, unparseRam, parseCpu, unparseCpu} from '../utils/unitHelpers'; export default class PodsPanel extends Base { + constructor(props) { + super(props); + this.sortByCpu = this.sortByCpu.bind(this); + this.sortByRam = this.sortByRam.bind(this); + } + + sortByCpu(item) { + const actual = getCpuUsage(item, this.props.metrics); + const requested = getCpuRequest(item); + return actual / requested; + } + + sortByRam(item) { + const actual = getRamUsage(item, this.props.metrics); + const requested = getRamRequest(item); + return actual / requested; + } + render() { const {items, metrics, sort, filter, skipNamespace, skipNodeName} = this.props; const col = 8 + !skipNamespace + !skipNodeName; @@ -23,19 +41,21 @@ export default class PodsPanel extends Base { Status Containers Restarts - - {/* TODO: support sorting by metrics */} - Cpu -
- actual vs. request -
+ + Cpu +
+ actual vs. request +
+
- Ram -
- actual vs. request -
+ + Ram +
+ actual vs. request +
+
@@ -71,47 +91,37 @@ function getRestartCount({status}) { } function Cpu({item, metrics}) { - if (!item || !metrics) return null; - - const {containers} = metrics[item.metadata.name] || {}; + const containers = getContainerMetrics(item, metrics); if (!containers) return null; - const actual = _.sumBy(containers, x => parseCpu(x.usage.cpu)); - const podContainers = item.spec.containers.filter(x => x.resources && x.resources.requests); - const requested = _.sumBy(podContainers, x => parseCpu(x.resources.requests.cpu)); + const actual = getCpuUsage(item, metrics); + const requested = getCpuRequest(item); + return ; +} - const isWarning = requested && (actual > requested); - const actualCpu = unparseCpu(actual); - const requestedCpu = unparseCpu(requested); - const percent = requested ? _.round(actual / requested * 100, 1) : 100; +function getCpuUsage(item, metrics) { + const containers = getContainerMetrics(item, metrics); + return _.sumBy(containers, x => parseCpu(x.usage.cpu)); +} - return ( -
-
{requested ? `${percent}%` : 'N/A'}
-
- {actualCpu.value}{actualCpu.unit} - of - {requestedCpu.value}{requestedCpu.unit} -
-
- ); +function getCpuRequest(item) { + const podContainers = _.filter(item.spec.containers, x => x.resources && x.resources.requests); + return _.sumBy(podContainers, x => parseCpu(x.resources.requests.cpu)); } function Ram({item, metrics}) { - if (!item || !metrics) return null; - - const {containers} = metrics[item.metadata.name] || {}; + const containers = getContainerMetrics(item, metrics); if (!containers) return null; - const actual = _.sumBy(containers, x => parseRam(x.usage.memory)); - const requested = _.sumBy(item.spec.containers, (x) => { - if (!x.resources.requests) return 0; - return parseRam(x.resources.requests.memory); - }); + const actual = getRamUsage(item, metrics); + const requested = getRamRequest(item); + return ; +} +function Chart({actual, requested, unparser}) { const isWarning = requested && (actual > requested); - const actualRam = unparseRam(actual); - const requestedRam = unparseRam(requested); + const actualRam = unparser(actual); + const requestedRam = unparser(requested); const percent = requested ? _.round(actual / requested * 100, 1) : 100; return ( @@ -125,3 +135,21 @@ function Ram({item, metrics}) {
); } + +function getRamUsage(item, metrics) { + const containers = getContainerMetrics(item, metrics); + return _.sumBy(containers, x => parseRam(x.usage.memory)); +} + +function getRamRequest(item) { + return _.sumBy(item.spec.containers, (x) => { + if (!x.resources.requests) return 0; + return parseRam(x.resources.requests.memory); + }); +} + +function getContainerMetrics(item, metrics) { + if (!item || !metrics) return null; + const metric = metrics[item.metadata.name] || {}; + return metric.containers; +} diff --git a/client/src/components/ramChart.js b/client/src/components/ramChart.js index ee8e08f1..4b46c08e 100644 --- a/client/src/components/ramChart.js +++ b/client/src/components/ramChart.js @@ -6,15 +6,22 @@ import {parseRam, TO_GB} from '../utils/unitHelpers'; export default function RamChart({items, metrics}) { const totals = getPodRamTotals(items, metrics); + const decimals = totals && totals.used > 10 ? 1 : 2; return (
{totals ? ( - + ) : ( )} -
Requested Ram Use
+
Ram Used
); } diff --git a/client/src/services/api.js b/client/src/services/api.js index a2d7bbf8..e01079e4 100644 --- a/client/src/services/api.js +++ b/client/src/services/api.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import {request, stream, streamResult, streamResults, getToken} from './apiProxy'; +import {request, stream, streamResult, streamResults} from './apiProxy'; import log from '../utils/log'; const JSON_HEADERS = {Accept: 'application/json', 'Content-Type': 'application/json'}; @@ -40,15 +40,9 @@ const apis = { roleBinding: apiFactoryWithNamespace('/apis/rbac.authorization.k8s.io/v1', 'rolebindings'), }; -function testAuth() { - const token = getToken(); - const body = { - apiVerstion: 'authentication.k8s.io/v1', - kind: 'TokenReview', - spec: {token}, - }; - - return post('/apis/authentication.k8s.io/v1/tokenreviews', body, false); +async function testAuth() { + const spec = {resourceAttributes: {}}; + return post('apis/authorization.k8s.io/v1/selfsubjectaccessreviews', {spec}, false); } async function apply(body) { diff --git a/client/src/utils/unitHelpers.js b/client/src/utils/unitHelpers.js index b5194621..22565edb 100644 --- a/client/src/utils/unitHelpers.js +++ b/client/src/utils/unitHelpers.js @@ -52,6 +52,7 @@ export function parseCpu(value) { const number = parseInt(value, 10); if (value.endsWith('n')) return number; + if (value.endsWith('u')) return number * 1000; if (value.endsWith('m')) return number * 1000 * 1000; return number * 1000 * 1000 * 1000; } diff --git a/client/src/views/auth.js b/client/src/views/auth.js index 317a65ae..86a939d0 100644 --- a/client/src/views/auth.js +++ b/client/src/views/auth.js @@ -14,6 +14,10 @@ const LOGIN_MESSAGE = 'Invalid credentials'; const ERROR_MESSAGE = 'Error occured attempting to login'; export default class Auth extends Base { + state = { + token: '', + }; + async componentDidMount() { const url = new URL(window.location.href); const code = url.searchParams.get('code'); @@ -46,10 +50,10 @@ export default class Auth extends Base { className='auth_input' placeholder='Enter your auth token here...' spellCheck='false' - defaultValue={token} + value={token} onChange={x => this.setState({token: x.target.value})} /> - @@ -95,7 +99,14 @@ async function oidcLogin(code, returnedState) { async function login(token) { try { setToken(token); - await api.testAuth(); + + const result = await api.testAuth(); + log.info('Auth', result); + + if (!result || !result.status || !result.status.allowed) { + throw new Error('Invalid login'); + } + window.location.reload(); } catch (err) { log.error('Login Failed', err); diff --git a/client/src/views/node.js b/client/src/views/node.js index 3566d7e2..3ad46d0c 100644 --- a/client/src/views/node.js +++ b/client/src/views/node.js @@ -1,5 +1,6 @@ import _ from 'lodash'; import React from 'react'; +import moment from 'moment'; import api from '../services/api'; import Base from '../components/base'; import Chart from '../components/chart'; @@ -11,6 +12,7 @@ import PodsPanel from '../components/podsPanel'; import {defaultSortInfo} from '../components/sorter'; import {parseCpu, parseRam, TO_GB, TO_ONE_M_CPU} from '../utils/unitHelpers'; import PodStatusChart from '../components/podStatusChart'; +import Field from '../components/field'; export default class Node extends Base { state = { @@ -51,10 +53,45 @@ export default class Node extends Base { {!item ? : (
+ + + + + + +
)} +
+ {!item ? : ( + + + + + + + + + + + + {_.map(item.status.conditions, x => ( + + + + + + + + ))} + +
ConditionStatusTransitionReasonMessage
{x.type}{x.status}{moment(x.lastTransitionTime).fromNow()}{x.reason}{x.message}
+ )} +
+ + Ready Cpu Ram - - {/* TODO: support sorting by cpu/ram - Cpu - Ram */} + {/* Cpu + Ram */} @@ -81,6 +79,23 @@ export default class Nodes extends Base { } } +// TODO: get sortying by cpu/ram working +// function sortByCpu(item) { +// const used = getCpuUsed(item, filteredMetrics); +// if (used == null) return null; + +// const available = getCpuAvailable(item); +// return used / available; +// } + +// function sortByRam(item) { +// const used = getRamUsed(item, filteredMetrics); +// if (used == null) return null; + +// const available = getRamAvailable(item); +// return used / available; +// } + function getReadyStatus({status}) { if (!status.conditions) return null; @@ -117,31 +132,54 @@ function RamTotalsChart({items, metrics}) { } function CpuChart({item, metrics}) { - if (!item || !metrics) return null; + const used = getCpuUsed(item, metrics); + if (used == null) return null; + + const available = getCpuAvailable(item); + return ; +} - const {usage} = metrics[item.metadata.name] || {}; +function getCpuUsed(item, metrics) { + const usage = getUsage(item, metrics); if (!usage) return null; - const totalUsed = parseCpu(usage.cpu) / 1000000; - const totalAvailable = parseCpu(item.status.capacity.cpu) / 1000000; - const percent = _.round(totalUsed / totalAvailable * 100, 1); + return parseCpu(usage.cpu) / 1000000; +} - return ({`${percent}%`}); +function getCpuAvailable(item) { + return parseCpu(item.status.capacity.cpu) / 1000000; } function RamChart({item, metrics}) { - if (!item || !metrics) return null; + const used = getRamUsed(item, metrics); + if (used == null) return null; + + const available = getRamAvailable(item); + return ; +} - const {usage} = metrics[item.metadata.name] || {}; +function getRamUsed(item, metrics) { + const usage = getUsage(item, metrics); if (!usage) return null; - const totalUsed = parseRam(usage.memory); - const totalAvailable = parseRam(item.status.capacity.memory); - const percent = _.round(totalUsed / totalAvailable * 100, 1); + return parseRam(usage.memory); +} +function getRamAvailable(item) { + return parseRam(item.status.capacity.memory); +} + +function Percent({used, available}) { + const percent = _.round(used / available * 100, 1); return ({`${percent}%`}); } +function getUsage(item, metrics) { + if (!item || !metrics) return null; + const result = metrics[item.metadata.name] || {}; + return result && result.usage; +} + function getNodeMetrics(nodes, metrics) { if (!nodes || !metrics) return null; diff --git a/server/index.js b/server/index.js index 28e8729b..58f189b6 100644 --- a/server/index.js +++ b/server/index.js @@ -27,9 +27,11 @@ const target = kc.getCurrentCluster().server; const agent = new https.Agent({ca: opts.ca}); const proxySettings = {target, agent, secure: false, ws: true, onError}; +logClusterInfo(); + const app = express(); app.disable('x-powered-by'); // for security reasons, best not to tell attackers too much about our backend -app.use(cors()); +app.use(cors()); // TODO: remove in production builds app.use(logging); app.use('/', express.static('public')); @@ -79,11 +81,11 @@ function handleErrors(err, req, res, next) { next(); } -async function getOidcEndpoint(redirectUri) { // TODO: gotta pass this in from +async function getOidcEndpoint() { if (!OIDC_URL) return; const provider = await getOidcProvider(); - return provider.authorizationUrl({redirect_uri: redirectUri, scope: OIDC_SCOPES}); + return provider.authorizationUrl({scope: OIDC_SCOPES}); } async function oidcAuthenticate(code, redirectUri) { @@ -96,3 +98,20 @@ async function getOidcProvider() { const issuer = await Issuer.discover(OIDC_URL); return new issuer.Client({client_id: OIDC_CLIENT_ID, client_secret: OIDC_SECRET}); } + +async function logClusterInfo() { + try { + const versionClient = kc.makeApiClient(k8s.VersionApi); + const versionResponse = await versionClient.getCode(); + const versionJson = JSON.stringify(versionResponse.body, null, 4); + console.log('Version Info: ', versionJson); + + const apisClient = kc.makeApiClient(k8s.ApisApi); + const apisResponse = await apisClient.getAPIVersions(); + const apis = apisResponse.body.groups.map(x => x.preferredVersion.groupVersion).sort(); + const apisJson = JSON.stringify(apis, null, 4); + console.log('Available APIs:', apisJson); + } catch (err) { + console.error('Error getting cluster info', err); + } +} From 3642a26561580385d5d1f5b7049460aac5909ae7 Mon Sep 17 00:00:00 2001 From: Steven Gantz Date: Sat, 6 Apr 2019 09:14:20 -0400 Subject: [PATCH 005/103] nodeport yaml provided and readme updated for faster up-and-running option --- README.md | 9 ++++++++ kubernetes-k8dash-nodeport.yaml | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 kubernetes-k8dash-nodeport.yaml diff --git a/README.md b/README.md index af3c506b..83f51fa7 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,16 @@ kubectl apply -f https://raw.githubusercontent.com/herbrandson/k8dash/master/kub ``` +## Running k8dash with NodePort +If you do not have an ingress server setup, you can utilize a NodePort service as configured in the [kubernetes-k8dash-nodeport.yaml](https://raw.githubusercontent.com/herbrandson/k8dash/master/kubernetes-k8dash-nodeport.yaml). This is ideal when creating a single node master, or if you want to get up and running as fast as possible. +This will map the k8dash port 4654 to a randomly selected port on the running node. The assigned port can be found using +``` +$ kubectl get svc --namespace=kube-system + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +k8dash NodePort 10.107.107.62 4654:32565/TCP 1m +``` ## Metrics K8dash relies heavily on [metrics-server](https://github.com/kubernetes-incubator/metrics-server) to display real time cluster metrics. It is strongly recommended to have metrics-server installed to get the best experiance from k8dash. diff --git a/kubernetes-k8dash-nodeport.yaml b/kubernetes-k8dash-nodeport.yaml new file mode 100644 index 00000000..31b4499a --- /dev/null +++ b/kubernetes-k8dash-nodeport.yaml @@ -0,0 +1,40 @@ +kind: Deployment +apiVersion: apps/v1beta2 +metadata: + name: k8dash + namespace: kube-system +spec: + replicas: 1 + selector: + matchLabels: + k8s-app: k8dash + template: + metadata: + labels: + k8s-app: k8dash + spec: + containers: + - name: k8dash + image: herbrandson/k8dash:latest + ports: + - containerPort: 4654 + livenessProbe: + httpGet: + scheme: HTTP + path: / + port: 4654 + initialDelaySeconds: 30 + timeoutSeconds: 30 + +--- +kind: Service +apiVersion: v1 +metadata: + name: k8dash + namespace: kube-system +spec: + type: NodePort + ports: + - port: 4654 + selector: + k8s-app: k8dash From 24a6001c4ce6ae6b35eb50ee086cc762b232c927 Mon Sep 17 00:00:00 2001 From: Eric Herbrandson Date: Sat, 6 Apr 2019 14:35:27 -0500 Subject: [PATCH 006/103] Adding sorting by cpu/ram support to nodes view. Some better error handling and messaging. Improved logging on the server. Better proxy handling on server (changeOrigin) --- client/src/components/nodesPanel.js | 117 ++++++++++++++++++++++++++++ client/src/services/apiProxy.js | 13 +++- client/src/views/logs.js | 6 +- client/src/views/nodes.js | 113 +++------------------------ server/index.js | 34 +++++--- 5 files changed, 163 insertions(+), 120 deletions(-) create mode 100644 client/src/components/nodesPanel.js diff --git a/client/src/components/nodesPanel.js b/client/src/components/nodesPanel.js new file mode 100644 index 00000000..d054e45f --- /dev/null +++ b/client/src/components/nodesPanel.js @@ -0,0 +1,117 @@ +import _ from 'lodash'; +import React from 'react'; +import Base from './base'; +import {MetadataHeaders, MetadataColumns, TableBody, objectMap} from './listViewHelpers'; +import Sorter from './sorter'; +import {parseCpu, parseRam} from '../utils/unitHelpers'; + +export default class NodesPanel extends Base { + constructor(props) { + super(props); + this.sortByCpu = this.sortByCpu.bind(this); + this.sortByRam = this.sortByRam.bind(this); + } + + sortByCpu(item) { + const used = getCpuUsed(item, this.props.metrics); + if (used == null) return null; + + const available = getCpuAvailable(item); + return used / available; + } + + sortByRam(item) { + const used = getRamUsed(item, this.props.metrics); + if (used == null) return null; + + const available = getRamAvailable(item); + return used / available; + } + + render() { + const {items, metrics, sort, filter} = this.props; + + return ( +
+ + + + + + + + + + + + ( + + + + + + + + )} /> +
LabelsReadyCpuRam
{objectMap(x.metadata.labels)}{getReadyStatus(x)}
+
+ ); + } +} + +function getReadyStatus({status}) { + if (!status.conditions) return null; + + const ready = status.conditions.find(y => y.type === 'Ready'); + return ready && ready.status; +} + +function CpuPercent({item, metrics}) { + const used = getCpuUsed(item, metrics); + if (used == null) return null; + + const available = getCpuAvailable(item); + return ; +} + +function getCpuUsed(item, metrics) { + const usage = getUsage(item, metrics); + if (!usage) return null; + + return parseCpu(usage.cpu) / 1000000; +} + +function getCpuAvailable(item) { + return parseCpu(item.status.capacity.cpu) / 1000000; +} + +function RamPercent({item, metrics}) { + const used = getRamUsed(item, metrics); + if (used == null) return null; + + const available = getRamAvailable(item); + return ; +} + +function getRamUsed(item, metrics) { + const usage = getUsage(item, metrics); + if (!usage) return null; + + return parseRam(usage.memory); +} + +function getRamAvailable(item) { + return parseRam(item.status.capacity.memory); +} + +function Percent({used, available}) { + const percent = _.round(used / available * 100, 1); + const className = percent >= 85 ? 'contentPanel_warn' : undefined; + return ({`${percent}%`}); +} + +function getUsage(item, metrics) { + if (!item || !metrics) return null; + const result = metrics[item.metadata.name] || {}; + return result && result.usage; +} diff --git a/client/src/services/apiProxy.js b/client/src/services/apiProxy.js index 2084267f..78bb0f4f 100644 --- a/client/src/services/apiProxy.js +++ b/client/src/services/apiProxy.js @@ -2,7 +2,8 @@ import log from '../utils/log'; const {host, href, hash, search} = window.location; const nonHashedUrl = href.replace(hash, '').replace(search, ''); -const BASE_HTTP_URL = host === 'localhost:4653' ? 'http://localhost:4654' : nonHashedUrl; +const isDev = process.env.NODE_ENV !== 'production'; +const BASE_HTTP_URL = isDev && host === 'localhost:4653' ? 'http://localhost:4654' : nonHashedUrl; const BASE_WS_URL = BASE_HTTP_URL.replace('http', 'ws'); export function getToken() { @@ -47,7 +48,15 @@ export async function request(path, params, autoLogoutOnAuthError = true) { logout(); } - const error = new Error(`Api request error: ${statusText}`); + let message = `Api request error: ${statusText}`; + try { + const json = await response.json(); + message += ` - ${json.message}`; + } catch (err) { + log.error('Unable to parse error json', {err}); + } + + const error = new Error(message); error.status = status; throw error; } diff --git a/client/src/views/logs.js b/client/src/views/logs.js index 9d8d2339..53fb996d 100644 --- a/client/src/views/logs.js +++ b/client/src/views/logs.js @@ -20,8 +20,8 @@ export default class Logs extends Base { // From the lodash docs: "If leading and trailing options are true, func is invoked // on the trailing edge of the timeout only if the debounced function is invoked more // than once during the wait timeout." - const options = {leading: true, trailing: true, maxWait: 500}; - this.debouncedRefreshLogs = _.debounce(this.refreshLogs.bind(this), 500, options); + const options = {leading: true, trailing: true, maxWait: 1000}; + this.debouncedRefreshLogs = _.debounce(this.refreshLogs.bind(this), 1000, options); } componentDidMount() { @@ -66,7 +66,7 @@ export default class Logs extends Base { } refreshLogs(logs) { - const items = logs.slice(-2000); + const items = logs.slice(-1000); this.setState({items}); } diff --git a/client/src/views/nodes.js b/client/src/views/nodes.js index b9824d00..f0091831 100644 --- a/client/src/views/nodes.js +++ b/client/src/views/nodes.js @@ -2,14 +2,14 @@ import _ from 'lodash'; import React from 'react'; import Base from '../components/base'; import Filter from '../components/filter'; -import Chart from '../components/chart'; +import {defaultSortInfo} from '../components/sorter'; import NodeStatusChart from '../components/nodeStatusChart'; -import LoadingChart from '../components/loadingChart'; -import {MetadataHeaders, MetadataColumns, TableBody, objectMap} from '../components/listViewHelpers'; -import Sorter, {defaultSortInfo} from '../components/sorter'; import api from '../services/api'; -import {parseCpu, parseRam, TO_GB, TO_ONE_CPU} from '../utils/unitHelpers'; import test from '../utils/filterHelper'; +import NodesPanel from '../components/nodesPanel'; +import Chart from '../components/chart'; +import LoadingChart from '../components/loadingChart'; +import {parseCpu, parseRam, TO_GB, TO_ONE_CPU} from '../utils/unitHelpers'; export default class Nodes extends Base { state = { @@ -49,60 +49,16 @@ export default class Nodes extends Base { -
- - - - - - - - - {/* - */} - - - - ( - - - - - - - - )} /> -
LabelsReadyCpuRamCpuRam
{objectMap(x.metadata.labels)}{getReadyStatus(x)}
-
+ ); } } -// TODO: get sortying by cpu/ram working -// function sortByCpu(item) { -// const used = getCpuUsed(item, filteredMetrics); -// if (used == null) return null; - -// const available = getCpuAvailable(item); -// return used / available; -// } - -// function sortByRam(item) { -// const used = getRamUsed(item, filteredMetrics); -// if (used == null) return null; - -// const available = getRamAvailable(item); -// return used / available; -// } - -function getReadyStatus({status}) { - if (!status.conditions) return null; - - const ready = status.conditions.find(y => y.type === 'Ready'); - return ready && ready.status; -} - function CpuTotalsChart({items, metrics}) { const totals = getNodeCpuTotals(items, metrics); return ( @@ -131,55 +87,6 @@ function RamTotalsChart({items, metrics}) { ); } -function CpuChart({item, metrics}) { - const used = getCpuUsed(item, metrics); - if (used == null) return null; - - const available = getCpuAvailable(item); - return ; -} - -function getCpuUsed(item, metrics) { - const usage = getUsage(item, metrics); - if (!usage) return null; - - return parseCpu(usage.cpu) / 1000000; -} - -function getCpuAvailable(item) { - return parseCpu(item.status.capacity.cpu) / 1000000; -} - -function RamChart({item, metrics}) { - const used = getRamUsed(item, metrics); - if (used == null) return null; - - const available = getRamAvailable(item); - return ; -} - -function getRamUsed(item, metrics) { - const usage = getUsage(item, metrics); - if (!usage) return null; - - return parseRam(usage.memory); -} - -function getRamAvailable(item) { - return parseRam(item.status.capacity.memory); -} - -function Percent({used, available}) { - const percent = _.round(used / available * 100, 1); - return ({`${percent}%`}); -} - -function getUsage(item, metrics) { - if (!item || !metrics) return null; - const result = metrics[item.metadata.name] || {}; - return result && result.usage; -} - function getNodeMetrics(nodes, metrics) { if (!nodes || !metrics) return null; diff --git a/server/index.js b/server/index.js index 58f189b6..5a38c246 100644 --- a/server/index.js +++ b/server/index.js @@ -7,15 +7,14 @@ const proxy = require('http-proxy-middleware'); const toString = require('stream-to-string'); const {Issuer} = require('openid-client'); +const NODE_ENV = process.env.NODE_ENV; const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID; const OIDC_SECRET = process.env.OIDC_SECRET; const OIDC_URL = process.env.OIDC_URL; const OIDC_SCOPES = process.env.OIDC_SCOPES || 'openid email'; console.log('OIDC_URL: ', OIDC_URL || 'None'); -process.on('uncaughtException', err => { - console.error('Uncaught exception', err); -}); +process.on('uncaughtException', err => console.error('Uncaught exception', err)); const kc = new k8s.KubeConfig(); kc.loadFromDefault(); @@ -25,27 +24,31 @@ kc.applyToRequest(opts); const target = kc.getCurrentCluster().server; const agent = new https.Agent({ca: opts.ca}); -const proxySettings = {target, agent, secure: false, ws: true, onError}; - -logClusterInfo(); +const proxySettings = { + target, + agent, + ws: true, + secure: false, + changeOrigin: true, + logLevel: 'debug', + onError, +}; const app = express(); app.disable('x-powered-by'); // for security reasons, best not to tell attackers too much about our backend -app.use(cors()); // TODO: remove in production builds - app.use(logging); +if (NODE_ENV !== 'production') app.use(cors()); app.use('/', express.static('public')); app.get('/oidc', getOidc); app.post('/oidc', postOidc); app.use('/*', proxy(proxySettings)); app.use(handleErrors); -const server = http.createServer(app); -server.listen(4654); +http.createServer(app).listen(4654); console.log('Server started'); function logging(req, res, next) { - console.log(req.method, req.url); + res.once('finish', () => console.log(req.method, req.url, res.statusCode)); next(); } @@ -99,6 +102,7 @@ async function getOidcProvider() { return new issuer.Client({client_id: OIDC_CLIENT_ID, client_secret: OIDC_SECRET}); } +logClusterInfo(); async function logClusterInfo() { try { const versionClient = kc.makeApiClient(k8s.VersionApi); @@ -110,7 +114,13 @@ async function logClusterInfo() { const apisResponse = await apisClient.getAPIVersions(); const apis = apisResponse.body.groups.map(x => x.preferredVersion.groupVersion).sort(); const apisJson = JSON.stringify(apis, null, 4); - console.log('Available APIs:', apisJson); + console.log('Available APIs: ', apisJson); + + const authClient = kc.makeApiClient(k8s.Authorization_v1Api); + const spec = {resourceAttributes: {}}; + const authResponse = await authClient.createSelfSubjectAccessReview({spec}); + const authJson = JSON.stringify(authResponse.body, null, 4); + console.log('Auth Response: ', authJson); } catch (err) { console.error('Error getting cluster info', err); } From ef55ce722a27a00fba13e57b5846dda1a8149b00 Mon Sep 17 00:00:00 2001 From: Eric Herbrandson Date: Sat, 6 Apr 2019 15:09:50 -0500 Subject: [PATCH 007/103] Adding ingress.yaml to helm chart --- helm/Chart.yaml | 4 +++- helm/templates/ingress.yaml | 28 ++++++++++++++++++++++++++++ helm/values.yaml | 22 ++++++++++++++++++++-- 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 helm/templates/ingress.yaml diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 22149848..31b56ecf 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -5,4 +5,6 @@ name: k8dash description: A Helm chart for Kubernetes K8Dash maintainers: - name: Ivan Kirianov - email: kiryanov.i@gmail.com \ No newline at end of file + email: kiryanov.i@gmail.com + - name: Eric Herbrandson + email: eric@herbrandson.com \ No newline at end of file diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml new file mode 100644 index 00000000..ed06feba --- /dev/null +++ b/helm/templates/ingress.yaml @@ -0,0 +1,28 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: k8dash + labels: + app: k8dash + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +{{- if .Values.ingress.annotations }} + annotations: +{{ toYaml .Values.ingress.annotations | indent 4 }} +{{- end }} +spec: + rules: + - host: {{ .Values.ingress.host }} + http: + paths: + - path: / + backend: + serviceName: k8dash + servicePort: 80 + {{- if .Values.ingress.tls }} + tls: +{{ toYaml .Values.ingress.tls | indent 4 }} + {{- end -}} +{{- end -}} \ No newline at end of file diff --git a/helm/values.yaml b/helm/values.yaml index 3cc6c897..5edfb4d7 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -1,20 +1,38 @@ replicaCount: 1 revisionHistoryLimit: 1 + image: registry: herbrandson name: k8dash tag: latest - pullPolicy: IfNotPresent + pullPolicy: IfNotPresent resources: requests: cpu: 200m memory: 300Mi + livenessProbe: initialDelaySeconds: 30 periodSeconds: 30 + service: name: Node type: NodePort servicePort: 4654 - + +ingress: + enabled: false + + ## Kubernetes Dashboard Ingress annotations + # annotations: + # kubernetes.io/ingress.class: nginx + + ## Kubernetes Dashboard Ingress hostnames + # host: kubernetes-dashboard.domain.com + + ## Kubernetes Dashboard Ingress TLS configuration + # tls: + # - secretName: kubernetes-dashboard-tls + # hosts: + # - kubernetes-dashboard.domain.com From 42d5c2664623d20991c3aac966e8e433014b9463 Mon Sep 17 00:00:00 2001 From: frohikey Date: Sun, 7 Apr 2019 18:29:10 +0200 Subject: [PATCH 008/103] Fix parsing disk space / ram --- client/src/utils/unitHelpers.js | 63 +++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/client/src/utils/unitHelpers.js b/client/src/utils/unitHelpers.js index 22565edb..dbb7fd6f 100644 --- a/client/src/utils/unitHelpers.js +++ b/client/src/utils/unitHelpers.js @@ -1,6 +1,7 @@ import _ from 'lodash'; const RAM_TYPES = ['Bi', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei']; +const UNITS = ['B', 'K', 'M', 'G', 'T', 'P', 'E']; export const TO_GB = 1024 * 1024 * 1024; export const TO_ONE_M_CPU = 1000000; @@ -9,29 +10,55 @@ export const TO_ONE_CPU = 1000000000; export function parseDiskSpace(value) { if (!value) return 0; - const number = parseInt(value, 10); - if (value.endsWith('Ki')) return number * 1024; - if (value.endsWith('Mi')) return number * 1024 * 1024; - if (value.endsWith('Gi')) return number * 1024 * 1024 * 1024; - if (value.endsWith('Ti')) return number * 1024 * 1024 * 1024 * 1024; - if (value.endsWith('Pi')) return number * 1024 * 1024 * 1024 * 1024 * 1024; - if (value.endsWith('Ei')) return number * 1024 * 1024 * 1024 * 1024 * 1024 * 1024; - - return number; + const groups = value.match(/(\d+)([BKMGTPEe])?(i)?(\d+)?/); + const number = parseInt(groups[1], 10); + + // number ex. 1000 + if (groups[2] === undefined) { + return number; + } + + // number with exponent ex. 1e3 + if (groups[4] !== undefined) { + return number * (10 ** parseInt(groups[4], 10)); + } + + const unitIndex = _.indexOf(UNITS, groups[2]); + + // Unit + i ex. 1Ki + if (groups[3] !== undefined) { + return number * (1024 ** unitIndex); + } + + // Unit ex. 1K + return number * (1000 ** unitIndex); } export function parseRam(value) { if (!value) return 0; - const number = parseInt(value, 10); - if (value.endsWith('Ki')) return number * 1024; - if (value.endsWith('Mi')) return number * 1024 * 1024; - if (value.endsWith('Gi')) return number * 1024 * 1024 * 1024; - if (value.endsWith('Ti')) return number * 1024 * 1024 * 1024 * 1024; - if (value.endsWith('Pi')) return number * 1024 * 1024 * 1024 * 1024 * 1024; - if (value.endsWith('Ei')) return number * 1024 * 1024 * 1024 * 1024 * 1024 * 1024; - - return number; + const groups = value.match(/(\d+)([BKMGTPEe])?(i)?(\d+)?/); + const number = parseInt(groups[1], 10); + + // number ex. 1000 + if (groups[2] === undefined) { + return number; + } + + // number with exponent ex. 1e3 + if (groups[4] !== undefined) { + return number * (10 ** parseInt(groups[4], 10)); + } + + const unitIndex = _.indexOf(UNITS, groups[2]); + + // Unit + i ex. 1Ki + if (groups[3] !== undefined) { + return number * (1024 ** unitIndex); + } + + // Unit ex. 1K + return number * (1000 ** unitIndex); } export function unparseRam(value) { From 7c2f6d79b56bf93b13dddbc0de22f0c850b4ea11 Mon Sep 17 00:00:00 2001 From: Eric Herbrandson Date: Sun, 7 Apr 2019 14:31:32 -0500 Subject: [PATCH 009/103] Switching router to solve issue w/ routing to urls with colons in the item name --- client/package-lock.json | 28 +++++++-- client/package.json | 2 +- client/src/components/menu.js | 2 +- client/src/components/nodesPanel.js | 2 +- client/src/components/podsPanel.js | 2 +- client/src/components/replicaSetsPanel.js | 2 +- client/src/components/titleBar.js | 2 +- client/src/router.js | 66 +++++++++++----------- client/src/views/clusterRoleBinding.js | 4 +- client/src/views/configMaps.js | 2 +- client/src/views/ingresses.js | 2 +- client/src/views/namespaces.js | 2 +- client/src/views/persistentVolume.js | 4 +- client/src/views/persistentVolumeClaim.js | 4 +- client/src/views/persistentVolumeClaims.js | 2 +- client/src/views/persistentVolumes.js | 2 +- client/src/views/pod.js | 4 +- client/src/views/replicaSet.js | 2 +- client/src/views/role.js | 2 +- client/src/views/roleBinding.js | 4 +- client/src/views/roleBindings.js | 4 +- client/src/views/roles.js | 4 +- client/src/views/secrets.js | 2 +- client/src/views/serviceAccount.js | 2 +- client/src/views/serviceAccounts.js | 2 +- client/src/views/services.js | 2 +- client/src/views/storageClasses.js | 2 +- client/src/views/workloads.js | 2 +- 28 files changed, 89 insertions(+), 71 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index f3995dea..159f7efd 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -4979,11 +4979,6 @@ } } }, - "director": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/director/-/director-1.2.8.tgz", - "integrity": "sha1-xtm03YkOmv9TZRg/6cyOc5lM8tU=" - }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -11267,6 +11262,29 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" }, + "page": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/page/-/page-1.11.4.tgz", + "integrity": "sha512-8JMZzcE5W4qk+/DtmogN57cI+Yscy7xTYCpfSO7s3Tx6LjZuAfHFQY1+cKIAy60NaXdzVD6nOc3objaVbE0HJg==", + "requires": { + "path-to-regexp": "~1.2.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "path-to-regexp": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.2.1.tgz", + "integrity": "sha1-szcFwUAjTYc8hyHHuf2LVB7Tr/k=", + "requires": { + "isarray": "0.0.1" + } + } + } + }, "pako": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", diff --git a/client/package.json b/client/package.json index b0620762..fe628463 100644 --- a/client/package.json +++ b/client/package.json @@ -4,10 +4,10 @@ "private": true, "dependencies": { "chartist": "^0.10.1", - "director": "^1.2.8", "lodash": "^4.17.11", "moment": "^2.24.0", "node-sass": "^4.11.0", + "page": "^1.11.4", "react": "^16.8.2", "react-chartist": "^0.13.3", "react-dom": "^16.8.2", diff --git a/client/src/components/menu.js b/client/src/components/menu.js index 578c3814..678e0bdb 100644 --- a/client/src/components/menu.js +++ b/client/src/components/menu.js @@ -78,7 +78,7 @@ function MenuItem(item) { const {path, title, resource} = item; return ( - + {title} diff --git a/client/src/components/nodesPanel.js b/client/src/components/nodesPanel.js index d054e45f..778ff672 100644 --- a/client/src/components/nodesPanel.js +++ b/client/src/components/nodesPanel.js @@ -46,7 +46,7 @@ export default class NodesPanel extends Base { ( - + {objectMap(x.metadata.labels)} {getReadyStatus(x)} diff --git a/client/src/components/podsPanel.js b/client/src/components/podsPanel.js index 1a1bf904..db847d8b 100644 --- a/client/src/components/podsPanel.js +++ b/client/src/components/podsPanel.js @@ -66,7 +66,7 @@ export default class PodsPanel extends Base { item={x} isError={x.status.phase !== 'Running'} includeNamespace={!skipNamespace} - href={`#/pod/${x.metadata.namespace}/${x.metadata.name}`} + href={`#!pod/${x.metadata.namespace}/${x.metadata.name}`} /> {!skipNodeName && {x.spec.nodeName}} {x.status.phase} diff --git a/client/src/components/replicaSetsPanel.js b/client/src/components/replicaSetsPanel.js index 89b37780..0e504cce 100644 --- a/client/src/components/replicaSetsPanel.js +++ b/client/src/components/replicaSetsPanel.js @@ -49,7 +49,7 @@ export default class ReplicaSetsPanel extends Base { {x.status.observedGeneration} {x.spec.replicas} / {x.status.replicas} diff --git a/client/src/components/titleBar.js b/client/src/components/titleBar.js index 50c27639..b06629dd 100644 --- a/client/src/components/titleBar.js +++ b/client/src/components/titleBar.js @@ -4,7 +4,7 @@ import LogoSvg from '../art/logoSvg'; const TitleBar = () => (
- + diff --git a/client/src/router.js b/client/src/router.js index b41569b1..7b25810f 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -1,5 +1,5 @@ import React from 'react'; -import {Router} from 'director/build/director'; +import page from 'page'; import {hasToken} from './services/apiProxy'; import Account from './views/account'; import Auth from './views/auth'; @@ -45,51 +45,52 @@ import StorageClasses from './views/storageClasses'; import Workloads from './views/workloads'; const handlers = []; -const router = Router().configure({notfound: onNotFound}); +let path = ''; registerRoute('/', () => ); registerRoute('/account', () => ); -registerRoute('/clusterrole/:name', name => ); -registerRoute('/clusterrolebinding/:name', name => ); +registerRoute('/clusterrole/:name', params => ); +registerRoute('/clusterrolebinding/:name', params => ); registerRoute('/configmap', () => ); -registerRoute('/configmap/:namespace/:name', (namespace, name) => ); +registerRoute('/configmap/:namespace/:name', params => ); registerRoute('/ingress', () => ); -registerRoute('/ingress/:namespace/:name', (namespace, name) => ); +registerRoute('/ingress/:namespace/:name', params => ); registerRoute('/namespace', () => ); registerRoute('/namespace/:namespace', namespace => ); registerRoute('/node', () => ); -registerRoute('/node/:name', name => ); +registerRoute('/node/:name', params => ); registerRoute('/persistentvolume', () => ); -registerRoute('/persistentvolume/:name', name => ); +registerRoute('/persistentvolume/:name', params => ); registerRoute('/persistentvolumeclaim', () => ); -registerRoute('/persistentvolumeclaim/:namespace/:name', (namespace, name) => ); +registerRoute('/persistentvolumeclaim/:namespace/:name', params => ); registerRoute('/pod', () => ); -registerRoute('/pod/:namespace/:name', (namespace, name) => ); -registerRoute('/pod/:namespace/:name/exec', (namespace, name) => ); -registerRoute('/pod/:namespace/:name/logs', (namespace, name) => ); +registerRoute('/pod/:namespace/:name', params => ); +registerRoute('/pod/:namespace/:name/exec', params => ); +registerRoute('/pod/:namespace/:name/logs', params => ); registerRoute('/replicaset', () => ); -registerRoute('/replicaset/:namespace/:name', (namespace, name) => ); +registerRoute('/replicaset/:namespace/:name', params => ); registerRoute('/role', () => ); -registerRoute('/role/:namespace/:name', (namespace, name) => ); +registerRoute('/role/:namespace/:name', params => ); registerRoute('/rolebinding', () => ); -registerRoute('/rolebinding/:namespace/:name', (namespace, name) => ); +registerRoute('/rolebinding/:namespace/:name', params => ); registerRoute('/secret', () => ); -registerRoute('/secret/:namespace/:name', (namespace, name) => ); +registerRoute('/secret/:namespace/:name', params => ); registerRoute('/service', () => ); -registerRoute('/service/:namespace/:name', (namespace, name) => ); +registerRoute('/service/:namespace/:name', params => ); registerRoute('/serviceaccount', () => ); -registerRoute('/serviceaccount/:namespace/:name', (namespace, name) => ); +registerRoute('/serviceaccount/:namespace/:name', params => ); registerRoute('/storageclass', () => ); -registerRoute('/storageclass/:name', name => ); +registerRoute('/storageclass/:name', params => ); registerRoute('/workload', () => ); -registerRoute('/workload/cronjob/:namespace/:name', (namespace, name) => ); -registerRoute('/workload/daemonset/:namespace/:name', (namespace, name) => ); -registerRoute('/workload/deployment/:namespace/:name', (namespace, name) => ); -registerRoute('/workload/job/:namespace/:name', (namespace, name) => ); -registerRoute('/workload/statefulset/:namespace/:name', (namespace, name) => ); +registerRoute('/workload/cronjob/:namespace/:name', params => ); +registerRoute('/workload/daemonset/:namespace/:name', params => ); +registerRoute('/workload/deployment/:namespace/:name', params => ); +registerRoute('/workload/job/:namespace/:name', params => ); +registerRoute('/workload/statefulset/:namespace/:name', params => ); +registerRoute('*', () => ); export function initRouter() { - router.init(['/']); + page({hashbang: true}); } export function registerHandler(handler) { @@ -97,19 +98,22 @@ export function registerHandler(handler) { } export function getRootPath() { - return router.getRoute()[0]; + return path; } export function goTo(name) { - window.location = `#/${name}`; + window.location = `#!${name}`; } function registerRoute(route, factory) { - router.on(route, (...args) => { + page(route, (context) => { + const [current] = context.path.split('/').filter(x => !!x); + path = current || ''; + if (!hasToken()) { onRoute(); } else { - const result = factory(...args); + const result = factory(context.params); onRoute(result); } }); @@ -118,7 +122,3 @@ function registerRoute(route, factory) { function onRoute(value) { handlers.forEach(x => x(value)); } - -function onNotFound() { - onRoute(); -} diff --git a/client/src/views/clusterRoleBinding.js b/client/src/views/clusterRoleBinding.js index 0e774398..350fe812 100644 --- a/client/src/views/clusterRoleBinding.js +++ b/client/src/views/clusterRoleBinding.js @@ -101,9 +101,9 @@ export default class ClusterRoleBinding extends Base { } function getRoleHref({name, kind, namespace}) { - return kind === 'ClusterRole' ? `#/clusterrole/${name}` : `#/role/${namespace}/${name}`; + return kind === 'ClusterRole' ? `#!clusterrole/${name}` : `#!role/${namespace}/${name}`; } function getSubjectHref({name, namespace}) { - return namespace ? `#/serviceaccount/${namespace}/${name}` : `#/serviceaccount/${name}`; + return namespace ? `#!serviceaccount/${namespace}/${name}` : `#!serviceaccount/${name}`; } diff --git a/client/src/views/configMaps.js b/client/src/views/configMaps.js index e3beb0c1..cb3adc39 100644 --- a/client/src/views/configMaps.js +++ b/client/src/views/configMaps.js @@ -46,7 +46,7 @@ export default class ConfigMaps extends Base { )} /> diff --git a/client/src/views/ingresses.js b/client/src/views/ingresses.js index a6a5f778..e451bafc 100644 --- a/client/src/views/ingresses.js +++ b/client/src/views/ingresses.js @@ -49,7 +49,7 @@ export default class Ingresses extends Base { {getHosts(x, ' • ')} {getPaths(x, ' • ')} diff --git a/client/src/views/namespaces.js b/client/src/views/namespaces.js index 70306ba7..c573b9a3 100644 --- a/client/src/views/namespaces.js +++ b/client/src/views/namespaces.js @@ -45,7 +45,7 @@ export default class Namespaces extends Base { {x.status.phase} diff --git a/client/src/views/persistentVolume.js b/client/src/views/persistentVolume.js index d59c7f1c..9c9722b5 100644 --- a/client/src/views/persistentVolume.js +++ b/client/src/views/persistentVolume.js @@ -44,13 +44,13 @@ export default class PersistentVolume extends Base { - + {item.spec.storageClassName} {item.spec.claimRef && ( - + {`${item.spec.claimRef.namespace}/${item.spec.claimRef.name}`} )} diff --git a/client/src/views/persistentVolumeClaim.js b/client/src/views/persistentVolumeClaim.js index 8e9b1a2e..f790d34a 100644 --- a/client/src/views/persistentVolumeClaim.js +++ b/client/src/views/persistentVolumeClaim.js @@ -44,12 +44,12 @@ export default class PersistentVolumeClaim extends Base { - + {item.spec.storageClassName} - + {item.spec.volumeName} diff --git a/client/src/views/persistentVolumeClaims.js b/client/src/views/persistentVolumeClaims.js index b9de17dd..b76945c4 100644 --- a/client/src/views/persistentVolumeClaims.js +++ b/client/src/views/persistentVolumeClaims.js @@ -62,7 +62,7 @@ export default class PersistentVolumeClaims extends Base { {x.status.phase} {x.spec.storageClassName} diff --git a/client/src/views/persistentVolumes.js b/client/src/views/persistentVolumes.js index bc56b134..70c3609a 100644 --- a/client/src/views/persistentVolumes.js +++ b/client/src/views/persistentVolumes.js @@ -49,7 +49,7 @@ export default class PersistentVolumes extends Base { {x.status.phase} {x.spec.capacity && x.spec.capacity.storage} diff --git a/client/src/views/pod.js b/client/src/views/pod.js index 2881df88..4548f5fe 100644 --- a/client/src/views/pod.js +++ b/client/src/views/pod.js @@ -85,7 +85,7 @@ export default class Pod extends Base { {_.map(item.metadata.ownerReferences, x => ( @@ -109,7 +109,7 @@ export default class Pod extends Base { ))} - + {item.spec.nodeName} diff --git a/client/src/views/replicaSet.js b/client/src/views/replicaSet.js index 7b3ce5b1..f66a8af8 100644 --- a/client/src/views/replicaSet.js +++ b/client/src/views/replicaSet.js @@ -81,7 +81,7 @@ export default class ReplicaSet extends Base { {_.map(item.metadata.ownerReferences, x => ( diff --git a/client/src/views/role.js b/client/src/views/role.js index 97951f2c..81d46be8 100644 --- a/client/src/views/role.js +++ b/client/src/views/role.js @@ -75,5 +75,5 @@ export default class Role extends Base { } function toDiv(item) { - return (
{item}
); + return (
{item}
); } diff --git a/client/src/views/roleBinding.js b/client/src/views/roleBinding.js index e73ea1df..810638d6 100644 --- a/client/src/views/roleBinding.js +++ b/client/src/views/roleBinding.js @@ -92,9 +92,9 @@ export default class RoleBinding extends Base { function getRoleHref(namespace, item) { const {name, kind} = item.roleRef; - return kind === 'ClusterRole' ? `#/role/${name}` : `#/role/${namespace}/${name}`; + return kind === 'ClusterRole' ? `#!role/${name}` : `#!role/${namespace}/${name}`; } function getSubjectHref({name, namespace}) { - return namespace ? `#/serviceaccount/${namespace}/${name}` : `#/serviceaccount/${name}`; + return namespace ? `#!serviceaccount/${namespace}/${name}` : `#!serviceaccount/${name}`; } diff --git a/client/src/views/roleBindings.js b/client/src/views/roleBindings.js index b150ce0d..30a023e7 100644 --- a/client/src/views/roleBindings.js +++ b/client/src/views/roleBindings.js @@ -51,8 +51,8 @@ export default class RoleBindings extends Base { item={x} includeNamespace={true} href={x.kind === 'ClusterRoleBinding' - ? `#/clusterrolebinding/${x.metadata.name}` - : `#/rolebinding/${x.metadata.namespace}/${x.metadata.name}`} + ? `#!clusterrolebinding/${x.metadata.name}` + : `#!rolebinding/${x.metadata.namespace}/${x.metadata.name}`} /> )} /> diff --git a/client/src/views/roles.js b/client/src/views/roles.js index 019940ee..6847e5ce 100644 --- a/client/src/views/roles.js +++ b/client/src/views/roles.js @@ -51,8 +51,8 @@ export default class Roles extends Base { item={x} includeNamespace={true} href={x.kind === 'ClusterRole' - ? `#/clusterrole/${x.metadata.name}` - : `#/role/${x.metadata.namespace}/${x.metadata.name}`} + ? `#!clusterrole/${x.metadata.name}` + : `#!role/${x.metadata.namespace}/${x.metadata.name}`} /> )} /> diff --git a/client/src/views/secrets.js b/client/src/views/secrets.js index d646c789..a5684751 100644 --- a/client/src/views/secrets.js +++ b/client/src/views/secrets.js @@ -47,7 +47,7 @@ export default class Secrets extends Base { {x.type} diff --git a/client/src/views/serviceAccount.js b/client/src/views/serviceAccount.js index 3a199cc2..44bcac6c 100644 --- a/client/src/views/serviceAccount.js +++ b/client/src/views/serviceAccount.js @@ -46,7 +46,7 @@ export default class ServiceAccount extends Base { {_.map(item.secrets, x => ( ))} diff --git a/client/src/views/serviceAccounts.js b/client/src/views/serviceAccounts.js index 647075a7..e4a46af9 100644 --- a/client/src/views/serviceAccounts.js +++ b/client/src/views/serviceAccounts.js @@ -46,7 +46,7 @@ export default class ServiceAccounts extends Base { )} /> diff --git a/client/src/views/services.js b/client/src/views/services.js index 913349a9..d4392e7c 100644 --- a/client/src/views/services.js +++ b/client/src/views/services.js @@ -46,7 +46,7 @@ export default class Services extends Base { )} /> diff --git a/client/src/views/storageClasses.js b/client/src/views/storageClasses.js index 1efe6aaa..73dba26e 100644 --- a/client/src/views/storageClasses.js +++ b/client/src/views/storageClasses.js @@ -44,7 +44,7 @@ export default class StorageClasses extends Base { {x.reclaimPolicy} {x.volumeBindingMode} diff --git a/client/src/views/workloads.js b/client/src/views/workloads.js index 5074bbe2..89286939 100644 --- a/client/src/views/workloads.js +++ b/client/src/views/workloads.js @@ -71,7 +71,7 @@ export default class Workloads extends Base { From 9287513d42c3cba4ceaea6b6c7339eb6bd15a8bb Mon Sep 17 00:00:00 2001 From: frohikey Date: Sun, 7 Apr 2019 21:52:54 +0200 Subject: [PATCH 010/103] Helper function parseUnitsOfBytes --- client/src/utils/unitHelpers.js | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/client/src/utils/unitHelpers.js b/client/src/utils/unitHelpers.js index dbb7fd6f..73d0e302 100644 --- a/client/src/utils/unitHelpers.js +++ b/client/src/utils/unitHelpers.js @@ -8,33 +8,14 @@ export const TO_ONE_M_CPU = 1000000; export const TO_ONE_CPU = 1000000000; export function parseDiskSpace(value) { - if (!value) return 0; - - const groups = value.match(/(\d+)([BKMGTPEe])?(i)?(\d+)?/); - const number = parseInt(groups[1], 10); - - // number ex. 1000 - if (groups[2] === undefined) { - return number; - } - - // number with exponent ex. 1e3 - if (groups[4] !== undefined) { - return number * (10 ** parseInt(groups[4], 10)); - } - - const unitIndex = _.indexOf(UNITS, groups[2]); - - // Unit + i ex. 1Ki - if (groups[3] !== undefined) { - return number * (1024 ** unitIndex); - } - - // Unit ex. 1K - return number * (1000 ** unitIndex); + return parseUnitsOfBytes(value); } export function parseRam(value) { + return parseUnitsOfBytes(value); +} + +function parseUnitsOfBytes(value) { if (!value) return 0; const groups = value.match(/(\d+)([BKMGTPEe])?(i)?(\d+)?/); From d59174ffb1bbd12a3eda1c9df70a30c56737e5cf Mon Sep 17 00:00:00 2001 From: Eric Herbrandson Date: Sun, 7 Apr 2019 15:36:36 -0500 Subject: [PATCH 011/103] Some styling efforts --- client/src/components/button.scss | 8 ++- client/src/components/deleteButton.js | 2 +- client/src/components/doc.scss | 2 + client/src/components/itemHeader.js | 7 ++- client/src/components/menu.js | 2 +- client/src/components/saveButton.js | 2 +- client/src/components/scaleButton.js | 4 +- client/src/components/secretValue.js | 6 +-- client/src/components/secretValue.scss | 7 +++ client/src/scss/elements/header.scss | 61 ++++++++++++++++++++++- client/src/scss/elements/textbox.scss | 2 +- client/src/views/account.js | 4 +- client/src/views/clusterRole.js | 2 +- client/src/views/clusterRoleBinding.js | 2 +- client/src/views/configMap.js | 2 +- client/src/views/cronJob.js | 2 +- client/src/views/daemonSet.js | 2 +- client/src/views/deployment.js | 2 +- client/src/views/editorModal.js | 4 +- client/src/views/ingress.js | 2 +- client/src/views/job.js | 2 +- client/src/views/namespace.js | 2 +- client/src/views/persistentVolume.js | 2 +- client/src/views/persistentVolumeClaim.js | 2 +- client/src/views/pod.js | 6 +-- client/src/views/replicaSet.js | 2 +- client/src/views/role.js | 2 +- client/src/views/roleBinding.js | 2 +- client/src/views/secret.js | 2 +- client/src/views/service.js | 2 +- client/src/views/serviceAccount.js | 2 +- client/src/views/statefulSet.js | 2 +- client/src/views/storageClass.js | 2 +- 33 files changed, 111 insertions(+), 44 deletions(-) diff --git a/client/src/components/button.scss b/client/src/components/button.scss index 87abe47d..ca67bd0e 100644 --- a/client/src/components/button.scss +++ b/client/src/components/button.scss @@ -16,11 +16,13 @@ } .button_negative, .button_negative:visited { + @extend .button; background: $color-error; color: $color-white; } .button_clear, .button_clear:visited { + @extend .button; background: none; padding: 0; @@ -29,12 +31,14 @@ } .button:disabled, .button_negative:disabled { + border: none; background: $color-lightest; color: $color-white; pointer-events: none; } .button_clear:disabled { + @extend .button:disabled; background: none; color: $color-lightest; @@ -43,8 +47,8 @@ } } -.button_working { - background: $color-lightest; +.button_headerAction { + @extend .button_clear } .button_label { diff --git a/client/src/components/deleteButton.js b/client/src/components/deleteButton.js index 39f20d96..a59c51b8 100644 --- a/client/src/components/deleteButton.js +++ b/client/src/components/deleteButton.js @@ -7,7 +7,7 @@ import DeleteSvg from '../art/deleteSvg'; export default class DeleteButton extends Base { render() { return ( - diff --git a/client/src/components/doc.scss b/client/src/components/doc.scss index a10e8a74..7e21b5cb 100644 --- a/client/src/components/doc.scss +++ b/client/src/components/doc.scss @@ -8,6 +8,7 @@ ul.doc_group { .doc_item { margin: 5px; list-style-type: none; + cursor: pointer; } .doc_itemType { @@ -19,6 +20,7 @@ ul.doc_group { margin-left: 10px; color: $color-dark; font-size: 8pt; + cursor: default; } svg.doc_arrow { diff --git a/client/src/components/itemHeader.js b/client/src/components/itemHeader.js index 3a495bd6..5044efc1 100644 --- a/client/src/components/itemHeader.js +++ b/client/src/components/itemHeader.js @@ -3,15 +3,14 @@ import Base from './base'; export default class ItemHeader extends Base { render() { - const {title, item, children} = this.props; + const {title, ready, children} = this.props; return ( -