├── .prettierrc.js ├── README.md ├── app.js ├── index.html └── preview.gif /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | jsxSingleQuote: true, 8 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # K8bit — the tiny Kubernetes dashboard 2 | 3 | K8bit is a tiny dashboard that is meant to demonstrate how to use the Kubernetes API to watch for changes. 4 | 5 | ![K8bit — the tiny Kubernetes dashboard](preview.gif) 6 | 7 | ## Usage 8 | 9 | You can start the dashboard with: 10 | 11 | ```bash 12 | $ kubectl proxy --www=. 13 | Starting to serve on 127.0.0.1:8001 14 | ``` 15 | 16 | Open the following URL . 17 | 18 | ## Related 19 | 20 | This project is inspired by [kube-ops-view](https://github.com/hjacobs/kube-ops-view), which is a fully featured dashboard for Kubernetes. -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const app = App() 2 | let lastResourceVersion 3 | 4 | fetch('/api/v1/pods') 5 | .then((response) => response.json()) 6 | .then((response) => { 7 | const pods = response.items 8 | lastResourceVersion = response.metadata.resourceVersion 9 | pods.forEach((pod) => { 10 | const podId = `${pod.metadata.namespace}-${pod.metadata.name}` 11 | app.upsert(podId, pod) 12 | }) 13 | }) 14 | .then(() => streamUpdates()) 15 | 16 | function streamUpdates() { 17 | fetch(`/api/v1/pods?watch=1&resourceVersion=${lastResourceVersion}`) 18 | .then((response) => { 19 | const stream = response.body.getReader() 20 | const utf8Decoder = new TextDecoder('utf-8') 21 | let buffer = '' 22 | 23 | return stream.read().then(function processText({ done, value }) { 24 | if (done) { 25 | console.log('Request terminated') 26 | return 27 | } 28 | buffer += utf8Decoder.decode(value) 29 | buffer = onNewLine(buffer, (chunk) => { 30 | if (chunk.trim().length === 0) { 31 | return 32 | } 33 | try { 34 | const event = JSON.parse(chunk) 35 | console.log('PROCESSING EVENT: ', event) 36 | const pod = event.object 37 | const podId = `${pod.metadata.namespace}-${pod.metadata.name}` 38 | switch (event.type) { 39 | case 'ADDED': { 40 | app.upsert(podId, pod) 41 | break 42 | } 43 | case 'DELETED': { 44 | app.remove(podId) 45 | break 46 | } 47 | case 'MODIFIED': { 48 | app.upsert(podId, pod) 49 | break 50 | } 51 | default: 52 | break 53 | } 54 | lastResourceVersion = event.object.metadata.resourceVersion 55 | } catch (error) { 56 | console.log('Error while parsing', chunk, '\n', error) 57 | } 58 | }) 59 | return stream.read().then(processText) 60 | }) 61 | }) 62 | .catch(() => { 63 | console.log('Error! Retrying in 5 seconds...') 64 | setTimeout(() => streamUpdates(), 5000) 65 | }) 66 | 67 | function onNewLine(buffer, fn) { 68 | const newLineIndex = buffer.indexOf('\n') 69 | if (newLineIndex === -1) { 70 | return buffer 71 | } 72 | const chunk = buffer.slice(0, buffer.indexOf('\n')) 73 | const newBuffer = buffer.slice(buffer.indexOf('\n') + 1) 74 | fn(chunk) 75 | return onNewLine(newBuffer, fn) 76 | } 77 | } 78 | 79 | function App() { 80 | const allPods = new Map() 81 | const content = document.querySelector('#content') 82 | 83 | function render() { 84 | const pods = Array.from(allPods.values()) 85 | if (pods.length === 0) { 86 | return 87 | } 88 | const podsByNode = groupBy(pods, (it) => it.nodeName) 89 | const nodeTemplates = Object.keys(podsByNode).map((nodeName) => { 90 | const pods = podsByNode[nodeName] 91 | return [ 92 | '
  • ', 93 | '
    ', 94 | `

    ${nodeName}

    `, 95 | `
    ${renderNode(pods)}
    `, 96 | '
    ', 97 | '
  • ', 98 | ].join('') 99 | }) 100 | 101 | content.innerHTML = `` 102 | 103 | function renderNode(pods) { 104 | return [ 105 | '', 116 | ].join('') 117 | } 118 | } 119 | 120 | return { 121 | upsert(podId, pod) { 122 | if (!pod.spec.nodeName) { 123 | return 124 | } 125 | allPods.set(podId, { 126 | name: pod.metadata.name, 127 | namespace: pod.metadata.namespace, 128 | nodeName: pod.spec.nodeName, 129 | }) 130 | render() 131 | }, 132 | remove(podId) { 133 | allPods.delete(podId) 134 | render() 135 | }, 136 | } 137 | } 138 | 139 | function groupBy(arr, groupByKeyFn) { 140 | return arr.reduce((acc, c) => { 141 | const key = groupByKeyFn(c) 142 | if (!(key in acc)) { 143 | acc[key] = [] 144 | } 145 | acc[key].push(c) 146 | return acc 147 | }, {}) 148 | } 149 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | K8bit 8 | 9 | 38 | 39 | 40 | 41 |
    42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/k8bit/76eed46da05d94d7508f34d56197c64b5f6c4b5b/preview.gif --------------------------------------------------------------------------------