Merge branch 'upstream-master' into fix-no-masters

# Conflicts:
#	.gitattributes
This commit is contained in:
Matthew Casperson
2019-03-20 07:03:02 +10:00
18 changed files with 252 additions and 130 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
*
!Pipfile*
!pip*
!kube_ops_view

4
.gitattributes vendored
View File

@@ -1,4 +1,6 @@
* text eol=lf * text eol=lf
*.woff2 binary
*.png binary *.png binary
*.jpg binary
*.ico binary *.ico binary
*.woff2 binary

View File

@@ -1,4 +1,4 @@
FROM python:3.7-alpine3.8 FROM python:3.7-alpine3.9
WORKDIR / WORKDIR /
@@ -13,7 +13,7 @@ RUN /pipenv-install.py && \
apk del python3-dev gcc musl-dev zlib-dev libffi-dev openssl-dev && \ apk del python3-dev gcc musl-dev zlib-dev libffi-dev openssl-dev && \
rm -rf /var/cache/apk/* /root/.cache /tmp/* rm -rf /var/cache/apk/* /root/.cache /tmp/*
FROM python:3.7-alpine3.8 FROM python:3.7-alpine3.9
WORKDIR / WORKDIR /

View File

@@ -16,8 +16,8 @@ test:
pipenv run coverage report pipenv run coverage report
appjs: appjs:
docker run $(TTYFLAGS) -u $$(id -u) -v $$(pwd):/workdir -w /workdir/app -e NPM_CONFIG_CACHE=/tmp node:11.4-alpine npm install docker run $(TTYFLAGS) -u $$(id -u) -v $$(pwd):/workdir -w /workdir/app -e NPM_CONFIG_CACHE=/tmp node:11.10-alpine npm install
docker run $(TTYFLAGS) -u $$(id -u) -v $$(pwd):/workdir -w /workdir/app -e NPM_CONFIG_CACHE=/tmp node:11.4-alpine npm run build docker run $(TTYFLAGS) -u $$(id -u) -v $$(pwd):/workdir -w /workdir/app -e NPM_CONFIG_CACHE=/tmp node:11.10-alpine npm run build
docker: appjs docker: appjs
docker build --build-arg "VERSION=$(VERSION)" -t "$(IMAGE):$(TAG)" . docker build --build-arg "VERSION=$(VERSION)" -t "$(IMAGE):$(TAG)" .

View File

@@ -9,6 +9,10 @@ Kubernetes Operational View
.. image:: https://readthedocs.org/projects/kubernetes-operational-view/badge/?version=latest .. image:: https://readthedocs.org/projects/kubernetes-operational-view/badge/?version=latest
:target: http://kubernetes-operational-view.readthedocs.io/en/latest/?badge=latest :target: http://kubernetes-operational-view.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status :alt: Documentation Status
.. image:: https://img.shields.io/docker/pulls/hjacobs/kube-ops-view.svg
:target: https://hub.docker.com/r/hjacobs/kube-ops-view
:alt: Docker pulls
**This project is in a very early state, but it might already be useful.** **This project is in a very early state, but it might already be useful.**
@@ -131,6 +135,8 @@ The following environment variables are supported:
Optional OAuth 2 authorization endpoint URL for protecting the UI. Optional OAuth 2 authorization endpoint URL for protecting the UI.
``ACCESS_TOKEN_URL`` ``ACCESS_TOKEN_URL``
Optional token endpoint URL for the OAuth 2 Authorization Code Grant flow. Optional token endpoint URL for the OAuth 2 Authorization Code Grant flow.
``SCOPE``
Optional scope specifies level of access that the application is requesting.
``CLUSTERS`` ``CLUSTERS``
Comma separated list of Kubernetes API server URLs. It defaults to ``http://localhost:8001/`` (default endpoint of ``kubectl proxy``). Comma separated list of Kubernetes API server URLs. It defaults to ``http://localhost:8001/`` (default endpoint of ``kubectl proxy``).
``CLUSTER_REGISTRY_URL`` ``CLUSTER_REGISTRY_URL``

127
app/package-lock.json generated
View File

@@ -1277,6 +1277,11 @@
"integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=",
"dev": true "dev": true
}, },
"bit-twiddle": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz",
"integrity": "sha1-DGwfq+KyPRcXPZpht7cJPrnhdp4="
},
"bluebird": { "bluebird": {
"version": "3.5.1", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
@@ -2512,9 +2517,9 @@
} }
}, },
"earcut": { "earcut": {
"version": "2.1.4", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.1.4.tgz", "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.1.5.tgz",
"integrity": "sha512-ttRjmPD5oaTtXOoxhFp9aZvMB14kBjapYaiBuzBB1elOgSLU9P2Ev86G2OClBg+uspUXERsIzXKpUWweH2K4Xg==" "integrity": "sha1-gpKAqaOg9f7gUp8KR8Pk7/CbIeQ="
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.90", "version": "1.3.90",
@@ -3059,6 +3064,11 @@
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
"dev": true "dev": true
}, },
"eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo="
},
"events": { "events": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
@@ -3387,7 +3397,8 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@@ -3408,12 +3419,14 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -3428,17 +3441,20 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@@ -3555,7 +3571,8 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@@ -3567,6 +3584,7 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@@ -3581,6 +3599,7 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@@ -3588,12 +3607,14 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.2.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.1",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@@ -3612,6 +3633,7 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@@ -3692,7 +3714,8 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@@ -3704,6 +3727,7 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@@ -3789,7 +3813,8 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.1", "version": "5.1.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@@ -3825,6 +3850,7 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@@ -3844,6 +3870,7 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@@ -3887,12 +3914,14 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.2", "version": "3.0.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
} }
} }
}, },
@@ -4370,6 +4399,11 @@
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true "dev": true
}, },
"ismobilejs": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-0.5.1.tgz",
"integrity": "sha1-Dj+CXinjL4StXdu2Dp4EqJQEZIg="
},
"js-tokens": { "js-tokens": {
"version": "3.0.2", "version": "3.0.2",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
@@ -5010,8 +5044,7 @@
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
"dev": true
}, },
"object-copy": { "object-copy": {
"version": "0.1.0", "version": "0.1.0",
@@ -5234,6 +5267,11 @@
"pbkdf2": "^3.0.3" "pbkdf2": "^3.0.3"
} }
}, },
"parse-uri": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/parse-uri/-/parse-uri-1.0.0.tgz",
"integrity": "sha1-KHLcwi8aeXrN4Vg9igrClVLdrCA="
},
"pascalcase": { "pascalcase": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
@@ -5323,45 +5361,18 @@
"integrity": "sha1-i0tcQzsx5Bm8N53FZc4bg1qRs3I=" "integrity": "sha1-i0tcQzsx5Bm8N53FZc4bg1qRs3I="
}, },
"pixi.js": { "pixi.js": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-4.8.3.tgz", "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-4.8.6.tgz",
"integrity": "sha512-FhczbzbxCXURJWUVL+/rWDFlW7IGuSQfCCD7NDWM+WRs4dQCc9VCBrUWVq87MApt2XqEx8mky5O0Na1wal1MDg==", "integrity": "sha1-3hUFOhdnQTOkj9P+ATikAZove/E=",
"requires": { "requires": {
"bit-twiddle": "^1.0.2", "bit-twiddle": "^1.0.2",
"earcut": "^2.1.3", "earcut": "^2.1.4",
"eventemitter3": "^2.0.0", "eventemitter3": "^2.0.0",
"ismobilejs": "^0.4.0", "ismobilejs": "^0.5.1",
"object-assign": "^4.0.1", "object-assign": "^4.0.1",
"pixi-gl-core": "^1.1.4", "pixi-gl-core": "^1.1.4",
"remove-array-items": "^1.0.0", "remove-array-items": "^1.0.0",
"resource-loader": "^2.1.1" "resource-loader": "^2.2.3"
},
"dependencies": {
"bit-twiddle": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz",
"integrity": "sha1-DGwfq+KyPRcXPZpht7cJPrnhdp4="
},
"eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo="
},
"ismobilejs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-0.4.1.tgz",
"integrity": "sha1-Gl8SbHD+05yT2jgPpiy65XI+fcI="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"remove-array-items": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-array-items/-/remove-array-items-1.1.0.tgz",
"integrity": "sha512-+YAHWd5patqAM/F4uBsto9h8RXDVxPRrKW46AkbI6eH12OFrN9wlGpkNWYxCjCfwtkidTjaaCXqU634V4mysvw=="
}
} }
}, },
"pkg-dir": { "pkg-dir": {
@@ -5641,6 +5652,11 @@
} }
} }
}, },
"remove-array-items": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/remove-array-items/-/remove-array-items-1.1.1.tgz",
"integrity": "sha1-/XRf9z0IIuVh6pEL8bQB/HhD5pM="
},
"remove-trailing-separator": { "remove-trailing-separator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@@ -5720,19 +5736,12 @@
"dev": true "dev": true
}, },
"resource-loader": { "resource-loader": {
"version": "2.2.0", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/resource-loader/-/resource-loader-2.2.0.tgz", "resolved": "https://registry.npmjs.org/resource-loader/-/resource-loader-2.2.3.tgz",
"integrity": "sha512-DfFhj7jveciV2fGu0gzJiev9i8mpIIjGiASBpPyUbQCPfNp7rnGF9mPMeJxPWnxNF4N9+NBkTIE1823/AnQy8g==", "integrity": "sha1-LIsHW3uTKKLnL7pvUbTf8nSHAAA=",
"requires": { "requires": {
"mini-signals": "^1.1.1", "mini-signals": "^1.1.1",
"parse-uri": "^1.0.0" "parse-uri": "^1.0.0"
},
"dependencies": {
"parse-uri": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/parse-uri/-/parse-uri-1.0.0.tgz",
"integrity": "sha1-KHLcwi8aeXrN4Vg9igrClVLdrCA="
}
} }
}, },
"restore-cursor": { "restore-cursor": {

View File

@@ -28,7 +28,7 @@
}, },
"homepage": "https://github.com/hjacobs/kube-ops-view#readme", "homepage": "https://github.com/hjacobs/kube-ops-view#readme",
"dependencies": { "dependencies": {
"pixi.js": "^4.7.3", "pixi.js": "^4.8.5",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"babel-polyfill": "^6.26.0" "babel-polyfill": "^6.26.0"
}, },

View File

@@ -1,6 +1,6 @@
import Tooltip from './tooltip.js' import Tooltip from './tooltip.js'
import Cluster from './cluster.js' import Cluster from './cluster.js'
import {Pod, ALL_PODS, sortByName, sortByMemory, sortByCPU, sortByAge} from './pod.js' import {Pod, ALL_PODS, sortByName, sortByMemory, sortByCPU, sortByAge, sortByStatus} from './pod.js'
import SelectBox from './selectbox' import SelectBox from './selectbox'
import {Theme, ALL_THEMES} from './themes.js' import {Theme, ALL_THEMES} from './themes.js'
import {DESATURATION_FILTER} from './filters.js' import {DESATURATION_FILTER} from './filters.js'
@@ -20,7 +20,7 @@ export default class App {
this.filterString = (params.get('q') && decodeURIComponent(params.get('q'))) || '' this.filterString = (params.get('q') && decodeURIComponent(params.get('q'))) || ''
this.selectedClusters = new Set((params.get('clusters') || '').split(',').filter(x => x)) this.selectedClusters = new Set((params.get('clusters') || '').split(',').filter(x => x))
this.seenPods = new Set() this.seenPods = new Set()
this.sorterFn = '' this.sorterFn = sortByName
this.theme = Theme.get(localStorage.getItem('theme')) this.theme = Theme.get(localStorage.getItem('theme'))
this.eventSource = null this.eventSource = null
this.connectTime = null this.connectTime = null
@@ -29,6 +29,13 @@ export default class App {
this.clusterStatuses = new Map() this.clusterStatuses = new Map()
this.viewContainerTargetPosition = new PIXI.Point() this.viewContainerTargetPosition = new PIXI.Point()
this.bootstrapping = true this.bootstrapping = true
this.startDrawingPodsAt = 24
this.defaultPodsPerRow = 6
this.defaultWidthOfNodePx = 105
this.defaultHeightOfNodePx = 115
this.sizeOfPodPx = 13
this.heightOfTopHandlePx = 15
} }
parseLocationHash() { parseLocationHash() {
@@ -67,11 +74,22 @@ export default class App {
return labels && labels[name] === value return labels && labels[name] === value
} }
namespaceMatches(pod, value) {
return pod.namespace === value
}
createMatchesFunctionForQuery(query) { createMatchesFunctionForQuery(query) {
if (query.includes('=')) { if (query.startsWith('namespace=')) {
const labelAndValue = query.split('=', 2) // filter by namespace
return pod => this.labelMatches(pod, labelAndValue[0], labelAndValue[1]) const value = query.split('namespace=', 2)[1]
} else { return pod => this.namespaceMatches(pod, value)
} else if (query.includes('=')) {
// filter by label
const [label, value] = query.split('=', 2)
return pod => this.labelMatches(pod, label, value)
}
else {
// filter by name
return pod => this.nameMatches(pod, query) return pod => this.nameMatches(pod, query)
} }
} }
@@ -324,6 +342,9 @@ export default class App {
}, },
{ {
text: 'SORT: CPU', value: sortByCPU text: 'SORT: CPU', value: sortByCPU
},
{
text: 'SORT: STATUS', value: sortByStatus
} }
] ]
//setting default sort //setting default sort

View File

@@ -14,41 +14,42 @@ export default class Bars extends PIXI.Graphics {
draw() { draw() {
const bars = this const bars = this
const barHeight = 92 const barHeightPx = bars.entity.cluster.heightOfNodePx - (App.current.heightOfTopHandlePx + 5 + 3)
const heightOfNodeWoPaddingPx = bars.entity.cluster.heightOfNodePx - 5
bars.beginFill(App.current.theme.primaryColor, 0.1) bars.beginFill(App.current.theme.primaryColor, 0.1)
bars.drawRect(5, 110 - barHeight, 15, barHeight) bars.drawRect(5, heightOfNodeWoPaddingPx - barHeightPx, 15, barHeightPx)
bars.endFill() bars.endFill()
// CPU // CPU
const cpuHeight = barHeight / bars.resources.cpu.capacity const cpuHeight = barHeightPx / bars.resources.cpu.capacity
bars.interactive = true bars.interactive = true
bars.lineStyle(0, 0xaaffaa, 1) bars.lineStyle(0, 0xaaffaa, 1)
bars.beginFill(getBarColor(bars.resources.cpu.requested, bars.resources.cpu.capacity - bars.resources.cpu.reserved), 1) bars.beginFill(getBarColor(bars.resources.cpu.requested, bars.resources.cpu.capacity - bars.resources.cpu.reserved), 1)
bars.drawRect(5, 110 - (bars.resources.cpu.requested + bars.resources.cpu.reserved) * cpuHeight, 2.5, (bars.resources.cpu.requested + bars.resources.cpu.reserved) * cpuHeight) bars.drawRect(5, heightOfNodeWoPaddingPx - (bars.resources.cpu.requested + bars.resources.cpu.reserved) * cpuHeight, 2.5, (bars.resources.cpu.requested + bars.resources.cpu.reserved) * cpuHeight)
bars.beginFill(getBarColor(bars.resources.cpu.used, bars.resources.cpu.capacity), 1) bars.beginFill(getBarColor(bars.resources.cpu.used, bars.resources.cpu.capacity), 1)
bars.drawRect(7.5, 110 - bars.resources.cpu.used * cpuHeight, 2.5, bars.resources.cpu.used * cpuHeight) bars.drawRect(7.5, heightOfNodeWoPaddingPx - bars.resources.cpu.used * cpuHeight, 2.5, bars.resources.cpu.used * cpuHeight)
bars.endFill() bars.endFill()
bars.lineStyle(1, App.current.theme.primaryColor, 1) bars.lineStyle(1, App.current.theme.primaryColor, 1)
bars.drawRect(5, 110 - bars.resources.cpu.reserved * cpuHeight, 5, bars.resources.cpu.reserved * cpuHeight) bars.drawRect(5, heightOfNodeWoPaddingPx - bars.resources.cpu.reserved * cpuHeight, 5, bars.resources.cpu.reserved * cpuHeight)
// Memory // Memory
const scale = bars.resources.memory.capacity / barHeight const scale = bars.resources.memory.capacity / barHeightPx
bars.lineStyle(0, 0xaaffaa, 1) bars.lineStyle(0, 0xaaffaa, 1)
bars.beginFill(getBarColor(bars.resources.memory.requested, bars.resources.memory.capacity - bars.resources.memory.reserved), 1) bars.beginFill(getBarColor(bars.resources.memory.requested, bars.resources.memory.capacity - bars.resources.memory.reserved), 1)
bars.drawRect(14, 110 - (bars.resources.memory.requested + bars.resources.memory.reserved) / scale, 2.5, (bars.resources.memory.requested + bars.resources.memory.reserved) / scale) bars.drawRect(14, heightOfNodeWoPaddingPx - (bars.resources.memory.requested + bars.resources.memory.reserved) / scale, 2.5, (bars.resources.memory.requested + bars.resources.memory.reserved) / scale)
bars.beginFill(getBarColor(bars.resources.memory.used, bars.resources.memory.capacity), 1) bars.beginFill(getBarColor(bars.resources.memory.used, bars.resources.memory.capacity), 1)
bars.drawRect(16.5, 110 - bars.resources.memory.used / scale, 2.5, bars.resources.memory.used / scale) bars.drawRect(16.5, heightOfNodeWoPaddingPx - bars.resources.memory.used / scale, 2.5, bars.resources.memory.used / scale)
bars.endFill() bars.endFill()
bars.lineStyle(1, App.current.theme.primaryColor, 1) bars.lineStyle(1, App.current.theme.primaryColor, 1)
bars.drawRect(14, 110 - bars.resources.memory.reserved / scale, 5, bars.resources.memory.reserved / scale) bars.drawRect(14, heightOfNodeWoPaddingPx - bars.resources.memory.reserved / scale, 5, bars.resources.memory.reserved / scale)
bars.lineStyle(1, App.current.theme.primaryColor, 1) bars.lineStyle(1, App.current.theme.primaryColor, 1)
for (var i = 0; i < bars.resources.cpu.capacity; i++) { for (var i = 0; i < bars.resources.cpu.capacity; i++) {
bars.drawRect(5, 110 - (i + 1) * cpuHeight, 5, cpuHeight) bars.drawRect(5, heightOfNodeWoPaddingPx - (i + 1) * cpuHeight, 5, cpuHeight)
} }
bars.drawRect(14, 110 - bars.resources.memory.capacity / scale, 5, bars.resources.memory.capacity / scale) bars.drawRect(14, heightOfNodeWoPaddingPx - bars.resources.memory.capacity / scale, 5, bars.resources.memory.capacity / scale)
bars.on('mouseover', function () { bars.on('mouseover', function () {
let s = 'CPU: \n' let s = 'CPU: \n'

View File

@@ -38,7 +38,34 @@ export default class Cluster extends PIXI.Graphics {
let workerWidth = 0 let workerWidth = 0
let workerHeight = 0 let workerHeight = 0
const workerNodes = [] const workerNodes = []
const maxWidth = window.innerWidth - 130
let maxPods = 0
// get the largest number of pods
for (const n of Object.values(this.cluster.nodes)) {
const podsInNode = Object.values(n.pods).length
if (podsInNode >= maxPods) {
maxPods = podsInNode
}
}
// with maxPods we can calculate the size of all nodes in the cluster
this.podsPerRow = Math.max(
App.current.defaultPodsPerRow,
Math.ceil(Math.sqrt(maxPods))
)
this.widthOfNodePx = Math.max(
App.current.defaultWidthOfNodePx,
Math.floor(this.podsPerRow * App.current.sizeOfPodPx + App.current.startDrawingPodsAt + 2)
)
this.heightOfNodePx = Math.max(
App.current.defaultHeightOfNodePx,
Math.floor(this.podsPerRow * App.current.sizeOfPodPx + App.current.heightOfTopHandlePx + (App.current.sizeOfPodPx * 2) + 2)
)
const maxWidth = window.innerWidth - (this.widthOfNodePx * 1.2)
for (const nodeName of Object.keys(this.cluster.nodes).sort()) { for (const nodeName of Object.keys(this.cluster.nodes).sort()) {
const node = this.cluster.nodes[nodeName] const node = this.cluster.nodes[nodeName]
var nodeBox = new Node(node, this, this.tooltip) var nodeBox = new Node(node, this, this.tooltip)
@@ -47,29 +74,29 @@ export default class Cluster extends PIXI.Graphics {
if (masterX > maxWidth) { if (masterX > maxWidth) {
masterWidth = masterX masterWidth = masterX
masterX = left masterX = left
masterY += nodeBox.height + padding masterY += this.heightOfNodePx + padding
masterHeight += nodeBox.height + padding masterHeight += this.heightOfNodePx + padding
} }
if (masterHeight == 0) { if (masterHeight == 0) {
masterHeight = nodeBox.height + padding masterHeight = this.heightOfNodePx + padding
} }
nodeBox.x = masterX nodeBox.x = masterX
nodeBox.y = masterY nodeBox.y = masterY
masterX += nodeBox.width + padding masterX += this.widthOfNodePx + padding
} else { } else {
if (workerX > maxWidth) { if (workerX > maxWidth) {
workerWidth = workerX workerWidth = workerX
workerX = left workerX = left
workerY += nodeBox.height + padding workerY += this.heightOfNodePx + padding
workerHeight += nodeBox.height + padding workerHeight += this.heightOfNodePx + padding
} }
workerNodes.push(nodeBox) workerNodes.push(nodeBox)
if (workerHeight == 0) { if (workerHeight == 0) {
workerHeight = nodeBox.height + padding workerHeight = this.heightOfNodePx + padding
} }
nodeBox.x = workerX nodeBox.x = workerX
nodeBox.y = workerY nodeBox.y = workerY
workerX += nodeBox.width + padding workerX += this.widthOfNodePx + padding
} }
this.addChild(nodeBox) this.addChild(nodeBox)
} }
@@ -98,7 +125,7 @@ export default class Cluster extends PIXI.Graphics {
const topHandle = this.topHandle = new PIXI.Graphics() const topHandle = this.topHandle = new PIXI.Graphics()
topHandle.beginFill(App.current.theme.primaryColor, 1) topHandle.beginFill(App.current.theme.primaryColor, 1)
topHandle.drawRect(0, 0, width, 15) topHandle.drawRect(0, 0, width, App.current.heightOfTopHandlePx)
topHandle.endFill() topHandle.endFill()
topHandle.interactive = true topHandle.interactive = true
topHandle.buttonMode = true topHandle.buttonMode = true

View File

@@ -64,9 +64,11 @@ export default class Node extends PIXI.Graphics {
const nodeBox = this const nodeBox = this
const topHandle = new PIXI.Graphics() const topHandle = new PIXI.Graphics()
topHandle.beginFill(App.current.theme.primaryColor, 1) topHandle.beginFill(App.current.theme.primaryColor, 1)
topHandle.drawRect(0, 0, 105, 15) topHandle.drawRect(0, 0, this.cluster.widthOfNodePx, App.current.heightOfTopHandlePx)
topHandle.endFill() topHandle.endFill()
const ellipsizedNodeName = this.node.name.length > 17 ? this.node.name.substring(0, 17).concat('…') : this.node.name // there is about 2.83 letters per pod
const roomForText = Math.floor(2.83 * this.cluster.podsPerRow)
const ellipsizedNodeName = this.node.name.length > roomForText ? this.node.name.substring(0, roomForText).concat('…') : this.node.name
const text = new PIXI.Text(ellipsizedNodeName, {fontFamily: 'ShareTechMono', fontSize: 10, fill: 0x000000}) const text = new PIXI.Text(ellipsizedNodeName, {fontFamily: 'ShareTechMono', fontSize: 10, fill: 0x000000})
text.x = 2 text.x = 2
text.y = 2 text.y = 2
@@ -74,7 +76,7 @@ export default class Node extends PIXI.Graphics {
nodeBox.addChild(topHandle) nodeBox.addChild(topHandle)
nodeBox.lineStyle(2, App.current.theme.primaryColor, 1) nodeBox.lineStyle(2, App.current.theme.primaryColor, 1)
nodeBox.beginFill(App.current.theme.secondaryColor, 1) nodeBox.beginFill(App.current.theme.secondaryColor, 1)
nodeBox.drawRect(0, 0, 105, 115) nodeBox.drawRect(0, 0, this.cluster.widthOfNodePx, this.cluster.heightOfNodePx)
nodeBox.endFill() nodeBox.endFill()
nodeBox.lineStyle(2, 0xaaaaaa, 1) nodeBox.lineStyle(2, 0xaaaaaa, 1)
topHandle.interactive = true topHandle.interactive = true
@@ -85,7 +87,7 @@ export default class Node extends PIXI.Graphics {
s += '\n ' + key + ': ' + nodeBox.node.labels[key] s += '\n ' + key + ': ' + nodeBox.node.labels[key]
} }
nodeBox.tooltip.setText(s) nodeBox.tooltip.setText(s)
nodeBox.tooltip.position = nodeBox.toGlobal(new PIXI.Point(0, 15)) nodeBox.tooltip.position = nodeBox.toGlobal(new PIXI.Point(0, App.current.heightOfTopHandlePx))
nodeBox.tooltip.visible = true nodeBox.tooltip.visible = true
}) })
topHandle.on('mouseout', function () { topHandle.on('mouseout', function () {
@@ -103,33 +105,37 @@ export default class Node extends PIXI.Graphics {
addPods(sorterFn) { addPods(sorterFn) {
const nodeBox = this const nodeBox = this
let px = 24 const px = App.current.startDrawingPodsAt
let py = 20 const py = App.current.heightOfTopHandlePx + 5
let podsCounter = 0
let podsKubeSystemCounter = 0
const pods = Object.values(this.node.pods).sort(sorterFn) const pods = Object.values(this.node.pods).sort(sorterFn)
for (const pod of pods) { for (const pod of pods) {
if (pod.namespace != 'kube-system') { if (pod.namespace != 'kube-system') {
const podBox = Pod.getOrCreate(pod, this.cluster, this.tooltip) const podBox = Pod.getOrCreate(pod, this.cluster, this.tooltip)
podBox.movePodTo(new PIXI.Point(px, py)) podBox.movePodTo(
new PIXI.Point(
// we have a room for this.cluster.podsPerRow pods
px + (App.current.sizeOfPodPx * (podsCounter % this.cluster.podsPerRow)),
// we just count when to get to another row
py + (App.current.sizeOfPodPx * Math.floor(podsCounter / this.cluster.podsPerRow))
)
)
nodeBox.addChild(podBox.draw()) nodeBox.addChild(podBox.draw())
px += 13 podsCounter++
if (px > 90) { } else {
px = 24 // kube-system pods
py += 13
}
}
}
px = 24
py = 100
for (const pod of pods) {
if (pod.namespace == 'kube-system') {
const podBox = Pod.getOrCreate(pod, this.cluster, this.tooltip) const podBox = Pod.getOrCreate(pod, this.cluster, this.tooltip)
podBox.movePodTo(new PIXI.Point(px, py)) podBox.movePodTo(
new PIXI.Point(
// we have a room for this.cluster.podsPerRow pods
px + (App.current.sizeOfPodPx * (podsKubeSystemCounter % this.cluster.podsPerRow)),
// like above (for not kube-system pods), but we count from the bottom
this.cluster.heightOfNodePx - App.current.sizeOfPodPx - 2 - (App.current.sizeOfPodPx * Math.floor(podsKubeSystemCounter / this.cluster.podsPerRow))
)
)
nodeBox.addChild(podBox.draw()) nodeBox.addChild(podBox.draw())
px += 13 podsKubeSystemCounter++
if (px > 90) {
px = 24
py -= 13
}
} }
} }
} }

View File

@@ -34,7 +34,11 @@ const sortByCPU = (a, b) => {
return bCpu - aCpu return bCpu - aCpu
} }
export {ALL_PODS, sortByAge, sortByCPU, sortByMemory, sortByName} const sortByStatus = (a, b) => {
return (a.phase).localeCompare(b.phase)
}
export {ALL_PODS, sortByAge, sortByCPU, sortByMemory, sortByName, sortByStatus}
export class Pod extends PIXI.Graphics { export class Pod extends PIXI.Graphics {
@@ -272,7 +276,7 @@ export class Pod extends PIXI.Graphics {
} }
// CPU // CPU
const scaleCpu = resources.cpu.requested <= resources.cpu.limit ? resources.cpu.limit / 8 : resources.cpu.requested / 8 const scaleCpu = Math.max(resources.cpu.requested, resources.cpu.limit, resources.cpu.used) / 8
const scaledCpuReq = resources.cpu.requested !== 0 && scaleCpu !== 0 ? resources.cpu.requested / scaleCpu : 0 const scaledCpuReq = resources.cpu.requested !== 0 && scaleCpu !== 0 ? resources.cpu.requested / scaleCpu : 0
const scaledCpuUsed = resources.cpu.used !== 0 && scaleCpu !== 0 ? resources.cpu.used / scaleCpu : 0 const scaledCpuUsed = resources.cpu.used !== 0 && scaleCpu !== 0 ? resources.cpu.used / scaleCpu : 0
podBox.lineStyle() podBox.lineStyle()
@@ -283,7 +287,7 @@ export class Pod extends PIXI.Graphics {
podBox.endFill() podBox.endFill()
// Memory // Memory
const scale = resources.memory.requested <= resources.memory.limit ? resources.memory.limit / 8 : resources.memory.requested / 8 const scale = Math.max(resources.memory.requested, resources.memory.limit, resources.memory.used) / 8
const scaledMemReq = resources.memory.requested !== 0 && scale !== 0 ? resources.memory.requested / scale : 0 const scaledMemReq = resources.memory.requested !== 0 && scale !== 0 ? resources.memory.requested / scale : 0
const scaledMemUsed = resources.memory.used !== 0 && scale !== 0 ? resources.memory.used / scale : 0 const scaledMemUsed = resources.memory.used !== 0 && scale !== 0 ? resources.memory.used / scale : 0
podBox.lineStyle() podBox.lineStyle()

View File

@@ -3,7 +3,7 @@ kind: Deployment
metadata: metadata:
labels: labels:
application: kube-ops-view application: kube-ops-view
version: v0.9.1 version: v0.10
name: kube-ops-view name: kube-ops-view
spec: spec:
replicas: 1 replicas: 1
@@ -14,13 +14,13 @@ spec:
metadata: metadata:
labels: labels:
application: kube-ops-view application: kube-ops-view
version: v0.9.1 version: v0.10
spec: spec:
serviceAccount: kube-ops-view serviceAccount: kube-ops-view
containers: containers:
- name: service - name: service
# see https://github.com/hjacobs/kube-ops-view/releases # see https://github.com/hjacobs/kube-ops-view/releases
image: hjacobs/kube-ops-view:0.9.1 image: hjacobs/kube-ops-view:0.10
args: args:
# remove this option to use built-in memory store # remove this option to use built-in memory store
- --redis-url=redis://kube-ops-view-redis:6379 - --redis-url=redis://kube-ops-view-redis:6379

View File

@@ -18,7 +18,7 @@ spec:
spec: spec:
containers: containers:
- name: redis - name: redis
image: redis:3.2-alpine image: redis:5-alpine
ports: ports:
- containerPort: 6379 - containerPort: 6379
protocol: TCP protocol: TCP
@@ -35,4 +35,5 @@ spec:
securityContext: securityContext:
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
runAsNonRoot: true runAsNonRoot: true
runAsUser: 1000 # we need to use the "redis" uid
runAsUser: 100

View File

@@ -12,6 +12,8 @@ Relevant configuration settings (environment variables) for OAuth are:
OAuth 2 authorization endpoint URL, e.g. https://oauth2.example.org/authorize OAuth 2 authorization endpoint URL, e.g. https://oauth2.example.org/authorize
``ACCESS_TOKEN_URL`` ``ACCESS_TOKEN_URL``
Token endpoint URL for the OAuth 2 Authorization Code Grant flow, e.g. https://oauth2.example.org/token Token endpoint URL for the OAuth 2 Authorization Code Grant flow, e.g. https://oauth2.example.org/token
``SCOPE``
OAuth 2 scopes provide a way to limit the amount of access that is granted to an access token, e.g. https://oauth2.example.org/authorize/readonly
``CREDENTIALS_DIR`` ``CREDENTIALS_DIR``
Folder path to load client credentials from. The folder needs to contain two files: ``authcode-client-id`` and ``authcode-client-secret``. Folder path to load client credentials from. The folder needs to contain two files: ``authcode-client-id`` and ``authcode-client-secret``.

View File

@@ -35,6 +35,7 @@ You can filter by:
* name * name
* labels - when query includes ``=``, e.g. ``env=prod`` * labels - when query includes ``=``, e.g. ``env=prod``
* namespace - when query starts with ``namespace``, e.g. ``namespace=default``
The pod filter is persisted in the location bar (``#q=..`` query parameter) which allows to conveniently send the filtered view to other users (e.g. for troubleshooting). The pod filter is persisted in the location bar (``#q=..`` query parameter) which allows to conveniently send the filtered view to other users (e.g. for troubleshooting).

View File

@@ -34,6 +34,7 @@ logger = logging.getLogger(__name__)
SERVER_STATUS = {'shutdown': False} SERVER_STATUS = {'shutdown': False}
AUTHORIZE_URL = os.getenv('AUTHORIZE_URL') AUTHORIZE_URL = os.getenv('AUTHORIZE_URL')
APP_URL = os.getenv('APP_URL') APP_URL = os.getenv('APP_URL')
SCOPE = os.getenv('SCOPE')
app = Flask(__name__) app = Flask(__name__)
@@ -45,7 +46,8 @@ auth = OAuthRemoteAppWithRefresh(
request_token_url=None, request_token_url=None,
access_token_method='POST', access_token_method='POST',
access_token_url=os.getenv('ACCESS_TOKEN_URL'), access_token_url=os.getenv('ACCESS_TOKEN_URL'),
authorize_url=AUTHORIZE_URL authorize_url=AUTHORIZE_URL,
request_token_params={'scope': SCOPE} if SCOPE else None
) )
oauth.remote_apps['auth'] = auth oauth.remote_apps['auth'] = auth

View File

@@ -1,4 +1,6 @@
import time import time
import random
import string
def hash_int(x: int): def hash_int(x: int):
@@ -35,8 +37,20 @@ def generate_mock_pod(index: int, i: int, j: int):
phase = pod_phases[hash_int((index + 1) * (i + 1) * (j + 1)) % len(pod_phases)] phase = pod_phases[hash_int((index + 1) * (i + 1) * (j + 1)) % len(pod_phases)]
containers = [] containers = []
for k in range(1 + j % 2): for k in range(1 + j % 2):
# generate "more real data"
requests_cpu = random.randint(10, 50)
requests_memory = random.randint(64, 256)
# with max, we defend ourselves against negative cpu/memory ;)
usage_cpu = max(requests_cpu + random.randint(-30, 30), 1)
usage_memory = max(requests_memory + random.randint(-64, 128), 1)
container = { container = {
'name': 'myapp', 'image': 'foo/bar/{}'.format(j), 'resources': {'requests': {'cpu': '100m', 'memory': '100Mi'}, 'limits': {}}, 'name': 'myapp',
'image': 'foo/bar/{}'.format(j),
'resources': {
'requests': {'cpu': f'{requests_cpu}m', 'memory': f'{requests_memory}Mi'},
'limits': {},
'usage': {'cpu': f'{usage_cpu}m', 'memory': f'{usage_memory}Mi'},
},
'ready': True, 'ready': True,
'state': {'running': {}} 'state': {'running': {}}
} }
@@ -86,9 +100,31 @@ def query_mock_cluster(cluster):
else: else:
pod = generate_mock_pod(index, i, j) pod = generate_mock_pod(index, i, j)
pods['{}/{}'.format(pod['namespace'], pod['name'])] = pod pods['{}/{}'.format(pod['namespace'], pod['name'])] = pod
node = {'name': 'node-{}'.format(i), 'labels': labels, 'status': {
'capacity': {'cpu': '4', 'memory': '32Gi', 'pods': '110'}, # use data from containers (usage)
'allocatable': {'cpu': '3800m', 'memory': '31Gi'}}, 'pods': pods} usage_cpu = 0
usage_memory = 0
for p in pods.values():
for c in p["containers"]:
usage_cpu += int(c["resources"]["usage"]["cpu"].split("m")[0])
usage_memory += int(c["resources"]["usage"]["memory"].split("Mi")[0])
# generate longer name for a node
suffix = ''.join(
[random.choice(string.ascii_letters) for n in range(random.randint(1, 20))]
)
node = {
'name': f'node-{i}-{suffix}',
'labels': labels,
'status': {
'capacity': {'cpu': '8', 'memory': '64Gi', 'pods': '110'},
'allocatable': {'cpu': '7800m', 'memory': '62Gi'}
},
'pods': pods,
# get data from containers (usage)
'usage': {'cpu': f'{usage_cpu}m', 'memory': f'{usage_memory}Mi'}
}
nodes[node['name']] = node nodes[node['name']] = node
pod = generate_mock_pod(index, 11, index) pod = generate_mock_pod(index, 11, index)
unassigned_pods = {'{}/{}'.format(pod['namespace'], pod['name']): pod} unassigned_pods = {'{}/{}'.format(pod['namespace'], pod['name']): pod}