Merge pull request #63 from hjacobs/35-sort-pods

35 sort pods
This commit is contained in:
Henning Jacobs
2016-12-23 18:35:14 +01:00
committed by GitHub
9 changed files with 236 additions and 70 deletions

18
app.py
View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import gevent.monkey
gevent.monkey.patch_all()
import flask
@@ -183,7 +184,8 @@ def get_kubernetes_clusters():
pods_by_namespace_name = {}
unassigned_pods = []
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_by_name[obj['name']] = obj
response = session.get(urljoin(api_server_url, '/api/v1/pods'), timeout=5)
@@ -191,9 +193,15 @@ def get_kubernetes_clusters():
for pod in response.json()['items']:
obj = {'name': pod['metadata']['name'],
'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']:
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']:
obj['containers'].append({'name': cont['name'], 'image': cont['image'], 'resources': cont['resources']})
pods_by_namespace_name[(obj['namespace'], obj['name'])] = obj
@@ -210,7 +218,9 @@ def get_kubernetes_clusters():
except:
logging.exception('Failed to get metrics')
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()
for metrics in response.json()['items']:
pod = pods_by_namespace_name.get((metrics['metadata']['namespace'], metrics['metadata']['name']))

View File

@@ -26,6 +26,7 @@
},
"homepage": "https://github.com/hjacobs/kube-ops-view#readme",
"dependencies": {
"pixi-display": "^1.0.1",
"pixi.js": "^4.3.0",
"webpack-dev-server": "^1.16.2"
},

View File

