Merge branch 'master' into 35-sort-pods
# Conflicts: # app/src/app.js
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ __pycache__
|
|||||||
**/node_modules/
|
**/node_modules/
|
||||||
static/build/
|
static/build/
|
||||||
*-secret
|
*-secret
|
||||||
|
npm-debug.log*
|
||||||
|
|||||||
16
README.rst
16
README.rst
@@ -61,6 +61,18 @@ Afterwards you can open "kube-ops-view" via the kubectl proxy:
|
|||||||
Now direct your browser to http://localhost:8001/api/v1/proxy/namespaces/default/services/kube-ops-view/
|
Now direct your browser to http://localhost:8001/api/v1/proxy/namespaces/default/services/kube-ops-view/
|
||||||
|
|
||||||
|
|
||||||
|
Mock Mode
|
||||||
|
=========
|
||||||
|
|
||||||
|
You can start the app in "mock mode" to see all UI features without running any Kubernetes cluster:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pip3 install -r requirements.txt
|
||||||
|
$ (cd app && npm start &)
|
||||||
|
$ MOCK=true ./app.py
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
=============
|
=============
|
||||||
|
|
||||||
@@ -71,7 +83,9 @@ The following environment variables are supported:
|
|||||||
``CREDENTIALS_DIR``
|
``CREDENTIALS_DIR``
|
||||||
Directory to read (OAuth) credentials from --- these credentials are only used for non-localhost cluster URLs.
|
Directory to read (OAuth) credentials from --- these credentials are only used for non-localhost cluster URLs.
|
||||||
``DEBUG``
|
``DEBUG``
|
||||||
Set to a non-empty value for local development to reload code changes.
|
Set to "true" for local development to reload code changes.
|
||||||
|
``MOCK``
|
||||||
|
Set to "true" to mock Kubernetes cluster data.
|
||||||
|
|
||||||
|
|
||||||
Supported Browsers
|
Supported Browsers
|
||||||
|
|||||||
61
app.py
61
app.py
@@ -22,9 +22,10 @@ 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')
|
||||||
APP_URL = os.getenv('APP_URL')
|
APP_URL = os.getenv('APP_URL')
|
||||||
|
MOCK = os.getenv('MOCK', '').lower() == 'true'
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.debug = os.getenv('DEBUG') == 'true'
|
app.debug = os.getenv('DEBUG', '').lower() == 'true'
|
||||||
app.secret_key = os.getenv('SECRET_KEY', 'development')
|
app.secret_key = os.getenv('SECRET_KEY', 'development')
|
||||||
|
|
||||||
oauth = OAuth(app)
|
oauth = OAuth(app)
|
||||||
@@ -98,9 +99,51 @@ def index():
|
|||||||
return flask.render_template('index.html', app_js=app_js)
|
return flask.render_template('index.html', app_js=app_js)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/kubernetes-clusters')
|
def hash_int(x: int):
|
||||||
@authorize
|
x = ((x >> 16) ^ x) * 0x45d9f3b
|
||||||
def get_clusters():
|
x = ((x >> 16) ^ x) * 0x45d9f3b
|
||||||
|
x = (x >> 16) ^ x
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def generate_mock_cluster_data(index: int):
|
||||||
|
'''Generate deterministic (no randomness!) mock data'''
|
||||||
|
nodes = []
|
||||||
|
pod_phases = ['Pending', 'Running', 'Running']
|
||||||
|
for i in range(10):
|
||||||
|
labels = {}
|
||||||
|
if i < 2:
|
||||||
|
labels['master'] = 'true'
|
||||||
|
pods = []
|
||||||
|
for j in range(hash_int((index + 1) * (i + 1)) % 32):
|
||||||
|
phase = pod_phases[hash_int((index + 1) * (i + 1) * (j + 1)) % len(pod_phases)]
|
||||||
|
containers = []
|
||||||
|
for k in range(1):
|
||||||
|
containers.append({'name': 'myapp', 'image': 'foo/bar/{}'.format(j), 'resources': {'requests': {'cpu': '100m', 'memory': '100Mi'}}})
|
||||||
|
status = {'phase': phase}
|
||||||
|
if phase == 'Running':
|
||||||
|
if j % 13 == 0:
|
||||||
|
status['containerStatuses'] = [{'ready': False, 'state': {'waiting': {'reason': 'CrashLoopBackOff'}}}]
|
||||||
|
elif j % 7 == 0:
|
||||||
|
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})
|
||||||
|
nodes.append({'name': 'node-{}'.format(i), 'labels': labels, 'status': {'capacity': {'cpu': '4', 'memory': '32Gi', 'pods': '110'}}, 'pods': pods})
|
||||||
|
unassigned_pods = []
|
||||||
|
return {
|
||||||
|
'api_server_url': 'https://kube-{}.example.org'.format(index),
|
||||||
|
'nodes': nodes,
|
||||||
|
'unassigned_pods': unassigned_pods
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_mock_clusters():
|
||||||
|
clusters = []
|
||||||
|
for i in range(3):
|
||||||
|
clusters.append(generate_mock_cluster_data(i))
|
||||||
|
return clusters
|
||||||
|
|
||||||
|
|
||||||
|
def get_kubernetes_clusters():
|
||||||
clusters = []
|
clusters = []
|
||||||
for api_server_url in (os.getenv('CLUSTERS') or DEFAULT_CLUSTERS).split(','):
|
for api_server_url in (os.getenv('CLUSTERS') or DEFAULT_CLUSTERS).split(','):
|
||||||
if 'localhost' not in api_server_url:
|
if 'localhost' not in api_server_url:
|
||||||
@@ -152,6 +195,16 @@ def get_clusters():
|
|||||||
except:
|
except:
|
||||||
logging.exception('Failed to get metrics')
|
logging.exception('Failed to get metrics')
|
||||||
clusters.append({'api_server_url': api_server_url, 'nodes': nodes, 'unassigned_pods': unassigned_pods})
|
clusters.append({'api_server_url': api_server_url, 'nodes': nodes, 'unassigned_pods': unassigned_pods})
|
||||||
|
return clusters
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/kubernetes-clusters')
|
||||||
|
@authorize
|
||||||
|
def get_clusters():
|
||||||
|
if MOCK:
|
||||||
|
clusters = get_mock_clusters()
|
||||||
|
else:
|
||||||
|
clusters = get_kubernetes_clusters()
|
||||||
|
|
||||||
return json.dumps({'kubernetes_clusters': clusters}, separators=(',', ':'))
|
return json.dumps({'kubernetes_clusters': clusters}, separators=(',', ':'))
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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} from './pod.js'
|
||||||
import SelectBox from './selectbox'
|
import SelectBox from './selectbox'
|
||||||
|
import { PRIMARY_VIOLET } from './colors.js'
|
||||||
import 'pixi-display'
|
import 'pixi-display'
|
||||||
const PIXI = require('pixi.js')
|
const PIXI = require('pixi.js')
|
||||||
|
|
||||||
@@ -51,7 +52,13 @@ export default class App {
|
|||||||
|
|
||||||
stage.displayList = new PIXI.DisplayList()
|
stage.displayList = new PIXI.DisplayList()
|
||||||
|
|
||||||
const searchPrompt = new PIXI.Text('>', {fontFamily: 'ShareTechMono', fontSize: 18, fill: 0xaaaaff})
|
const menuBar = new PIXI.Graphics()
|
||||||
|
menuBar.beginFill(PRIMARY_VIOLET, 1)
|
||||||
|
menuBar.drawRect(0, 0, window.innerWidth, 25)
|
||||||
|
menuBar.endFill()
|
||||||
|
stage.addChild(menuBar)
|
||||||
|
|
||||||
|
const searchPrompt = new PIXI.Text('>', {fontFamily: 'ShareTechMono', fontSize: 18})
|
||||||
searchPrompt.x = 20
|
searchPrompt.x = 20
|
||||||
searchPrompt.y = 5
|
searchPrompt.y = 5
|
||||||
PIXI.ticker.shared.add(function (_) {
|
PIXI.ticker.shared.add(function (_) {
|
||||||
@@ -60,7 +67,7 @@ export default class App {
|
|||||||
})
|
})
|
||||||
stage.addChild(searchPrompt)
|
stage.addChild(searchPrompt)
|
||||||
|
|
||||||
const searchText = new PIXI.Text('', {fontFamily: 'ShareTechMono', fontSize: 18, fill: 0xaaaaff})
|
const searchText = new PIXI.Text('', {fontFamily: 'ShareTechMono', fontSize: 18})
|
||||||
searchText.x = 40
|
searchText.x = 40
|
||||||
searchText.y = 5
|
searchText.y = 5
|
||||||
stage.addChild(searchText)
|
stage.addChild(searchText)
|
||||||
@@ -92,7 +99,6 @@ export default class App {
|
|||||||
viewContainer.y = 40
|
viewContainer.y = 40
|
||||||
stage.addChild(viewContainer)
|
stage.addChild(viewContainer)
|
||||||
|
|
||||||
|
|
||||||
const tooltip = new Tooltip()
|
const tooltip = new Tooltip()
|
||||||
tooltip.draw()
|
tooltip.draw()
|
||||||
stage.addChild(tooltip)
|
stage.addChild(tooltip)
|
||||||
@@ -122,7 +128,7 @@ export default class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
animatePodCreation(originalPod, globalX, globalY) {
|
animatePodCreation(originalPod, globalX, globalY) {
|
||||||
const pod = new Pod(originalPod.pod, this.tooltip, false)
|
const pod = new Pod(originalPod.pod, this.tooltip, null)
|
||||||
pod.draw()
|
pod.draw()
|
||||||
const targetPosition = new PIXI.Point(globalX, globalY)
|
const targetPosition = new PIXI.Point(globalX, globalY)
|
||||||
const angle = Math.random() * Math.PI * 2
|
const angle = Math.random() * Math.PI * 2
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Node from './node.js'
|
import Node from './node.js'
|
||||||
import {Pod} from './pod.js'
|
import { Pod } from './pod.js'
|
||||||
|
import { PRIMARY_VIOLET } from './colors.js'
|
||||||
const PIXI = require('pixi.js')
|
const PIXI = require('pixi.js')
|
||||||
|
|
||||||
export default class Cluster extends PIXI.Graphics {
|
export default class Cluster extends PIXI.Graphics {
|
||||||
@@ -12,7 +13,7 @@ export default class Cluster extends PIXI.Graphics {
|
|||||||
draw () {
|
draw () {
|
||||||
var rows = [10, 10]
|
var rows = [10, 10]
|
||||||
for (var node of this.cluster.nodes) {
|
for (var node of this.cluster.nodes) {
|
||||||
var nodeBox = new Node(node, this.tooltip)
|
var nodeBox = new Node(node, this, this.tooltip)
|
||||||
nodeBox.draw()
|
nodeBox.draw()
|
||||||
if (nodeBox.isMaster()) {
|
if (nodeBox.isMaster()) {
|
||||||
nodeBox.x = rows[0]
|
nodeBox.x = rows[0]
|
||||||
@@ -29,7 +30,7 @@ export default class Cluster extends PIXI.Graphics {
|
|||||||
|
|
||||||
|
|
||||||
for (const pod of this.cluster.unassigned_pods) {
|
for (const pod of this.cluster.unassigned_pods) {
|
||||||
var podBox = Pod.getOrCreate(pod, this.tooltip)
|
var podBox = Pod.getOrCreate(pod, this, this.tooltip)
|
||||||
podBox.x = rows[0]
|
podBox.x = rows[0]
|
||||||
podBox.y = 20
|
podBox.y = 20
|
||||||
podBox.draw()
|
podBox.draw()
|
||||||
@@ -37,12 +38,12 @@ export default class Cluster extends PIXI.Graphics {
|
|||||||
rows[0] += 20
|
rows[0] += 20
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lineStyle(2, 0xaaaaff, 1)
|
this.lineStyle(2, PRIMARY_VIOLET, 1)
|
||||||
const width = Math.max(rows[0], rows[1])
|
const width = Math.max(rows[0], rows[1])
|
||||||
this.drawRect(0, 0, width, nodeBox.height * 2 + 30)
|
this.drawRect(0, 0, width, nodeBox.height * 2 + 30)
|
||||||
|
|
||||||
var topHandle = new PIXI.Graphics()
|
var topHandle = new PIXI.Graphics()
|
||||||
topHandle.beginFill(0xaaaaff, 1)
|
topHandle.beginFill(PRIMARY_VIOLET, 1)
|
||||||
topHandle.drawRect(0, 0, width, 15)
|
topHandle.drawRect(0, 0, width, 15)
|
||||||
topHandle.endFill()
|
topHandle.endFill()
|
||||||
var text = new PIXI.Text(this.cluster.api_server_url, {fontFamily: 'ShareTechMono', fontSize: 10, fill: 0x000000})
|
var text = new PIXI.Text(this.cluster.api_server_url, {fontFamily: 'ShareTechMono', fontSize: 10, fill: 0x000000})
|
||||||
|
|||||||
3
app/src/colors.js
Normal file
3
app/src/colors.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const PRIMARY_VIOLET = 0xaaaaff
|
||||||
|
|
||||||
|
export { PRIMARY_VIOLET }
|
||||||
@@ -4,9 +4,10 @@ import {parseResource} from './utils.js'
|
|||||||
const PIXI = require('pixi.js')
|
const PIXI = require('pixi.js')
|
||||||
|
|
||||||
export default class Node extends PIXI.Graphics {
|
export default class Node extends PIXI.Graphics {
|
||||||
constructor(node, tooltip) {
|
constructor(node, cluster, tooltip) {
|
||||||
super()
|
super()
|
||||||
this.node = node
|
this.node = node
|
||||||
|
this.cluster = cluster
|
||||||
this.tooltip = tooltip
|
this.tooltip = tooltip
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ export default class Node extends PIXI.Graphics {
|
|||||||
var py = 20
|
var py = 20
|
||||||
for (const pod of this.node.pods) {
|
for (const pod of this.node.pods) {
|
||||||
if (pod.namespace != 'kube-system') {
|
if (pod.namespace != 'kube-system') {
|
||||||
const podBox = Pod.getOrCreate(pod, 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
|
||||||
podBox.y = py
|
podBox.y = py
|
||||||
nodeBox.addChild(podBox.draw())
|
nodeBox.addChild(podBox.draw())
|
||||||
@@ -98,7 +99,7 @@ export default class Node extends PIXI.Graphics {
|
|||||||
py = 100
|
py = 100
|
||||||
for (const pod of this.node.pods) {
|
for (const pod of this.node.pods) {
|
||||||
if (pod.namespace == 'kube-system') {
|
if (pod.namespace == 'kube-system') {
|
||||||
const podBox = Pod.getOrCreate(pod, 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
|
||||||
podBox.y = py
|
podBox.y = py
|
||||||
nodeBox.addChild(podBox.draw())
|
nodeBox.addChild(podBox.draw())
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ export const ALL_PODS = {}
|
|||||||
|
|
||||||
export class Pod extends PIXI.Graphics {
|
export class Pod extends PIXI.Graphics {
|
||||||
|
|
||||||
constructor(pod, tooltip, register=true) {
|
constructor(pod, tooltip, cluster) {
|
||||||
super()
|
super()
|
||||||
this.pod = pod
|
this.pod = pod
|
||||||
this.tooltip = tooltip
|
this.tooltip = tooltip
|
||||||
this.tick = null
|
this.tick = null
|
||||||
this._progress = 1
|
this._progress = 1
|
||||||
|
|
||||||
if (register) {
|
if (cluster) {
|
||||||
ALL_PODS[pod.namespace + '/' + pod.name] = this
|
ALL_PODS[cluster.cluster.api_server_url + '/' + pod.namespace + '/' + pod.name] = this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +51,8 @@ export class Pod extends PIXI.Graphics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getOrCreate(pod, tooltip) {
|
static getOrCreate(pod, cluster, tooltip) {
|
||||||
const existingPod = ALL_PODS[pod.namespace + '/' + pod.name]
|
const existingPod = ALL_PODS[cluster.cluster.api_server_url + '/' + pod.namespace + '/' + pod.name]
|
||||||
if (existingPod) {
|
if (existingPod) {
|
||||||
existingPod.pod = pod
|
existingPod.pod = pod
|
||||||
existingPod.clear()
|
existingPod.clear()
|
||||||
@@ -84,6 +84,7 @@ export class Pod extends PIXI.Graphics {
|
|||||||
const containerStatuses = this.pod.status.containerStatuses || []
|
const containerStatuses = this.pod.status.containerStatuses || []
|
||||||
var ready = 0
|
var ready = 0
|
||||||
var running = 0
|
var running = 0
|
||||||
|
var restarts = 0
|
||||||
for (const containerStatus of containerStatuses) {
|
for (const containerStatus of containerStatuses) {
|
||||||
if (containerStatus.ready) {
|
if (containerStatus.ready) {
|
||||||
ready++
|
ready++
|
||||||
@@ -91,6 +92,7 @@ export class Pod extends PIXI.Graphics {
|
|||||||
if (containerStatus.state.running) {
|
if (containerStatus.state.running) {
|
||||||
running++
|
running++
|
||||||
}
|
}
|
||||||
|
restarts += containerStatus.restartCount || 0
|
||||||
}
|
}
|
||||||
const allReady = ready >= containerStatuses.length
|
const allReady = ready >= containerStatuses.length
|
||||||
const allRunning = running >= containerStatuses.length
|
const allRunning = running >= containerStatuses.length
|
||||||
@@ -120,6 +122,9 @@ export class Pod extends PIXI.Graphics {
|
|||||||
// "CrashLoopBackOff"
|
// "CrashLoopBackOff"
|
||||||
s += ': ' + containerStatus.state[key].reason
|
s += ': ' + containerStatus.state[key].reason
|
||||||
}
|
}
|
||||||
|
if (containerStatus.restartCount) {
|
||||||
|
s += ' (' + containerStatus.restartCount + ' restarts)'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
s += '\nCPU:'
|
s += '\nCPU:'
|
||||||
s += '\n Requested: ' + (resources.cpu.requested / FACTORS.m).toFixed(0) + ' m'
|
s += '\n Requested: ' + (resources.cpu.requested / FACTORS.m).toFixed(0) + ' m'
|
||||||
@@ -181,6 +186,16 @@ export class Pod extends PIXI.Graphics {
|
|||||||
}
|
}
|
||||||
newTick = this.terminating
|
newTick = this.terminating
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (newTick) {
|
if (newTick) {
|
||||||
this.tick = newTick
|
this.tick = newTick
|
||||||
PIXI.ticker.shared.add(this.tick, this)
|
PIXI.ticker.shared.add(this.tick, this)
|
||||||
|
|||||||
Reference in New Issue
Block a user