From cdb437578fcc8a0ddc0b0ffca4dcf8695da6cd65 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Thu, 22 Dec 2016 10:57:24 +0100 Subject: [PATCH 1/5] #21 first mock data --- README.rst | 16 +++++++++++++++- app.py | 45 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index d346e5a..adbcc6c 100644 --- a/README.rst +++ b/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/ +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 ============= @@ -71,7 +83,9 @@ The following environment variables are supported: ``CREDENTIALS_DIR`` Directory to read (OAuth) credentials from --- these credentials are only used for non-localhost cluster URLs. ``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 diff --git a/app.py b/app.py index beec72b..9ec96a3 100755 --- a/app.py +++ b/app.py @@ -22,9 +22,10 @@ DEFAULT_CLUSTERS = 'http://localhost:8001/' CREDENTIALS_DIR = os.getenv('CREDENTIALS_DIR', '') AUTHORIZE_URL = os.getenv('AUTHORIZE_URL') APP_URL = os.getenv('APP_URL') +MOCK = os.getenv('MOCK', '').lower() == 'true' app = Flask(__name__) -app.debug = os.getenv('DEBUG') == 'true' +app.debug = os.getenv('DEBUG', '').lower() == 'true' app.secret_key = os.getenv('SECRET_KEY', 'development') oauth = OAuth(app) @@ -98,9 +99,35 @@ def index(): return flask.render_template('index.html', app_js=app_js) -@app.route('/kubernetes-clusters') -@authorize -def get_clusters(): +def generate_mock_cluster_data(index: int): + nodes = [] + for i in range(10): + labels = {} + if i < 2: + labels['master'] = 'true' + pods = [] + for j in range(32): + containers = [] + for k in range(1): + containers.append({'name': 'myapp', 'image': 'foo/bar/{}'.format(j), 'resources': {}}) + pods.append({'name': 'my-pod-{}'.format(j), 'namespace': 'default', 'labels': {}, '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 = [] for api_server_url in (os.getenv('CLUSTERS') or DEFAULT_CLUSTERS).split(','): if 'localhost' not in api_server_url: @@ -152,6 +179,16 @@ def get_clusters(): except: logging.exception('Failed to get metrics') 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=(',', ':')) From 8c22ea176efec32bcb338507432bb8b250561719 Mon Sep 17 00:00:00 2001 From: Shuaib Yunus Date: Thu, 22 Dec 2016 11:17:26 +0100 Subject: [PATCH 2/5] Add menubar and color constants file --- app/src/app.js | 12 +++++++++--- app/src/cluster.js | 7 ++++--- app/src/colors.js | 3 +++ 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 app/src/colors.js diff --git a/app/src/app.js b/app/src/app.js index f4a57be..f5f4c66 100644 --- a/app/src/app.js +++ b/app/src/app.js @@ -1,6 +1,7 @@ import Tooltip from './tooltip.js' import Cluster from './cluster.js' import { Pod, ALL_PODS } from './pod.js' +import { PRIMARY_VIOLET } from './colors.js' const PIXI = require('pixi.js') export default class App { @@ -46,7 +47,13 @@ export default class App { //Create a container object called the `stage` const stage = new PIXI.Container() - 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.y = 5 PIXI.ticker.shared.add(function(_) { @@ -55,7 +62,7 @@ export default class App { }) 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.y = 5 stage.addChild(searchText) @@ -65,7 +72,6 @@ export default class App { viewContainer.y = 40 stage.addChild(viewContainer) - const tooltip = new Tooltip() tooltip.draw() stage.addChild(tooltip) diff --git a/app/src/cluster.js b/app/src/cluster.js index aa7bc2d..3c290ca 100644 --- a/app/src/cluster.js +++ b/app/src/cluster.js @@ -1,5 +1,6 @@ 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') export default class Cluster extends PIXI.Graphics { @@ -37,12 +38,12 @@ export default class Cluster extends PIXI.Graphics { rows[0] += 20 } - this.lineStyle(2, 0xaaaaff, 1) + this.lineStyle(2, PRIMARY_VIOLET, 1) const width = Math.max(rows[0], rows[1]) this.drawRect(0, 0, width, nodeBox.height * 2 + 30) var topHandle = new PIXI.Graphics() - topHandle.beginFill(0xaaaaff, 1) + topHandle.beginFill(PRIMARY_VIOLET, 1) topHandle.drawRect(0, 0, width, 15) topHandle.endFill() var text = new PIXI.Text(this.cluster.api_server_url, {fontFamily: 'ShareTechMono', fontSize: 10, fill: 0x000000}) diff --git a/app/src/colors.js b/app/src/colors.js new file mode 100644 index 0000000..8a7b2cd --- /dev/null +++ b/app/src/colors.js @@ -0,0 +1,3 @@ +const PRIMARY_VIOLET = 0xaaaaff + +export { PRIMARY_VIOLET } From b0bcacfdf0c9553712428b72b523df65e51096b9 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Thu, 22 Dec 2016 12:07:15 +0100 Subject: [PATCH 3/5] #46 scope ALL_NODES to cluster --- app.py | 26 ++++++++++++++++++++------ app/src/app.js | 2 +- app/src/cluster.js | 4 ++-- app/src/node.js | 7 ++++--- app/src/pod.js | 10 +++++----- tox.ini | 3 +++ 6 files changed, 35 insertions(+), 17 deletions(-) create mode 100644 tox.ini diff --git a/app.py b/app.py index 9ec96a3..58c732f 100755 --- a/app.py +++ b/app.py @@ -99,24 +99,38 @@ def index(): return flask.render_template('index.html', app_js=app_js) +def hash_int(x: int): + x = ((x >> 16) ^ x) * 0x45d9f3b + 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(32): + 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': {}}) - pods.append({'name': 'my-pod-{}'.format(j), 'namespace': 'default', 'labels': {}, 'status': {}, 'containers': containers}) + containers.append({'name': 'myapp', 'image': 'foo/bar/{}'.format(j), 'resources': {'requests': {'cpu': '100m', 'memory': '100Mi'}}}) + status = {'phase': phase} + if phase == 'Running': + if j % 10 == 0: + status['containerStatuses'] = [{'ready': False, 'state': {'waiting': {'reason': 'CrashLoopBackOff'}}}] + 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 + 'api_server_url': 'https://kube-{}.example.org'.format(index), + 'nodes': nodes, + 'unassigned_pods': unassigned_pods } diff --git a/app/src/app.js b/app/src/app.js index f4a57be..ce5b3f7 100644 --- a/app/src/app.js +++ b/app/src/app.js @@ -95,7 +95,7 @@ export default class App { } animatePodCreation(originalPod, globalX, globalY) { - const pod = new Pod(originalPod.pod, this.tooltip, false) + const pod = new Pod(originalPod.pod, this.tooltip, null) pod.draw() const targetPosition = new PIXI.Point(globalX, globalY) const angle = Math.random()*Math.PI*2 diff --git a/app/src/cluster.js b/app/src/cluster.js index aa7bc2d..e552821 100644 --- a/app/src/cluster.js +++ b/app/src/cluster.js @@ -12,7 +12,7 @@ export default class Cluster extends PIXI.Graphics { draw () { var rows = [10, 10] for (var node of this.cluster.nodes) { - var nodeBox = new Node(node, this.tooltip) + var nodeBox = new Node(node, this, this.tooltip) nodeBox.draw() if (nodeBox.isMaster()) { nodeBox.x = rows[0] @@ -29,7 +29,7 @@ export default class Cluster extends PIXI.Graphics { 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.y = 20 podBox.draw() diff --git a/app/src/node.js b/app/src/node.js index f166812..03572ac 100644 --- a/app/src/node.js +++ b/app/src/node.js @@ -4,9 +4,10 @@ import {parseResource} from './utils.js' const PIXI = require('pixi.js') export default class Node extends PIXI.Graphics { - constructor(node, tooltip) { + constructor(node, cluster, tooltip) { super() this.node = node + this.cluster = cluster this.tooltip = tooltip } @@ -82,7 +83,7 @@ export default class Node extends PIXI.Graphics { var py = 20 for (const pod of this.node.pods) { 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.y = py nodeBox.addChild(podBox.draw()) @@ -98,7 +99,7 @@ export default class Node extends PIXI.Graphics { py = 100 for (const pod of this.node.pods) { 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.y = py nodeBox.addChild(podBox.draw()) diff --git a/app/src/pod.js b/app/src/pod.js index 23b9948..e60eae4 100644 --- a/app/src/pod.js +++ b/app/src/pod.js @@ -5,15 +5,15 @@ export const ALL_PODS = {} export class Pod extends PIXI.Graphics { - constructor(pod, tooltip, register=true) { + constructor(pod, tooltip, cluster) { super() this.pod = pod this.tooltip = tooltip this.tick = null this._progress = 1 - if (register) { - ALL_PODS[pod.namespace + '/' + pod.name] = this + if (cluster) { + 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) { - const existingPod = ALL_PODS[pod.namespace + '/' + pod.name] + static getOrCreate(pod, cluster, tooltip) { + const existingPod = ALL_PODS[cluster.cluster.api_server_url + '/' + pod.namespace + '/' + pod.name] if (existingPod) { existingPod.pod = pod existingPod.clear() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2eab242 --- /dev/null +++ b/tox.ini @@ -0,0 +1,3 @@ +[flake8] +max-line-length=120 +ignore=E402 From 8778ebfe9d83f5a8ab60410f74fef8f50da5db10 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Thu, 22 Dec 2016 14:08:12 +0100 Subject: [PATCH 4/5] #50 indicate pod restarts as red "ticks" and show number in tooltip --- .gitignore | 1 + app/src/pod.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/.gitignore b/.gitignore index a98e69f..9cc8fe2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__ **/node_modules/ static/build/ *-secret +npm-debug.log* diff --git a/app/src/pod.js b/app/src/pod.js index e60eae4..4ab1386 100644 --- a/app/src/pod.js +++ b/app/src/pod.js @@ -84,6 +84,7 @@ export class Pod extends PIXI.Graphics { const containerStatuses = this.pod.status.containerStatuses || [] var ready = 0 var running = 0 + var restarts = 0 for (const containerStatus of containerStatuses) { if (containerStatus.ready) { ready++ @@ -91,6 +92,7 @@ export class Pod extends PIXI.Graphics { if (containerStatus.state.running) { running++ } + restarts += containerStatus.restartCount || 0 } const allReady = ready >= containerStatuses.length const allRunning = running >= containerStatuses.length @@ -120,6 +122,9 @@ export class Pod extends PIXI.Graphics { // "CrashLoopBackOff" s += ': ' + containerStatus.state[key].reason } + if (containerStatus.restartCount) { + s += ' (' + containerStatus.restartCount + ' restarts)' + } } s += '\nCPU:' s += '\n Requested: ' + (resources.cpu.requested / FACTORS.m).toFixed(0) + ' m' @@ -181,6 +186,16 @@ export class Pod extends PIXI.Graphics { } newTick = this.terminating } + + + if (restarts) { + this.lineStyle(2, 0xff9999, 1) + for (let i=0; i Date: Thu, 22 Dec 2016 14:10:42 +0100 Subject: [PATCH 5/5] #50 add restarts to mock data --- app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app.py b/app.py index 58c732f..555d975 100755 --- a/app.py +++ b/app.py @@ -122,8 +122,10 @@ def generate_mock_cluster_data(index: int): containers.append({'name': 'myapp', 'image': 'foo/bar/{}'.format(j), 'resources': {'requests': {'cpu': '100m', 'memory': '100Mi'}}}) status = {'phase': phase} if phase == 'Running': - if j % 10 == 0: + 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 = []