@@ -1,9 +1,12 @@
import Tooltip from './tooltip.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 { PRIMARY_VIOLET } from './colors.js'
const PIXI = require('pixi.js')
export default class App {
constructor() {
@@ -16,6 +19,7 @@ export default class App {
this.seenPods = new Set()
this.desaturationFilter = new PIXI.filters.ColorMatrixFilter()
this.desaturationFilter.desaturate()
this.sorterFn = ''
}
filter() {
@@ -42,7 +46,7 @@ export default class App {
for (const pod of node.children) {
const name = pod.pod && pod.pod.name
if (name) {
if (!name.includes(searchString)){
if (!name.includes(searchString)) {
pod.filters = [filter]
} else {
// TODO: pod might have other filters set..
@@ -70,24 +74,46 @@ export default class App {
const menuBar = new PIXI.Graphics()
menuBar.beginFill(PRIMARY_VIOLET, 1)
menuBar.drawRect(0, 0, window.innerWidth, 25)
menuBar.endFill()
menuBar.drawRect(0, 0, window.innerWidth, 28)
menuBar.lineStyle(1.5, 0x000000, 1)
menuBar.drawRect(20, 3, 200, 22)
stage.addChild(menuBar)
const searchPrompt = new PIXI.Text('>', {fontFamily: 'ShareTechMono', fontSize: 18})
searchPrompt.x = 20
searchPrompt.y = 5
PIXI.ticker.shared.add(function(_) {
var v = Math.sin((PIXI.ticker.shared.lastTime % 2000)/2000.* Math.PI)
const searchPrompt = new PIXI.Text('>', {fontFamily: 'ShareTechMono', fontSize: 14})
searchPrompt.x = 26
searchPrompt.y = 8
PIXI.ticker.shared.add(function (_) {
var v = Math.sin((PIXI.ticker.shared.lastTime % 2000) / 2000. * Math.PI)
searchPrompt.alpha = v
})
stage.addChild(searchPrompt)
const searchText = new PIXI.Text('', {fontFamily: 'ShareTechMono', fontSize: 18})
const searchText = new PIXI.Text('', {fontFamily: 'ShareTechMono', fontSize: 14})
searchText.x = 40
searchText.y = 5
searchText.y = 8
stage.addChild(searchText)
const items = [
{
text: 'SORT: NAME', sorterFn: sortByName
},
{
text: 'SORT: AGE', sorterFn: sortByAge
},
{
text: 'SORT: MEMORY', sorterFn: sortByMemory
},
{
text: 'SORT: CPU', sorterFn: sortByCPU
}
]
//setting default sort
App.sorterFn = items[0].sorterFn
const selectBox = new SelectBox(items)
selectBox.x = 265
selectBox.y = 3
menuBar.addChild(selectBox.draw())
const viewContainer = new PIXI.Container()
viewContainer.x = 20
viewContainer.y = 40
@@ -104,7 +130,7 @@ export default class App {
event.preventDefault()
}
else if (event.key == 'Backspace') {
this.filterString = this.filterString.slice(0, Math.max(0, this.filterString.length-1))
this.filterString = this.filterString.slice(0, Math.max(0, this.filterString.length - 1))
this.filter()
event.preventDefault()
}
@@ -138,7 +164,7 @@ export default class App {
pod._progress = 0
originalPod.visible = false
const that = this
const tick = function(t) {
const tick = function (t) {
// progress goes from 0 to 1
const progress = Math.min(1, pod._progress + (0.01 * t))
const scale = 1 + ((1 - progress) * 140)
@@ -269,13 +295,13 @@ export default class App {
function fetchData() {
fetch('kubernetes-clusters', {credentials: 'include'})
.then(function(response) {
return response.json()
})
.then(function(json) {
const clusters = json.kubernetes_clusters
that.update(clusters)
})
.then(function (response) {
return response.json()
})
.then(function (json) {
const clusters = json.kubernetes_clusters
that.update(clusters)
})
window.setTimeout(fetchData, 5000)
}

View File

@@ -1,4 +1,5 @@
import {FACTORS, getBarColor} from './utils'
import {PRIMARY_VIOLET} from './colors'
const PIXI = require('pixi.js')
@@ -15,10 +16,11 @@ export default class Bars extends PIXI.Graphics {
const barHeight = 92
bars.lineStyle(0, 0xaaffaa, 1)
bars.beginFill(0x999999, 0.1)
bars.beginFill(PRIMARY_VIOLET, 0.1)
bars.drawRect(5, 110 - barHeight, 15, barHeight)
bars.endFill()
// CPU
const cpuHeight = barHeight / bars.resources.cpu.capacity
bars.interactive = true
bars.lineStyle(0, 0xaaffaa, 1)
@@ -27,19 +29,23 @@ export default class Bars extends PIXI.Graphics {
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.endFill()
bars.lineStyle(1, 0xaaaaaa, 1)
for (var i = 0; i < bars.resources.cpu.capacity; i++) {
bars.drawRect(5, 110 - (i + 1) * cpuHeight, 5, cpuHeight)
}
// Memory
const scale = bars.resources.memory.capacity / barHeight
bars.drawRect(14, 110 - bars.resources.memory.capacity / scale, 5, bars.resources.memory.capacity / scale)
bars.lineStyle(0, 0xaaffaa, 1)
bars.beginFill(getBarColor(bars.resources.memory.requested, bars.resources.memory.capacity), 1)
bars.drawRect(14, 110 - bars.resources.memory.requested / scale, 2.5, bars.resources.memory.requested / scale)
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.endFill()
bars.lineStyle(1, PRIMARY_VIOLET, 1)
for (var i = 0; i < bars.resources.cpu.capacity; i++) {
bars.drawRect(5, 110 - (i + 1) * cpuHeight, 5, cpuHeight)
}
bars.drawRect(14, 110 - bars.resources.memory.capacity / scale, 5, bars.resources.memory.capacity / scale)
bars.on('mouseover', function () {
let s = 'CPU: \n'
const {capacity: cpuCap, requested: cpuReq, used: cpuUsed} = bars.resources.cpu

View File

@@ -1,6 +1,8 @@
import {Pod} from './pod.js'
import Bars from './bars.js'
import {parseResource} from './utils.js'
import {PRIMARY_VIOLET} from './colors.js'
import App from './app'
const PIXI = require('pixi.js')
export default class Node extends PIXI.Graphics {
@@ -43,10 +45,10 @@ export default class Node extends PIXI.Graphics {
return resources
}
draw () {
var nodeBox = this
var topHandle = new PIXI.Graphics()
topHandle.beginFill(0xaaaaff, 1)
draw() {
const nodeBox = this
const topHandle = new PIXI.Graphics()
topHandle.beginFill(PRIMARY_VIOLET, 1)
topHandle.drawRect(0, 0, 105, 15)
topHandle.endFill()
const ellipsizedNodeName = this.node.name.substring(0, 18).concat('...')
@@ -55,15 +57,15 @@ export default class Node extends PIXI.Graphics {
text.y = 2
topHandle.addChild(text)
nodeBox.addChild(topHandle)
nodeBox.lineStyle(2, 0xaaaaff, 1)
nodeBox.beginFill(0x999999, 0.5)
nodeBox.lineStyle(2, PRIMARY_VIOLET, 1)
nodeBox.beginFill(PRIMARY_VIOLET, 0.2)
nodeBox.drawRect(0, 0, 105, 115)
nodeBox.endFill()
nodeBox.lineStyle(2, 0xaaaaaa, 1)
topHandle.interactive = true
topHandle.on('mouseover', function () {
var s = nodeBox.node.name
for (var key of Object.keys(nodeBox.node.labels)) {
let s = nodeBox.node.name
for (const key of Object.keys(nodeBox.node.labels)) {
s += '\n' + key + ': ' + nodeBox.node.labels[key]
}
nodeBox.tooltip.setText(s)
@@ -79,9 +81,16 @@ export default class Node extends PIXI.Graphics {
bars.y = 1
nodeBox.addChild(bars.draw())
var px = 24
var py = 20
for (const pod of this.node.pods) {
nodeBox.addPods(App.sorterFn)
return nodeBox
}
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') {
const podBox = Pod.getOrCreate(pod, this.cluster, this.tooltip)
podBox.movePodTo(new PIXI.Point(px, py))
@@ -92,11 +101,10 @@ export default class Node extends PIXI.Graphics {
py += 13
}
}
}
px = 24
py = 100
for (const pod of this.node.pods) {
for (const pod of pods) {
if (pod.namespace == 'kube-system') {
const podBox = Pod.getOrCreate(pod, this.cluster, this.tooltip)
podBox.movePodTo(new PIXI.Point(px, py))
@@ -107,9 +115,6 @@ export default class Node extends PIXI.Graphics {
py -= 13
}
}
}
return nodeBox
}
}

View File

@@ -1,7 +1,36 @@
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 {
@@ -54,13 +83,6 @@ export class Pod extends PIXI.Graphics {
}
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 podMem = podResource('memory')
@@ -99,7 +121,7 @@ export class Pod extends PIXI.Graphics {
}
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
}
@@ -117,9 +139,9 @@ export class Pod extends PIXI.Graphics {
// pod.status.containerStatuses might be undefined!
const containerStatuses = this.pod.status.containerStatuses || []
var ready = 0
var running = 0
var restarts = 0
let ready = 0
let running = 0
let restarts = 0
for (const containerStatus of containerStatuses) {
if (containerStatus.ready) {
ready++
@@ -133,17 +155,18 @@ export class Pod extends PIXI.Graphics {
const allRunning = running >= containerStatuses.length
const resources = this.getResourceUsage()
var newTick = null
let newTick = null
const podBox = this
podBox.interactive = true
podBox.on('mouseover', function() {
podBox.on('mouseover', function () {
const filter = new PIXI.filters.ColorMatrixFilter()
filter.brightness(1.3)
podBox.filters = [filter]
let s = this.pod.name
s += '\nStatus: ' + this.pod.status.phase + ' (' + ready + '/' + containerStatuses.length + ' ready)'
s += '\nLabels:'
s += '\nStatus : ' + this.pod.status.phase + ' (' + ready + '/' + containerStatuses.length + ' ready)'
s += '\nStart Time: ' + this.pod.status.startTime
s += '\nLabels :'
for (var key of Object.keys(this.pod.labels)) {
if (key !== 'pod-template-hash') {
s += '\n ' + key + ': ' + this.pod.labels[key]
@@ -179,8 +202,8 @@ export class Pod extends PIXI.Graphics {
this.tooltip.visible = false
})
podBox.lineStyle(2, 0xaaaaaa, 1)
var i = 0
var w = 10 / this.pod.containers.length
let i = 0
const w = 10 / this.pod.containers.length
for (const container of this.pod.containers) {
podBox.drawRect(i * w, 0, w, 10)
i++
@@ -225,9 +248,9 @@ export class Pod extends PIXI.Graphics {
if (restarts) {
this.lineStyle(2, 0xff9999, 1)
for (let i=0; i<Math.min(restarts, 4); i++) {
this.moveTo(10, i*3 - 1)
this.lineTo(10, i*3 + 1)
for (let i = 0; i < Math.min(restarts, 4); i++) {
this.moveTo(10, i * 3 - 1)
this.lineTo(10, i * 3 + 1)
}
}

86
app/src/selectbox.js Normal file
View File

@@ -0,0 +1,86 @@
import { PRIMARY_VIOLET } from './colors.js'
import App from './app'
const PIXI = require('pixi.js')
export default class SelectBox extends PIXI.Graphics {
constructor(items) {
super()
this.items = items
this.count = 0
this.text = new PIXI.Text(this.items[this.count].text, {
fontFamily: 'ShareTechMono',
fontSize: 14,
fill: 0x000000,
align: 'center'
})
this.text.x = 10
this.text.y = 5
this.addChild(this.text)
}
onForwardPressed() {
const selectBox = this
selectBox.count++
if (selectBox.count >= this.items.length) {
selectBox.count = 0
}
selectBox.text.text = selectBox.items[selectBox.count].text
App.sorterFn = selectBox.items[selectBox.count].sorterFn
}
onBackPressed() {
const selectBox = this
selectBox.count--
if (selectBox.count < 0) {
selectBox.count = selectBox.items.length - 1
}
selectBox.text.text = selectBox.items[selectBox.count].text
App.sorterFn = selectBox.items[selectBox.count].sorterFn
}
draw() {
const selectBox = this
const backArrow = new PIXI.Graphics()
const forwardArrow = new PIXI.Graphics()
backArrow.interactive = true
forwardArrow.interactive = true
selectBox.interactive = true
// draw a triangle
backArrow.lineStyle(1.5, 0x000000, 1)
backArrow.beginFill(PRIMARY_VIOLET, 0.9)
backArrow.drawRect(-22, 0, 22, 22)
backArrow.moveTo(-7, 6)
backArrow.lineTo(-16, 11)
backArrow.lineTo(-7, 16)
backArrow.lineTo(-7, 6)
backArrow.endFill()
selectBox.addChild(backArrow)
selectBox.lineStyle(1.5, 0x000000, 1)
selectBox.beginFill(PRIMARY_VIOLET, 0.5)
selectBox.drawRect(4, 0, 100, 22)
selectBox.endFill()
forwardArrow.lineStyle(1.5, 0x000000, 1)
forwardArrow.beginFill(PRIMARY_VIOLET, 0.9)
forwardArrow.drawRect(108, 0, 22, 22)
forwardArrow.moveTo(115, 6)
forwardArrow.lineTo(124, 11)
forwardArrow.lineTo(115, 16)
forwardArrow.lineTo(115, 6)
forwardArrow.endFill()
selectBox.addChild(forwardArrow)
backArrow.on('mousedown', selectBox.onBackPressed.bind(this))
backArrow.on('touchstart', selectBox.onBackPressed.bind(this))
forwardArrow.on('mousedown', selectBox.onForwardPressed.bind(this))
forwardArrow.on('touchstart', selectBox.onForwardPressed.bind(this))
return selectBox
}
}

View File

@@ -68,4 +68,12 @@ function parseResource(v) {
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: /\.jpe?g$|\.svg$|\.png$/, exclude: /node_modules/, loader: 'file-loader?name=[path][name].[ext]'},
{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'}
],
postLoaders: [{