added sorting feature

This commit is contained in:
Christian Lohmann
2016-12-23 12:00:40 +01:00
parent e9584543df
commit a267272796
7 changed files with 124 additions and 66 deletions

32
app.py
View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import gevent.monkey import gevent.monkey
gevent.monkey.patch_all() gevent.monkey.patch_all()
import flask import flask
@@ -17,7 +18,6 @@ from flask import Flask, redirect
from flask_oauthlib.client import OAuth, OAuthRemoteApp from flask_oauthlib.client import OAuth, OAuthRemoteApp
from urllib.parse import urljoin from urllib.parse import urljoin
DEFAULT_CLUSTERS = 'http://localhost:8001/' DEFAULT_CLUSTERS = 'http://localhost:8001/'
CREDENTIALS_DIR = os.getenv('CREDENTIALS_DIR', '') CREDENTIALS_DIR = os.getenv('CREDENTIALS_DIR', '')
AUTHORIZE_URL = os.getenv('AUTHORIZE_URL') AUTHORIZE_URL = os.getenv('AUTHORIZE_URL')
@@ -119,15 +119,20 @@ def generate_mock_cluster_data(index: 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): for k in range(1):
containers.append({'name': 'myapp', 'image': 'foo/bar/{}'.format(j), 'resources': {'requests': {'cpu': '100m', 'memory': '100Mi'}}}) containers.append({'name': 'myapp', 'image': 'foo/bar/{}'.format(j),
'resources': {'requests': {'cpu': '100m', 'memory': '100Mi'}}})
status = {'phase': phase} status = {'phase': phase}
if phase == 'Running': if phase == 'Running':
if j % 13 == 0: if j % 13 == 0:
status['containerStatuses'] = [{'ready': False, 'state': {'waiting': {'reason': 'CrashLoopBackOff'}}}] status['containerStatuses'] = [
{'ready': False, 'state': {'waiting': {'reason': 'CrashLoopBackOff'}}}]
elif j % 7 == 0: elif j % 7 == 0:
status['containerStatuses'] = [{'ready': True, 'state': {'running': {}}, 'restartCount': 3}] status['containerStatuses'] = [{'ready': True, 'state': {'running': {}}, 'restartCount': 3}]
pods.append({'name': 'my-pod-{}'.format(j), 'namespace': 'kube-system' if j < 3 else 'default', 'labels': labels, 'status': status, 'containers': containers}) pods.append(
nodes.append({'name': 'node-{}'.format(i), 'labels': labels, 'status': {'capacity': {'cpu': '4', 'memory': '32Gi', 'pods': '110'}}, 'pods': pods}) {'name': 'my-pod-{}'.format(j), 'namespace': 'kube-system' if j < 3 else 'default', 'labels': labels,
'status': status, 'containers': containers})
nodes.append({'name': 'node-{}'.format(i), 'labels': labels,
'status': {'capacity': {'cpu': '4', 'memory': '32Gi', 'pods': '110'}}, 'pods': pods})
unassigned_pods = [] unassigned_pods = []
return { return {
'api_server_url': 'https://kube-{}.example.org'.format(index), 'api_server_url': 'https://kube-{}.example.org'.format(index),
@@ -156,7 +161,8 @@ def get_kubernetes_clusters():
pods_by_namespace_name = {} pods_by_namespace_name = {}
unassigned_pods = [] unassigned_pods = []
for node in response.json()['items']: for node in response.json()['items']:
obj = {'name': node['metadata']['name'], 'labels': node['metadata']['labels'], 'status': node['status'], 'pods': []} obj = {'name': node['metadata']['name'], 'labels': node['metadata']['labels'], 'status': node['status'],
'pods': []}
nodes.append(obj) nodes.append(obj)
nodes_by_name[obj['name']] = obj nodes_by_name[obj['name']] = obj
response = session.get(urljoin(api_server_url, '/api/v1/pods'), timeout=5) response = session.get(urljoin(api_server_url, '/api/v1/pods'), timeout=5)
@@ -164,9 +170,15 @@ def get_kubernetes_clusters():
for pod in response.json()['items']: for pod in response.json()['items']:
obj = {'name': pod['metadata']['name'], obj = {'name': pod['metadata']['name'],
'namespace': pod['metadata']['namespace'], 'namespace': pod['metadata']['namespace'],
'labels': pod['metadata'].get('labels', {}), 'status': pod['status'], 'containers': []} 'labels': pod['metadata'].get('labels', {}),
'status': pod['status'],
'startTime': pod['status']['startTime'] if 'startTime' in pod['status'] else '',
'containers': []
}
if 'deletionTimestamp' in pod['metadata']: if 'deletionTimestamp' in pod['metadata']:
obj['deleted'] = datetime.datetime.strptime(pod['metadata']['deletionTimestamp'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=datetime.timezone.utc).timestamp() obj['deleted'] = datetime.datetime.strptime(pod['metadata']['deletionTimestamp'],
'%Y-%m-%dT%H:%M:%SZ').replace(
tzinfo=datetime.timezone.utc).timestamp()
for cont in pod['spec']['containers']: for cont in pod['spec']['containers']:
obj['containers'].append({'name': cont['name'], 'image': cont['image'], 'resources': cont['resources']}) obj['containers'].append({'name': cont['name'], 'image': cont['image'], 'resources': cont['resources']})
pods_by_namespace_name[(obj['namespace'], obj['name'])] = obj pods_by_namespace_name[(obj['namespace'], obj['name'])] = obj
@@ -183,7 +195,9 @@ def get_kubernetes_clusters():
except: except:
logging.exception('Failed to get metrics') logging.exception('Failed to get metrics')
try: try:
response = session.get(urljoin(api_server_url, '/api/v1/namespaces/kube-system/services/heapster/proxy/apis/metrics/v1alpha1/pods'), timeout=5) response = session.get(urljoin(api_server_url,
'/api/v1/namespaces/kube-system/services/heapster/proxy/apis/metrics/v1alpha1/pods'),
timeout=5)
response.raise_for_status() response.raise_for_status()
for metrics in response.json()['items']: for metrics in response.json()['items']:
pod = pods_by_namespace_name.get((metrics['metadata']['namespace'], metrics['metadata']['name'])) pod = pods_by_namespace_name.get((metrics['metadata']['namespace'], metrics['metadata']['name']))

View File

@@ -1,9 +1,10 @@
import Tooltip from './tooltip.js' import Tooltip from './tooltip.js'
import Cluster from './cluster.js' import Cluster from './cluster.js'
import {Pod, ALL_PODS} from './pod.js' import {Pod, ALL_PODS, sortByName, sortByMemory, sortByCPU, sortByAge} from './pod.js'
import SelectBox from './selectbox' import SelectBox from './selectbox'
import { PRIMARY_VIOLET } from './colors.js' import { PRIMARY_VIOLET } from './colors.js'
import 'pixi-display' import 'pixi-display'
const PIXI = require('pixi.js') const PIXI = require('pixi.js')
@@ -12,6 +13,7 @@ export default class App {
constructor() { constructor() {
this.filterString = '' this.filterString = ''
this.seenPods = {} this.seenPods = {}
this.sorterFn = ''
} }
filter() { filter() {
@@ -54,13 +56,13 @@ export default class App {
const menuBar = new PIXI.Graphics() const menuBar = new PIXI.Graphics()
menuBar.beginFill(PRIMARY_VIOLET, 1) menuBar.beginFill(PRIMARY_VIOLET, 1)
menuBar.drawRect(0, 0, window.innerWidth, 25) menuBar.drawRect(0, 0, window.innerWidth, 35)
menuBar.endFill() menuBar.endFill()
stage.addChild(menuBar) stage.addChild(menuBar)
const searchPrompt = new PIXI.Text('>', {fontFamily: 'ShareTechMono', fontSize: 18}) const searchPrompt = new PIXI.Text('>', {fontFamily: 'ShareTechMono', fontSize: 18})
searchPrompt.x = 20 searchPrompt.x = 20
searchPrompt.y = 5 searchPrompt.y = 10
PIXI.ticker.shared.add(function (_) { PIXI.ticker.shared.add(function (_) {
var v = Math.sin((PIXI.ticker.shared.lastTime % 2000) / 2000. * Math.PI) var v = Math.sin((PIXI.ticker.shared.lastTime % 2000) / 2000. * Math.PI)
searchPrompt.alpha = v searchPrompt.alpha = v
@@ -74,22 +76,23 @@ export default class App {
const items = [ const items = [
{ {
text: 'Name', sorterFn: () => {} text: 'Name', sorterFn: sortByName
}, },
{ {
text: 'Age', sorterFn: () => {} text: 'Age', sorterFn: sortByAge
}, },
{ {
text: 'Memory', sorterFn: () => {} text: 'Memory', sorterFn: sortByMemory
}, },
{ {
text: 'CPU', sorterFn: () => {} text: 'CPU', sorterFn: sortByCPU
}, }
] ]
//setting default sort
this.sorterFn = items[0].sorterFn
const selectBox = new SelectBox(items) const selectBox = new SelectBox(items)
selectBox.x = 265 selectBox.x = 265
selectBox.y = 5 selectBox.y = 3
const mainLayer = new PIXI.DisplayGroup(1, true) const mainLayer = new PIXI.DisplayGroup(1, true)
selectBox.displayGroup = mainLayer selectBox.displayGroup = mainLayer
stage.addChild(selectBox.draw()) stage.addChild(selectBox.draw())

View File

@@ -1,6 +1,7 @@
import {Pod} from './pod.js' import {Pod} from './pod.js'
import Bars from './bars.js' import Bars from './bars.js'
import {parseResource} from './utils.js' import {parseResource} from './utils.js'
import App from './app'
const PIXI = require('pixi.js') const PIXI = require('pixi.js')
export default class Node extends PIXI.Graphics { export default class Node extends PIXI.Graphics {
@@ -43,9 +44,9 @@ export default class Node extends PIXI.Graphics {
return resources return resources
} }
draw () { draw() {
var nodeBox = this const nodeBox = this
var topHandle = new PIXI.Graphics() const topHandle = new PIXI.Graphics()
topHandle.beginFill(0xaaaaff, 1) topHandle.beginFill(0xaaaaff, 1)
topHandle.drawRect(0, 0, 105, 15) topHandle.drawRect(0, 0, 105, 15)
topHandle.endFill() topHandle.endFill()
@@ -62,8 +63,8 @@ export default class Node extends PIXI.Graphics {
nodeBox.lineStyle(2, 0xaaaaaa, 1) nodeBox.lineStyle(2, 0xaaaaaa, 1)
topHandle.interactive = true topHandle.interactive = true
topHandle.on('mouseover', function () { topHandle.on('mouseover', function () {
var s = nodeBox.node.name let s = nodeBox.node.name
for (var key of Object.keys(nodeBox.node.labels)) { for (const key of Object.keys(nodeBox.node.labels)) {
s += '\n' + key + ': ' + nodeBox.node.labels[key] s += '\n' + key + ': ' + nodeBox.node.labels[key]
} }
nodeBox.tooltip.setText(s) nodeBox.tooltip.setText(s)
@@ -79,9 +80,16 @@ export default class Node extends PIXI.Graphics {
bars.y = 1 bars.y = 1
nodeBox.addChild(bars.draw()) nodeBox.addChild(bars.draw())
var px = 24 nodeBox.addPods(App.sorterFn)
var py = 20 return nodeBox
for (const pod of this.node.pods) { }
addPods(sorterFn) {
const nodeBox = this
let px = 24
let py = 20
const pods = sorterFn !== 'undefined' ? this.node.pods.sort(sorterFn) : this.node.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) //new Pod(pod, this.tooltip) const podBox = Pod.getOrCreate(pod, this.cluster, this.tooltip) //new Pod(pod, this.tooltip)
podBox.x = px podBox.x = px
@@ -93,11 +101,10 @@ export default class Node extends PIXI.Graphics {
py += 13 py += 13
} }
} }
} }
px = 24 px = 24
py = 100 py = 100
for (const pod of this.node.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) //new Pod(pod, this.tooltip) const podBox = Pod.getOrCreate(pod, this.cluster, this.tooltip) //new Pod(pod, this.tooltip)
podBox.x = px podBox.x = px
@@ -109,9 +116,6 @@ export default class Node extends PIXI.Graphics {
py -= 13 py -= 13
} }
} }
} }
return nodeBox
} }
} }

View File

@@ -1,7 +1,36 @@
const PIXI = require('pixi.js') const PIXI = require('pixi.js')
import {FACTORS, parseResource, getBarColor} from './utils.js' import {FACTORS, getBarColor, podResource} from './utils.js'
export const ALL_PODS = {} const ALL_PODS = {}
const sortByName = (a, b) => {
return a.name.localeCompare(b.name)
}
const sortByAge = (a, b) => {
const dateA = new Date(a.status.startTime)
const dateB = new Date(b.status.startTime)
if (dateA.getTime() < dateB.getTime()) {
return -1
} else if (dateA.getTime() === dateB.getTime())
return 0
else
return 1
}
const sortByMemory = (a, b) => {
const aMem = podResource('memory')(a.containers, 'usage')
const bMem = podResource('memory')(b.containers, 'usage')
return bMem - aMem
}
const sortByCPU = (a, b) => {
const aCpu = podResource('cpu')(a.containers, 'usage')
const bCpu = podResource('cpu')(b.containers, 'usage')
return bCpu - aCpu
}
export {ALL_PODS, sortByAge, sortByCPU, sortByMemory, sortByName}
export class Pod extends PIXI.Graphics { export class Pod extends PIXI.Graphics {
@@ -18,13 +47,6 @@ export class Pod extends PIXI.Graphics {
} }
getResourceUsage() { getResourceUsage() {
const metric = (metric, type) =>
metric ? (metric[type] ? parseResource(metric[type]) : 0) : 0
const podResource = type => (containers, resource) =>
containers
.map(({resources}) => metric(resources[resource], type))
.reduce((a, b) => a + b, 0)
const podCpu = podResource('cpu') const podCpu = podResource('cpu')
const podMem = podResource('memory') const podMem = podResource('memory')
@@ -63,7 +85,7 @@ export class Pod extends PIXI.Graphics {
} }
pulsate(time) { pulsate(time) {
const v = Math.sin((PIXI.ticker.shared.lastTime % 1000)/1000.* Math.PI) const v = Math.sin((PIXI.ticker.shared.lastTime % 1000) / 1000. * Math.PI)
this.alpha = v * this._progress this.alpha = v * this._progress
} }
@@ -82,9 +104,9 @@ export class Pod extends PIXI.Graphics {
// pod.status.containerStatuses might be undefined! // pod.status.containerStatuses might be undefined!
const containerStatuses = this.pod.status.containerStatuses || [] const containerStatuses = this.pod.status.containerStatuses || []
var ready = 0 let ready = 0
var running = 0 let running = 0
var restarts = 0 let restarts = 0
for (const containerStatus of containerStatuses) { for (const containerStatus of containerStatuses) {
if (containerStatus.ready) { if (containerStatus.ready) {
ready++ ready++
@@ -98,17 +120,18 @@ export class Pod extends PIXI.Graphics {
const allRunning = running >= containerStatuses.length const allRunning = running >= containerStatuses.length
const resources = this.getResourceUsage() const resources = this.getResourceUsage()
var newTick = null let newTick = null
const podBox = this const podBox = this
podBox.interactive = true podBox.interactive = true
podBox.on('mouseover', function() { podBox.on('mouseover', function () {
const filter = new PIXI.filters.ColorMatrixFilter() const filter = new PIXI.filters.ColorMatrixFilter()
filter.brightness(1.3) filter.brightness(1.3)
podBox.filters = [filter] podBox.filters = [filter]
let s = this.pod.name let s = this.pod.name
s += '\nStatus: ' + this.pod.status.phase + ' (' + ready + '/' + containerStatuses.length + ' ready)' s += '\nStatus : ' + this.pod.status.phase + ' (' + ready + '/' + containerStatuses.length + ' ready)'
s += '\nLabels:' s += '\nStart Time: ' + this.pod.status.startTime
s += '\nLabels :'
for (var key of Object.keys(this.pod.labels)) { for (var key of Object.keys(this.pod.labels)) {
if (key !== 'pod-template-hash') { if (key !== 'pod-template-hash') {
s += '\n ' + key + ': ' + this.pod.labels[key] s += '\n ' + key + ': ' + this.pod.labels[key]
@@ -144,8 +167,8 @@ export class Pod extends PIXI.Graphics {
this.tooltip.visible = false this.tooltip.visible = false
}) })
podBox.lineStyle(2, 0xaaaaaa, 1) podBox.lineStyle(2, 0xaaaaaa, 1)
var i = 0 let i = 0
var w = 10 / this.pod.containers.length const w = 10 / this.pod.containers.length
for (const container of this.pod.containers) { for (const container of this.pod.containers) {
podBox.drawRect(i * w, 0, w, 10) podBox.drawRect(i * w, 0, w, 10)
i++ i++
@@ -190,9 +213,9 @@ export class Pod extends PIXI.Graphics {
if (restarts) { if (restarts) {
this.lineStyle(2, 0xff9999, 1) this.lineStyle(2, 0xff9999, 1)
for (let i=0; i<Math.min(restarts, 4); i++) { for (let i = 0; i < Math.min(restarts, 4); i++) {
this.moveTo(10, i*3 - 1) this.moveTo(10, i * 3 - 1)
this.lineTo(10, i*3 + 1) this.lineTo(10, i * 3 + 1)
} }
} }

View File

@@ -1,3 +1,6 @@
import { PRIMARY_VIOLET } from './colors.js'
import App from './app'
const PIXI = require('pixi.js') const PIXI = require('pixi.js')
export default class SelectBox extends PIXI.Graphics { export default class SelectBox extends PIXI.Graphics {
@@ -8,11 +11,11 @@ export default class SelectBox extends PIXI.Graphics {
this.text = new PIXI.Text(this.items[this.count].text, { this.text = new PIXI.Text(this.items[this.count].text, {
fontFamily: 'ShareTechMono', fontFamily: 'ShareTechMono',
fontSize: 14, fontSize: 14,
fill: 0xaaaaff, fill: 0x000000,
align: 'center' align: 'center'
}) })
this.text.x = 10 this.text.x = 10
this.text.y = 10 this.text.y = 8
this.addChild(this.text) this.addChild(this.text)
} }
@@ -24,6 +27,7 @@ export default class SelectBox extends PIXI.Graphics {
selectBox.count = 0 selectBox.count = 0
} }
selectBox.text.text = selectBox.items[selectBox.count].text selectBox.text.text = selectBox.items[selectBox.count].text
App.sorterFn = selectBox.items[selectBox.count].sorterFn
} }
onBackPressed() { onBackPressed() {
@@ -34,6 +38,7 @@ export default class SelectBox extends PIXI.Graphics {
selectBox.count = selectBox.items.length - 1 selectBox.count = selectBox.items.length - 1
} }
selectBox.text.text = selectBox.items[selectBox.count].text selectBox.text.text = selectBox.items[selectBox.count].text
App.sorterFn = selectBox.items[selectBox.count].sorterFn
} }
draw() { draw() {
@@ -44,27 +49,27 @@ export default class SelectBox extends PIXI.Graphics {
backArrow.interactive = true backArrow.interactive = true
forwardArrow.interactive = true forwardArrow.interactive = true
selectBox.interactive = true selectBox.interactive = true
// set a fill and line style
// draw a triangle // draw a triangle
backArrow.lineStyle(2, 0x000000, 1) backArrow.lineStyle(2, 0x000000, 1)
backArrow.beginFill(0x1b7c87, 0.5) backArrow.beginFill(PRIMARY_VIOLET, 0.5)
backArrow.moveTo(0, 2) backArrow.moveTo(0, 2)
backArrow.lineTo(-20, 15) backArrow.lineTo(-20, 14)
backArrow.lineTo(0, 28) backArrow.lineTo(0, 26)
backArrow.lineTo(0, 2) backArrow.lineTo(0, 2)
backArrow.endFill() backArrow.endFill()
selectBox.addChild(backArrow) selectBox.addChild(backArrow)
selectBox.lineStyle(2, 0x000000, 1) selectBox.lineStyle(2, 0x000000, 1)
selectBox.beginFill(0x1b7c87, 0.5) selectBox.beginFill(PRIMARY_VIOLET, 0.5)
selectBox.drawRoundedRect(4, 0, 100, 30, 10) selectBox.drawRoundedRect(4, 0, 100, 28, 5)
selectBox.endFill() selectBox.endFill()
forwardArrow.lineStyle(2, 0x000000, 1) forwardArrow.lineStyle(2, 0x000000, 1)
forwardArrow.beginFill(0x1b7c87, 0.5) forwardArrow.beginFill(PRIMARY_VIOLET, 0.5)
forwardArrow.moveTo(108, 2) forwardArrow.moveTo(108, 2)
forwardArrow.lineTo(128, 15) forwardArrow.lineTo(128, 14)
forwardArrow.lineTo(108, 28) forwardArrow.lineTo(108, 26)
forwardArrow.lineTo(108, 2) forwardArrow.lineTo(108, 2)
forwardArrow.endFill() forwardArrow.endFill()
selectBox.addChild(forwardArrow) selectBox.addChild(forwardArrow)

View File

@@ -68,4 +68,12 @@ function parseResource(v) {
return parseInt(match[1]) * factor return parseInt(match[1]) * factor
} }
export {FACTORS, hsvToRgb, getBarColor, parseResource} const metric = (metric, type) =>
metric ? (metric[type] ? parseResource(metric[type]) : 0) : 0
const podResource = type => (containers, resource) =>
containers
.map(({resources}) => metric(resources[resource], type))
.reduce((a, b) => a + b, 0)
export {FACTORS, hsvToRgb, getBarColor, parseResource, metric, podResource}

View File

@@ -33,6 +33,7 @@ module.exports = {
{test: /\.html$/, exclude: /node_modules/, loader: 'file-loader?name=[path][name].[ext]'}, {test: /\.html$/, exclude: /node_modules/, loader: 'file-loader?name=[path][name].[ext]'},
{test: /\.jpe?g$|\.svg$|\.png$/, exclude: /node_modules/, loader: 'file-loader?name=[path][name].[ext]'}, {test: /\.jpe?g$|\.svg$|\.png$/, exclude: /node_modules/, loader: 'file-loader?name=[path][name].[ext]'},
{test: /\.json$/, exclude: /node_modules/, loader: 'json'}, {test: /\.json$/, exclude: /node_modules/, loader: 'json'},
{test: /\.(otf|eot|svg|ttf|woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=8192&mimetype=application/font-woff'},
{test: /\.json$/, include: path.join(__dirname, 'node_modules', 'pixi.js'), loader: 'json'} {test: /\.json$/, include: path.join(__dirname, 'node_modules', 'pixi.js'), loader: 'json'}
], ],
postLoaders: [{ postLoaders: [{